changeset 2257:a437a986d399

merged beta into stable
author Marcin Kuzminski <marcin@python-works.com>
date Thu, 10 May 2012 20:27:45 +0200
parents deb816e5a579 (current diff) 790398822cad (diff)
children 90431eda6608
files docs/changelog.rst requires.txt rhodecode/__init__.py rhodecode/config/routing.py rhodecode/controllers/admin/ldap_settings.py rhodecode/controllers/admin/settings.py rhodecode/controllers/changelog.py rhodecode/controllers/changeset.py rhodecode/controllers/files.py rhodecode/controllers/summary.py rhodecode/lib/base.py rhodecode/lib/celerylib/tasks.py rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py rhodecode/lib/helpers.py rhodecode/lib/hooks.py rhodecode/lib/middleware/simplegit.py rhodecode/lib/middleware/simplehg.py rhodecode/lib/utils.py rhodecode/model/db.py rhodecode/model/forms.py rhodecode/model/repo.py rhodecode/model/scm.py rhodecode/model/user.py rhodecode/public/css/style.css rhodecode/templates/admin/settings/settings.html rhodecode/templates/changelog/changelog.html rhodecode/templates/changeset/changeset.html rhodecode/templates/files/files_annotate.html rhodecode/templates/files/files_browser.html rhodecode/templates/files/files_source.html rhodecode/tests/__init__.py
diffstat 67 files changed, 1487 insertions(+), 830 deletions(-) [+]
line wrap: on
line diff
--- a/docs/changelog.rst	Mon Apr 23 18:31:51 2012 +0200
+++ b/docs/changelog.rst	Thu May 10 20:27:45 2012 +0200
@@ -4,6 +4,39 @@
 Changelog
 =========
 
+1.3.5 (**2012-05-10**)
+----------------------
+
+news
+++++
+
+- use ext_json for json module
+- unified annotation view with file source view
+- notification improvements, better inbox + css
+- #419 don't strip passwords for login forms, make rhodecode 
+  more compatible with LDAP servers
+- Added HTTP_X_FORWARDED_FOR as another method of extracting 
+  IP for pull/push logs. - moved all to base controller  
+- #415: Adding comment to changeset causes reload. 
+  Comments are now added via ajax and doesn't reload the page
+- #374 LDAP config is discarded when LDAP can't be activated
+- limited push/pull operations are now logged for git in the journal
+- bumped mercurial to 2.2.X series
+- added support for displaying submodules in file-browser
+- #421 added bookmarks in changelog view
+
+fixes
++++++
+
+- fixed dev-version marker for stable when served from source codes
+- fixed missing permission checks on show forks page
+- #418 cast to unicode fixes in notification objects
+- #426 fixed mention extracting regex
+- fixed remote-pulling for git remotes remopositories
+- fixed #434: Error when accessing files or changesets of a git repository 
+  with submodules
+- fixed issue with empty APIKEYS for users after registration ref. #438
+- fixed issue with getting README files from git repositories
 
 1.3.4 (**2012-03-28**)
 ----------------------
--- a/docs/conf.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/docs/conf.py	Thu May 10 20:27:45 2012 +0200
@@ -11,7 +11,9 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys, os
+import sys
+import os
+import datetime
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -25,7 +27,9 @@
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
+              'sphinx.ext.intersphinx', 'sphinx.ext.todo',
+              'sphinx.ext.viewcode']
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -41,7 +45,7 @@
 
 # General information about the project.
 project = u'RhodeCode'
-copyright = u'2010, Marcin Kuzminski'
+copyright = u'%s, Marcin Kuzminski' % (datetime.datetime.now().year)
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
--- a/rhodecode/__init__.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/__init__.py	Thu May 10 20:27:45 2012 +0200
@@ -26,7 +26,7 @@
 import sys
 import platform
 
-VERSION = (1, 3, 4)
+VERSION = (1, 3, 5)
 
 try:
     from rhodecode.lib import get_current_revision
@@ -46,19 +46,22 @@
 PLATFORM_WIN = ('Windows')
 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
 
+is_windows = __platform__ in PLATFORM_WIN
+is_unix = __platform__ in PLATFORM_OTHERS
+
 requirements = [
     "Pylons==1.0.0",
     "Beaker==1.6.3",
     "WebHelpers==1.3",
     "formencode==1.2.4",
     "SQLAlchemy==0.7.6",
-    "Mako==0.6.2",
+    "Mako==0.7.0",
     "pygments>=1.4",
-    "whoosh>=2.3.0,<2.4",
+    "whoosh>=2.4.0,<2.5",
     "celery>=2.2.5,<2.3",
     "babel",
     "python-dateutil>=1.5.0,<2.0.0",
-    "dulwich>=0.8.4,<0.9.0",
+    "dulwich>=0.8.5,<0.9.0",
     "webob==1.0.8",
     "markdown==2.1.1",
     "docutils==0.8.1",
@@ -68,11 +71,11 @@
     requirements.append("simplejson")
     requirements.append("pysqlite")
 
-if __platform__ in PLATFORM_WIN:
-    requirements.append("mercurial>=2.1,<2.2")
+if is_windows:
+    requirements.append("mercurial>=2.2.1,<2.3")
 else:
     requirements.append("py-bcrypt")
-    requirements.append("mercurial>=2.1,<2.2")
+    requirements.append("mercurial>=2.2.1,<2.3")
 
 
 def get_version():
--- a/rhodecode/config/routing.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/config/routing.py	Thu May 10 20:27:45 2012 +0200
@@ -454,8 +454,8 @@
 
     rmap.connect('files_annotate_home',
                  '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
-                 controller='files', action='annotate', revision='tip',
-                 f_path='', conditions=dict(function=check_repo))
+                 controller='files', action='index', revision='tip',
+                 f_path='', annotate=True, conditions=dict(function=check_repo))
 
     rmap.connect('files_edit_home',
                  '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
--- a/rhodecode/controllers/admin/ldap_settings.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/admin/ldap_settings.py	Thu May 10 20:27:45 2012 +0200
@@ -100,25 +100,37 @@
         _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices],
                                  [x[0] for x in self.search_scope_choices],
                                  [x[0] for x in self.tls_kind_choices])()
+        # check the ldap lib
+        ldap_active = False
+        try:
+            import ldap
+            ldap_active = True
+        except ImportError:
+            pass
 
         try:
             form_result = _form.to_python(dict(request.POST))
+
             try:
 
                 for k, v in form_result.items():
                     if k.startswith('ldap_'):
+                        if k == 'ldap_active':
+                            v = ldap_active
                         setting = RhodeCodeSetting.get_by_name(k)
                         setting.app_settings_value = v
                         self.sa.add(setting)
 
                 self.sa.commit()
                 h.flash(_('Ldap settings updated successfully'),
-                    category='success')
+                        category='success')
+                if not ldap_active:
+                    #if ldap is missing send an info to user
+                    h.flash(_('Unable to activate ldap. The "python-ldap" library '
+                              'is missing.'), category='warning')
+
             except (DatabaseError,):
                 raise
-        except LdapImportError:
-            h.flash(_('Unable to activate ldap. The "python-ldap" library '
-                      'is missing.'), category='warning')
 
         except formencode.Invalid, errors:
             e = errors.error_dict or {}
--- a/rhodecode/controllers/admin/notifications.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/admin/notifications.py	Thu May 10 20:27:45 2012 +0200
@@ -30,6 +30,8 @@
 from pylons import tmpl_context as c, url
 from pylons.controllers.util import redirect
 
+from webhelpers.paginate import Page
+
 from rhodecode.lib.base import BaseController, render
 from rhodecode.model.db import Notification
 
@@ -58,8 +60,9 @@
         """GET /_admin/notifications: All items in the collection"""
         # url('notifications')
         c.user = self.rhodecode_user
-        c.notifications = NotificationModel()\
-                            .get_for_user(self.rhodecode_user.user_id)
+        notif = NotificationModel().get_for_user(self.rhodecode_user.user_id)
+        p = int(request.params.get('page', 1))
+        c.notifications = Page(notif, page=p, items_per_page=10)
         return render('admin/notifications/notifications.html')
 
     def mark_all_read(self):
@@ -69,7 +72,8 @@
             nm.mark_all_read_for_user(self.rhodecode_user.user_id)
             Session.commit()
             c.user = self.rhodecode_user
-            c.notifications = nm.get_for_user(self.rhodecode_user.user_id)
+            notif = nm.get_for_user(self.rhodecode_user.user_id)
+            c.notifications = Page(notif, page=1, items_per_page=10)
             return render('admin/notifications/notifications_data.html')
 
     def create(self):
--- a/rhodecode/controllers/admin/settings.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/admin/settings.py	Thu May 10 20:27:45 2012 +0200
@@ -26,6 +26,8 @@
 import logging
 import traceback
 import formencode
+import pkg_resources
+import platform
 
 from sqlalchemy import func
 from formencode import htmlfill
@@ -64,6 +66,11 @@
     def __before__(self):
         c.admin_user = session.get('admin_user')
         c.admin_username = session.get('admin_username')
+        c.modules = sorted([(p.project_name, p.version)
+                            for p in pkg_resources.working_set],
+                           key=lambda k: k[0].lower())
+        c.py_version = platform.python_version()
+        c.platform = platform.platform()
         super(SettingsController, self).__before__()
 
     @HasPermissionAllDecorator('hg.admin')
@@ -73,6 +80,7 @@
 
         defaults = RhodeCodeSetting.get_app_settings()
         defaults.update(self.get_hg_ui_settings())
+
         return htmlfill.render(
             render('admin/settings/settings.html'),
             defaults=defaults,
--- a/rhodecode/controllers/changelog.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/changelog.py	Thu May 10 20:27:45 2012 +0200
@@ -125,7 +125,8 @@
                 data.append(['', vtx, edges])
 
         elif repo.alias == 'hg':
-            c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs))
+            dag = graphmod.dagwalker(repo._repo, revs)
+            c.dag = graphmod.colored(dag, repo._repo)
             for (id, type, ctx, vtx, edges) in c.dag:
                 if type != graphmod.CHANGESET:
                     continue
--- a/rhodecode/controllers/changeset.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/changeset.py	Thu May 10 20:27:45 2012 +0200
@@ -359,16 +359,31 @@
 
         return render('changeset/raw_changeset.html')
 
+    @jsonify
     def comment(self, repo_name, revision):
-        ChangesetCommentsModel().create(text=request.POST.get('text'),
-                                        repo_id=c.rhodecode_db_repo.repo_id,
-                                        user_id=c.rhodecode_user.user_id,
-                                        revision=revision,
-                                        f_path=request.POST.get('f_path'),
-                                        line_no=request.POST.get('line'))
+        comm = ChangesetCommentsModel().create(
+            text=request.POST.get('text'),
+            repo_id=c.rhodecode_db_repo.repo_id,
+            user_id=c.rhodecode_user.user_id,
+            revision=revision,
+            f_path=request.POST.get('f_path'),
+            line_no=request.POST.get('line')
+        )
         Session.commit()
-        return redirect(h.url('changeset_home', repo_name=repo_name,
-                              revision=revision))
+        if not request.environ.get('HTTP_X_PARTIAL_XHR'):
+            return redirect(h.url('changeset_home', repo_name=repo_name,
+                                  revision=revision))
+
+        data = {
+           'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
+        }
+        if comm:
+            c.co = comm
+            data.update(comm.get_dict())
+            data.update({'rendered_text':
+                         render('changeset/changeset_comment_block.html')})
+
+        return data
 
     @jsonify
     def delete_comment(self, repo_name, comment_id):
--- a/rhodecode/controllers/files.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/files.py	Thu May 10 20:27:45 2012 +0200
@@ -48,6 +48,7 @@
 
 from rhodecode.model.repo import RepoModel
 from rhodecode.model.scm import ScmModel
+from rhodecode.model.db import Repository
 
 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
     _context_url, get_line_ctx, get_ignore_ws
@@ -112,7 +113,7 @@
 
     @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                    'repository.admin')
-    def index(self, repo_name, revision, f_path):
+    def index(self, repo_name, revision, f_path, annotate=False):
         # redirect to given revision from form if given
         post_revision = request.POST.get('at_rev', None)
         if post_revision:
@@ -123,7 +124,7 @@
         c.changeset = self.__get_cs_or_redirect(revision, repo_name)
         c.branch = request.GET.get('branch', None)
         c.f_path = f_path
-
+        c.annotate = annotate
         cur_rev = c.changeset.revision
 
         # prev link
@@ -168,7 +169,7 @@
         file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
 
         response.content_disposition = 'attachment; filename=%s' % \
-            safe_str(f_path.split(os.sep)[-1])
+            safe_str(f_path.split(Repository.url_sep())[-1])
 
         response.content_type = file_node.mimetype
         return file_node.content
@@ -219,16 +220,6 @@
         response.content_type = mimetype
         return file_node.content
 
-    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
-                                   'repository.admin')
-    def annotate(self, repo_name, revision, f_path):
-        c.cs = self.__get_cs_or_redirect(revision, repo_name)
-        c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
-
-        c.file_history = self._get_node_history(c.cs, f_path)
-        c.f_path = f_path
-        return render('files/files_annotate.html')
-
     @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
     def edit(self, repo_name, revision, f_path):
         r_post = request.POST
@@ -316,10 +307,10 @@
 
             try:
                 self.scm_model.create_node(repo=c.rhodecode_repo,
-                                             repo_name=repo_name, cs=c.cs,
-                                             user=self.rhodecode_user,
-                                             author=author, message=message,
-                                             content=content, f_path=node_path)
+                                           repo_name=repo_name, cs=c.cs,
+                                           user=self.rhodecode_user,
+                                           author=author, message=message,
+                                           content=content, f_path=node_path)
                 h.flash(_('Successfully committed to %s' % node_path),
                         category='success')
             except NodeAlreadyExistsError, e:
--- a/rhodecode/controllers/forks.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/forks.py	Thu May 10 20:27:45 2012 +0200
@@ -35,7 +35,7 @@
 
 from rhodecode.lib.helpers import Page
 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
-    NotAnonymous
+    NotAnonymous, HasRepoPermissionAny
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
 from rhodecode.model.repo import RepoModel
@@ -103,7 +103,13 @@
     def forks(self, repo_name):
         p = int(request.params.get('page', 1))
         repo_id = c.rhodecode_db_repo.repo_id
-        d = Repository.get_repo_forks(repo_id)
+        d = []
+        for r in Repository.get_repo_forks(repo_id):
+            if not HasRepoPermissionAny(
+                'repository.read', 'repository.write', 'repository.admin'
+            )(r.repo_name, 'get forks check'):
+                continue
+            d.append(r)
         c.forks_pager = Page(d, page=p, items_per_page=20)
 
         c.forks_data = render('/forks/forks_data.html')
--- a/rhodecode/controllers/summary.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/controllers/summary.py	Thu May 10 20:27:45 2012 +0200
@@ -179,10 +179,12 @@
         if c.enable_downloads:
             c.download_options = self._get_download_links(c.rhodecode_repo)
 
-        c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo)
+        c.readme_data, c.readme_file = self.__get_readme_data(
+            c.rhodecode_db_repo.repo_name, c.rhodecode_repo
+        )
         return render('summary/summary.html')
 
-    def __get_readme_data(self, repo):
+    def __get_readme_data(self, repo_name, repo):
 
         @cache_region('long_term')
         def _get_readme_from_cache(key):
@@ -190,7 +192,7 @@
             readme_file = None
             log.debug('Fetching readme file')
             try:
-                cs = repo.get_changeset('tip')
+                cs = repo.get_changeset()  # fetches TIP
                 renderer = MarkupRenderer()
                 for f in README_FILES:
                     try:
@@ -202,6 +204,7 @@
                     except NodeDoesNotExistError:
                         continue
             except ChangesetError:
+                log.error(traceback.format_exc())
                 pass
             except EmptyRepositoryError:
                 pass
@@ -210,7 +213,7 @@
 
             return readme_data, readme_file
 
-        key = repo.repo_name + '_README'
+        key = repo_name + '_README'
         inv = CacheInvalidation.invalidate(key)
         if inv is not None:
             region_invalidate(_get_readme_from_cache, None, key)
--- a/rhodecode/lib/base.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/base.py	Thu May 10 20:27:45 2012 +0200
@@ -116,6 +116,17 @@
 
         return True
 
+    def _get_ip_addr(self, environ):
+        proxy_key = 'HTTP_X_REAL_IP'
+        proxy_key2 = 'HTTP_X_FORWARDED_FOR'
+        def_key = 'REMOTE_ADDR'
+
+        return environ.get(proxy_key2,
+                           environ.get(proxy_key,
+                                       environ.get(def_key, '0.0.0.0')
+                            )
+                        )
+
     def __call__(self, environ, start_response):
         start = time.time()
         try:
--- a/rhodecode/lib/celerylib/tasks.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/celerylib/tasks.py	Thu May 10 20:27:45 2012 +0200
@@ -47,6 +47,7 @@
 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
 from rhodecode.lib.utils import add_cache, action_logger
 from rhodecode.lib.compat import json, OrderedDict
+from rhodecode.lib.hooks import log_create_repository
 
 from rhodecode.model.db import Statistics, Repository, User
 
@@ -372,7 +373,8 @@
 
     base_path = Repository.base_path()
 
-    RepoModel(DBS).create(form_data, cur_user, just_db=True, fork=True)
+    fork_repo = RepoModel(DBS).create(form_data, cur_user,
+                                      just_db=True, fork=True)
 
     alias = form_data['repo_type']
     org_repo_name = form_data['org_path']
@@ -387,6 +389,8 @@
     backend(safe_str(destination_fork_path), create=True,
             src_url=safe_str(source_repo_path),
             update_after_clone=update_after_clone)
+    log_create_repository(fork_repo.get_dict(), created_by=cur_user.username)
+
     action_logger(cur_user, 'user_forked_repo:%s' % fork_name,
                    org_repo_name, '', DBS)
 
@@ -395,6 +399,7 @@
     # finally commit at latest possible stage
     DBS.commit()
 
+
 def __get_codes_stats(repo_name):
     from rhodecode.config.conf import  LANGUAGES_EXTENSIONS_MAP
     repo = Repository.get_by_repo_name(repo_name).scm_instance
--- a/rhodecode/lib/compat.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/compat.py	Thu May 10 20:27:45 2012 +0200
@@ -25,92 +25,12 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
-import datetime
-import functools
-import decimal
 from rhodecode import __platform__, PLATFORM_WIN
 
 #==============================================================================
 # json
 #==============================================================================
-
-
-def _is_aware(value):
-    """
-    Determines if a given datetime.time is aware.
-
-    The logic is described in Python's docs:
-    http://docs.python.org/library/datetime.html#datetime.tzinfo
-    """
-    return (value.tzinfo is not None
-            and value.tzinfo.utcoffset(value) is not None)
-
-
-def _obj_dump(obj):
-    """
-    Custom function for dumping objects to JSON, if obj has __json__ attribute
-    or method defined it will be used for serialization
-
-    :param obj:
-    """
-
-    if isinstance(obj, complex):
-        return [obj.real, obj.imag]
-    # See "Date Time String Format" in the ECMA-262 specification.
-    # some code borrowed from django 1.4
-    elif isinstance(obj, datetime.datetime):
-        r = obj.isoformat()
-        if obj.microsecond:
-            r = r[:23] + r[26:]
-        if r.endswith('+00:00'):
-            r = r[:-6] + 'Z'
-        return r
-    elif isinstance(obj, datetime.date):
-        return obj.isoformat()
-    elif isinstance(obj, decimal.Decimal):
-        return str(obj)
-    elif isinstance(obj, datetime.time):
-        if _is_aware(obj):
-            raise ValueError("JSON can't represent timezone-aware times.")
-        r = obj.isoformat()
-        if obj.microsecond:
-            r = r[:12]
-        return r
-    elif isinstance(obj, set):
-        return list(obj)
-    elif isinstance(obj, OrderedDict):
-        return obj.as_dict()
-    elif hasattr(obj, '__json__'):
-        if callable(obj.__json__):
-            return obj.__json__()
-        else:
-            return obj.__json__
-    else:
-        raise NotImplementedError
-
-try:
-    import json
-
-    # extended JSON encoder for json
-    class ExtendedEncoder(json.JSONEncoder):
-        def default(self, obj):
-            try:
-                return _obj_dump(obj)
-            except NotImplementedError:
-                pass
-            return json.JSONEncoder.default(self, obj)
-    # monkey-patch JSON encoder to use extended version
-    json.dumps = functools.partial(json.dumps, cls=ExtendedEncoder)
-except ImportError:
-    import simplejson as json
-
-    def extended_encode(obj):
-        try:
-            return _obj_dump(obj)
-        except NotImplementedError:
-            pass
-        raise TypeError("%r is not JSON serializable" % (obj,))
-    json.dumps = functools.partial(json.dumps, default=extended_encode)
+from rhodecode.lib.ext_json import json
 
 
 #==============================================================================
--- a/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py	Thu May 10 20:27:45 2012 +0200
@@ -71,7 +71,6 @@
     is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
     is_ldap.drop(User().__table__)
 
-
     #==========================================================================
     # Upgrade of `repositories` table
     #==========================================================================
@@ -100,7 +99,6 @@
 
     group_id.create(Repository().__table__)
 
-
     #==========================================================================
     # Upgrade of `user_followings` table
     #==========================================================================
--- a/rhodecode/lib/diffs.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/diffs.py	Thu May 10 20:27:45 2012 +0200
@@ -33,8 +33,8 @@
 from pylons.i18n.translation import _
 
 from rhodecode.lib.vcs.exceptions import VCSError
-from rhodecode.lib.vcs.nodes import FileNode
-
+from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
+from rhodecode.lib.helpers import escape
 from rhodecode.lib.utils import EmptyChangeset
 
 
@@ -79,9 +79,13 @@
                                'diff menu to display this diff'))
         stats = (0, 0)
         size = 0
-
     if not diff:
-        diff = wrap_to_table(_('No changes detected'))
+        submodules = filter(lambda o: isinstance(o, SubModuleNode),
+                            [filenode_new, filenode_old])
+        if submodules:
+            diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
+        else:
+            diff = wrap_to_table(_('No changes detected'))
 
     cs1 = filenode_old.changeset.raw_id
     cs2 = filenode_new.changeset.raw_id
@@ -97,6 +101,10 @@
     """
     # make sure we pass in default context
     context = context or 3
+    submodules = filter(lambda o: isinstance(o, SubModuleNode),
+                        [filenode_new, filenode_old])
+    if submodules:
+        return ''
 
     for filenode in (filenode_old, filenode_new):
         if not isinstance(filenode, FileNode):
@@ -109,7 +117,6 @@
 
     vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
                                  ignore_whitespace, context)
-
     return vcs_gitdiff
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/lib/ext_json.py	Thu May 10 20:27:45 2012 +0200
@@ -0,0 +1,104 @@
+import datetime
+import functools
+import decimal
+
+__all__ = ['json', 'simplejson', 'stdjson']
+
+
+def _is_aware(value):
+    """
+    Determines if a given datetime.time is aware.
+
+    The logic is described in Python's docs:
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+    """
+    return (value.tzinfo is not None
+            and value.tzinfo.utcoffset(value) is not None)
+
+
+def _obj_dump(obj):
+    """
+    Custom function for dumping objects to JSON, if obj has __json__ attribute
+    or method defined it will be used for serialization
+
+    :param obj:
+    """
+
+    if isinstance(obj, complex):
+        return [obj.real, obj.imag]
+    # See "Date Time String Format" in the ECMA-262 specification.
+    # some code borrowed from django 1.4
+    elif isinstance(obj, datetime.datetime):
+        r = obj.isoformat()
+        if obj.microsecond:
+            r = r[:23] + r[26:]
+        if r.endswith('+00:00'):
+            r = r[:-6] + 'Z'
+        return r
+    elif isinstance(obj, datetime.date):
+        return obj.isoformat()
+    elif isinstance(obj, decimal.Decimal):
+        return str(obj)
+    elif isinstance(obj, datetime.time):
+        if _is_aware(obj):
+            raise ValueError("JSON can't represent timezone-aware times.")
+        r = obj.isoformat()
+        if obj.microsecond:
+            r = r[:12]
+        return r
+    elif isinstance(obj, set):
+        return list(obj)
+    elif hasattr(obj, '__json__'):
+        if callable(obj.__json__):
+            return obj.__json__()
+        else:
+            return obj.__json__
+    else:
+        raise NotImplementedError
+
+
+# Import simplejson
+try:
+    # import simplejson initially
+    import simplejson as _sj
+
+    def extended_encode(obj):
+        try:
+            return _obj_dump(obj)
+        except NotImplementedError:
+            pass
+        raise TypeError("%r is not JSON serializable" % (obj,))
+    # we handle decimals our own it makes unified behavior of json vs
+    # simplejson
+    _sj.dumps = functools.partial(_sj.dumps, default=extended_encode,
+                                  use_decimal=False)
+    _sj.dump = functools.partial(_sj.dump, default=extended_encode,
+                                 use_decimal=False)
+    simplejson = _sj
+
+except ImportError:
+    # no simplejson set it to None
+    _sj = None
+
+
+# simplejson not found try out regular json module
+import json as _json
+
+
+# extended JSON encoder for json
+class ExtendedEncoder(_json.JSONEncoder):
+    def default(self, obj):
+        try:
+            return _obj_dump(obj)
+        except NotImplementedError:
+            pass
+        return _json.JSONEncoder.default(self, obj)
+# monkey-patch JSON encoder to use extended version
+_json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder)
+_json.dump = functools.partial(_json.dump, cls=ExtendedEncoder)
+stdlib = _json
+
+# set all available json modules
+simplejson = _sj
+stdjson = _json
+json = _sj if _sj else _json
--- a/rhodecode/lib/helpers.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/helpers.py	Thu May 10 20:27:45 2012 +0200
@@ -87,7 +87,7 @@
     if not token_key in session:
         try:
             token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
-        except AttributeError: # Python < 2.4
+        except AttributeError:  # Python < 2.4
             token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
         session[token_key] = token
         if hasattr(session, 'save'):
@@ -454,11 +454,14 @@
                         revision=rev.raw_id),
                     title=tooltip(message(rev)), class_='tooltip')
         )
-        # get only max revs_top_limit of changeset for performance/ui reasons
-        revs = [
-            x for x in repo.get_changesets(revs_ids[0],
-                                           revs_ids[:revs_top_limit][-1])
-        ]
+
+        revs = []
+        if len(filter(lambda v: v != '', revs_ids)) > 0:
+            # get only max revs_top_limit of changeset for performance/ui reasons
+            revs = [
+                x for x in repo.get_changesets(revs_ids[0],
+                                               revs_ids[:revs_top_limit][-1])
+            ]
 
         cs_links = []
         cs_links.append(" " + ', '.join(
--- a/rhodecode/lib/hooks.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/hooks.py	Thu May 10 20:27:45 2012 +0200
@@ -33,6 +33,33 @@
 from inspect import isfunction
 
 
+def _get_scm_size(alias, root_path):
+
+    if not alias.startswith('.'):
+        alias += '.'
+
+    size_scm, size_root = 0, 0
+    for path, dirs, files in os.walk(root_path):
+        if path.find(alias) != -1:
+            for f in files:
+                try:
+                    size_scm += os.path.getsize(os.path.join(path, f))
+                except OSError:
+                    pass
+        else:
+            for f in files:
+                try:
+                    size_root += os.path.getsize(os.path.join(path, f))
+                except OSError:
+                    pass
+
+    size_scm_f = h.format_byte_size(size_scm)
+    size_root_f = h.format_byte_size(size_root)
+    size_total_f = h.format_byte_size(size_root + size_scm)
+
+    return size_scm_f, size_root_f, size_total_f
+
+
 def repo_size(ui, repo, hooktype=None, **kwargs):
     """
     Presents size of repository after push
@@ -42,24 +69,7 @@
     :param hooktype:
     """
 
-    size_hg, size_root = 0, 0
-    for path, dirs, files in os.walk(repo.root):
-        if path.find('.hg') != -1:
-            for f in files:
-                try:
-                    size_hg += os.path.getsize(os.path.join(path, f))
-                except OSError:
-                    pass
-        else:
-            for f in files:
-                try:
-                    size_root += os.path.getsize(os.path.join(path, f))
-                except OSError:
-                    pass
-
-    size_hg_f = h.format_byte_size(size_hg)
-    size_root_f = h.format_byte_size(size_root)
-    size_total_f = h.format_byte_size(size_root + size_hg)
+    size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
 
     last_cs = repo[len(repo) - 1]
 
@@ -82,6 +92,7 @@
     extras = dict(repo.ui.configitems('rhodecode_extras'))
     username = extras['username']
     repository = extras['repository']
+    scm = extras['scm']
     action = 'pull'
 
     action_logger(username, action, repository, extras['ip'], commit=True)
@@ -100,28 +111,33 @@
     Maps user last push action to new changeset id, from mercurial
 
     :param ui:
-    :param repo:
+    :param repo: repo object containing the `ui` object
     """
 
     extras = dict(repo.ui.configitems('rhodecode_extras'))
     username = extras['username']
     repository = extras['repository']
     action = extras['action'] + ':%s'
-    node = kwargs['node']
+    scm = extras['scm']
 
-    def get_revs(repo, rev_opt):
-        if rev_opt:
-            revs = revrange(repo, rev_opt)
+    if scm == 'hg':
+        node = kwargs['node']
+
+        def get_revs(repo, rev_opt):
+            if rev_opt:
+                revs = revrange(repo, rev_opt)
 
-            if len(revs) == 0:
-                return (nullrev, nullrev)
-            return (max(revs), min(revs))
-        else:
-            return (len(repo) - 1, 0)
+                if len(revs) == 0:
+                    return (nullrev, nullrev)
+                return (max(revs), min(revs))
+            else:
+                return (len(repo) - 1, 0)
 
-    stop, start = get_revs(repo, [node + ':'])
+        stop, start = get_revs(repo, [node + ':'])
 
-    revs = (str(repo[r]) for r in xrange(start, stop + 1))
+        revs = (str(repo[r]) for r in xrange(start, stop + 1))
+    elif scm == 'git':
+        revs = []
 
     action = action % ','.join(revs)
 
--- a/rhodecode/lib/markup_renderer.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/markup_renderer.py	Thu May 10 20:27:45 2012 +0200
@@ -27,7 +27,7 @@
 import re
 import logging
 
-from rhodecode.lib.utils2 import safe_unicode
+from rhodecode.lib.utils2 import safe_unicode, MENTIONS_REGEX
 
 log = logging.getLogger(__name__)
 
@@ -128,10 +128,10 @@
 
     @classmethod
     def rst_with_mentions(cls, source):
-        mention_pat = re.compile(r'(?:^@|\s@)(\w+)')
+        mention_pat = re.compile(MENTIONS_REGEX)
 
         def wrapp(match_obj):
             uname = match_obj.groups()[0]
-            return ' **@%(uname)s** ' % {'uname':uname}
+            return ' **@%(uname)s** ' % {'uname': uname}
         mention_hl = mention_pat.sub(wrapp, source).strip()
         return cls.rst(mention_hl)
--- a/rhodecode/lib/middleware/simplegit.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/middleware/simplegit.py	Thu May 10 20:27:45 2012 +0200
@@ -44,13 +44,14 @@
           graph_walker.determine_wants, graph_walker, self.progress,
           get_tagged=self.get_tagged)
 
-        # Do they want any objects?
-        if objects_iter is None or len(objects_iter) == 0:
+        # Did the process short-circuit (e.g. in a stateless RPC call)? Note
+        # that the client still expects a 0-object pack in most cases.
+        if objects_iter is None:
             return
 
         self.progress("counting objects: %d, done.\n" % len(objects_iter))
         dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
-                                  objects_iter, len(objects_iter))
+                                     objects_iter)
         messages = []
         messages.append('thank you for using rhodecode')
 
@@ -59,6 +60,7 @@
         # we are done
         self.proto.write("0000")
 
+
 dulserver.DEFAULT_HANDLERS = {
   'git-upload-pack': SimpleGitUploadPackHandler,
   'git-receive-pack': dulserver.ReceivePackHandler,
@@ -72,7 +74,7 @@
 from rhodecode.lib.utils2 import safe_str
 from rhodecode.lib.base import BaseVCSController
 from rhodecode.lib.auth import get_container_username
-from rhodecode.lib.utils import is_valid_repo
+from rhodecode.lib.utils import is_valid_repo, make_ui
 from rhodecode.model.db import User
 
 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
@@ -99,10 +101,9 @@
         if not is_git(environ):
             return self.application(environ, start_response)
 
-        proxy_key = 'HTTP_X_REAL_IP'
-        def_key = 'REMOTE_ADDR'
-        ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
+        ipaddr = self._get_ip_addr(environ)
         username = None
+        self._git_first_op = False
         # skip passing error to error controller
         environ['pylons.status_code_redirect'] = True
 
@@ -178,6 +179,13 @@
                     perm = self._check_permission(action, user, repo_name)
                     if perm is not True:
                         return HTTPForbidden()(environ, start_response)
+        extras = {
+            'ip': ipaddr,
+            'username': username,
+            'action': action,
+            'repository': repo_name,
+            'scm': 'git',
+        }
 
         #===================================================================
         # GIT REQUEST HANDLING
@@ -185,10 +193,16 @@
         repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
         log.debug('Repository path is %s' % repo_path)
 
+        baseui = make_ui('db')
+        self.__inject_extras(repo_path, baseui, extras)
+
+
         try:
-            #invalidate cache on push
+            # invalidate cache on push
             if action == 'push':
                 self._invalidate_cache(repo_name)
+            self._handle_githooks(repo_name, action, baseui, environ)
+
             log.info('%s action on GIT repo "%s"' % (action, repo_name))
             app = self.__make_app(repo_name, repo_path)
             return app(environ, start_response)
@@ -249,3 +263,38 @@
             # operation is pull/push
             op = getattr(self, '_git_stored_op', 'pull')
         return op
+
+    def _handle_githooks(self, repo_name, action, baseui, environ):
+        from rhodecode.lib.hooks import log_pull_action, log_push_action
+        service = environ['QUERY_STRING'].split('=')
+        if len(service) < 2:
+            return
+
+        from rhodecode.model.db import Repository
+        _repo = Repository.get_by_repo_name(repo_name)
+        _repo = _repo.scm_instance
+        _repo._repo.ui = baseui
+
+        push_hook = 'pretxnchangegroup.push_logger'
+        pull_hook = 'preoutgoing.pull_logger'
+        _hooks = dict(baseui.configitems('hooks')) or {}
+        if action == 'push' and _hooks.get(push_hook):
+            log_push_action(ui=baseui, repo=_repo._repo)
+        elif action == 'pull' and _hooks.get(pull_hook):
+            log_pull_action(ui=baseui, repo=_repo._repo)
+
+    def __inject_extras(self, repo_path, baseui, extras={}):
+        """
+        Injects some extra params into baseui instance
+
+        :param baseui: baseui instance
+        :param extras: dict with extra params to put into baseui
+        """
+
+        # make our hgweb quiet so it doesn't print output
+        baseui.setconfig('ui', 'quiet', 'true')
+
+        #inject some additional parameters that will be available in ui
+        #for hooks
+        for k, v in extras.items():
+            baseui.setconfig('rhodecode_extras', k, v)
--- a/rhodecode/lib/middleware/simplehg.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/middleware/simplehg.py	Thu May 10 20:27:45 2012 +0200
@@ -69,9 +69,7 @@
         if not is_mercurial(environ):
             return self.application(environ, start_response)
 
-        proxy_key = 'HTTP_X_REAL_IP'
-        def_key = 'REMOTE_ADDR'
-        ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
+        ipaddr = self._get_ip_addr(environ)
 
         # skip passing error to error controller
         environ['pylons.status_code_redirect'] = True
@@ -155,7 +153,8 @@
             'ip': ipaddr,
             'username': username,
             'action': action,
-            'repository': repo_name
+            'repository': repo_name,
+            'scm': 'hg',
         }
 
         #======================================================================
--- a/rhodecode/lib/utils.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/utils.py	Thu May 10 20:27:45 2012 +0200
@@ -150,7 +150,7 @@
 
         user_log = UserLog()
         user_log.user_id = user_obj.user_id
-        user_log.action = action
+        user_log.action = safe_unicode(action)
 
         user_log.repository_id = repo_obj.repo_id
         user_log.repository_name = repo_name
--- a/rhodecode/lib/utils2.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/utils2.py	Thu May 10 20:27:45 2012 +0200
@@ -392,14 +392,17 @@
     return cs
 
 
+MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
+
+
 def extract_mentioned_users(s):
     """
     Returns unique usernames from given string s that have @mention
 
     :param s: string to get mentions
     """
-    usrs = {}
-    for username in re.findall(r'(?:^@|\s@)(\w+)', s):
-        usrs[username] = username
+    usrs = set()
+    for username in re.findall(MENTIONS_REGEX, s):
+        usrs.add(username)
 
-    return sorted(usrs.keys())
+    return sorted(list(usrs), key=lambda k: k.lower())
--- a/rhodecode/lib/vcs/backends/base.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/backends/base.py	Thu May 10 20:27:45 2012 +0200
@@ -909,3 +909,48 @@
         :raises ``CommitError``: if any error occurs while committing
         """
         raise NotImplementedError
+
+
+class EmptyChangeset(BaseChangeset):
+    """
+    An dummy empty changeset. It's possible to pass hash when creating
+    an EmptyChangeset
+    """
+
+    def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
+                 alias=None):
+        self._empty_cs = cs
+        self.revision = -1
+        self.message = ''
+        self.author = ''
+        self.date = ''
+        self.repository = repo
+        self.requested_revision = requested_revision
+        self.alias = alias
+
+    @LazyProperty
+    def raw_id(self):
+        """
+        Returns raw string identifying this changeset, useful for web
+        representation.
+        """
+
+        return self._empty_cs
+
+    @LazyProperty
+    def branch(self):
+        from rhodecode.lib.vcs.backends import get_backend
+        return get_backend(self.alias).DEFAULT_BRANCH_NAME
+
+    @LazyProperty
+    def short_id(self):
+        return self.raw_id[:12]
+
+    def get_file_changeset(self, path):
+        return self
+
+    def get_file_content(self, path):
+        return u''
+
+    def get_file_size(self, path):
+        return 0
--- a/rhodecode/lib/vcs/backends/git/changeset.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/backends/git/changeset.py	Thu May 10 20:27:45 2012 +0200
@@ -10,7 +10,8 @@
 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
 from rhodecode.lib.vcs.backends.base import BaseChangeset
-from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode
+from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
+    RemovedFileNode, SubModuleNode
 from rhodecode.lib.vcs.utils import safe_unicode
 from rhodecode.lib.vcs.utils import date_fromtimestamp
 from rhodecode.lib.vcs.utils.lazy import LazyProperty
@@ -66,26 +67,12 @@
 
     @LazyProperty
     def branch(self):
-        # TODO: Cache as we walk (id <-> branch name mapping)
-        refs = self.repository._repo.get_refs()
-        heads = {}
-        for key, val in refs.items():
-            for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
-                if key.startswith(ref_key):
-                    n = key[len(ref_key):]
-                    if n not in ['HEAD']:
-                        heads[n] = val
+
+        heads = self.repository._heads(reverse=False)
 
-        for name, id in heads.iteritems():
-            walker = self.repository._repo.object_store.get_graph_walker([id])
-            while True:
-                id_ = walker.next()
-                if not id_:
-                    break
-                if id_ == self.id:
-                    return safe_unicode(name)
-        raise ChangesetError("This should not happen... Have you manually "
-                             "change id of the changeset?")
+        ref = heads.get(self.raw_id)
+        if ref:
+            return safe_unicode(ref)
 
     def _fix_path(self, path):
         """
@@ -144,7 +131,6 @@
                         name = item
                     self._paths[name] = id
                     self._stat_modes[name] = stat
-
             if not path in self._paths:
                 raise NodeDoesNotExistError("There is no file nor directory "
                     "at the given path %r at revision %r"
@@ -344,7 +330,13 @@
         tree = self.repository._repo[id]
         dirnodes = []
         filenodes = []
+        als = self.repository.alias
         for name, stat, id in tree.iteritems():
+            if objects.S_ISGITLINK(stat):
+                dirnodes.append(SubModuleNode(name, url=None, changeset=id,
+                                              alias=als))
+                continue
+
             obj = self.repository._repo.get_object(id)
             if path != '':
                 obj_path = '/'.join((path, name))
@@ -372,24 +364,31 @@
         path = self._fix_path(path)
         if not path in self.nodes:
             try:
-                id = self._get_id_for_path(path)
+                id_ = self._get_id_for_path(path)
             except ChangesetError:
                 raise NodeDoesNotExistError("Cannot find one of parents' "
                     "directories for a given path: %s" % path)
-            obj = self.repository._repo.get_object(id)
-            if isinstance(obj, objects.Tree):
-                if path == '':
-                    node = RootNode(changeset=self)
+
+            als = self.repository.alias
+            _GL = lambda m: m and objects.S_ISGITLINK(m)
+            if _GL(self._stat_modes.get(path)):
+                node = SubModuleNode(path, url=None, changeset=id_, alias=als)
+            else:
+                obj = self.repository._repo.get_object(id_)
+
+                if isinstance(obj, objects.Tree):
+                    if path == '':
+                        node = RootNode(changeset=self)
+                    else:
+                        node = DirNode(path, changeset=self)
+                    node._tree = obj
+                elif isinstance(obj, objects.Blob):
+                    node = FileNode(path, changeset=self)
+                    node._blob = obj
                 else:
-                    node = DirNode(path, changeset=self)
-                node._tree = obj
-            elif isinstance(obj, objects.Blob):
-                node = FileNode(path, changeset=self)
-                node._blob = obj
-            else:
-                raise NodeDoesNotExistError("There is no file nor directory "
-                    "at the given path %r at revision %r"
-                    % (path, self.short_id))
+                    raise NodeDoesNotExistError("There is no file nor directory "
+                        "at the given path %r at revision %r"
+                        % (path, self.short_id))
             # cache node
             self.nodes[path] = node
         return self.nodes[path]
@@ -406,7 +405,7 @@
     def _diff_name_status(self):
         output = []
         for parent in self.parents:
-            cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
+            cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
             so, se = self.repository.run_git_command(cmd)
             output.append(so.strip())
         return '\n'.join(output)
@@ -422,13 +421,15 @@
         for line in self._diff_name_status.splitlines():
             if not line:
                 continue
+
             if line.startswith(char):
-                splitted = line.split(char,1)
+                splitted = line.split(char, 1)
                 if not len(splitted) == 2:
                     raise VCSError("Couldn't parse diff result:\n%s\n\n and "
                         "particularly that line: %s" % (self._diff_name_status,
                         line))
-                paths.add(splitted[1].strip())
+                _path = splitted[1].strip()
+                paths.add(_path)
         return sorted(paths)
 
     @LazyProperty
--- a/rhodecode/lib/vcs/backends/git/inmemory.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/backends/git/inmemory.py	Thu May 10 20:27:45 2012 +0200
@@ -5,12 +5,13 @@
 from dulwich.repo import Repo
 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
 from rhodecode.lib.vcs.exceptions import RepositoryError
+from rhodecode.lib.vcs.utils import safe_str
 
 
 class GitInMemoryChangeset(BaseInMemoryChangeset):
 
     def commit(self, message, author, parents=None, branch=None, date=None,
-            **kwargs):
+               **kwargs):
         """
         Performs in-memory commit (doesn't check workdir in any way) and
         returns newly created ``Changeset``. Updates repository's
@@ -120,9 +121,9 @@
         commit = objects.Commit()
         commit.tree = commit_tree.id
         commit.parents = [p._commit.id for p in self.parents if p]
-        commit.author = commit.committer = author
+        commit.author = commit.committer = safe_str(author)
         commit.encoding = ENCODING
-        commit.message = message + ' '
+        commit.message = safe_str(message) + ' '
 
         # Compute date
         if date is None:
--- a/rhodecode/lib/vcs/backends/git/repository.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/backends/git/repository.py	Thu May 10 20:27:45 2012 +0200
@@ -47,6 +47,15 @@
 
         self.path = abspath(repo_path)
         self._repo = self._get_repo(create, src_url, update_after_clone, bare)
+        #temporary set that to now at later we will move it to constructor
+        baseui = None
+        if baseui is None:
+            from mercurial.ui import ui
+            baseui = ui()
+        # patch the instance of GitRepo with an "FAKE" ui object to add
+        # compatibility layer with Mercurial
+        setattr(self._repo, 'ui', baseui)
+
         try:
             self.head = self._repo.head()
         except KeyError:
@@ -78,11 +87,16 @@
 
         :param cmd: git command to be executed
         """
-        #cmd = '(cd %s && git %s)' % (self.path, cmd)
+
+        _copts = ['-c', 'core.quotepath=false', ]
+        _str_cmd = False
         if isinstance(cmd, basestring):
-            cmd = 'git %s' % cmd
-        else:
-            cmd = ['git'] + cmd
+            cmd = [cmd]
+            _str_cmd = True
+
+        cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
+        if _str_cmd:
+            cmd = ' '.join(cmd)
         try:
             opts = dict(
                 shell=isinstance(cmd, basestring),
@@ -245,6 +259,19 @@
             if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
         return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
 
+    def _heads(self, reverse=False):
+        refs = self._repo.get_refs()
+        heads = {}
+
+        for key, val in refs.items():
+            for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
+                if key.startswith(ref_key):
+                    n = key[len(ref_key):]
+                    if n not in ['HEAD']:
+                        heads[n] = val
+
+        return heads if reverse else dict((y,x) for x,y in heads.iteritems())
+
     def _get_tags(self):
         if not self.revisions:
             return {}
@@ -384,7 +411,7 @@
             yield self.get_changeset(rev)
 
     def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
-            context=3):
+                 context=3):
         """
         Returns (git like) *diff*, as plain text. Shows changes introduced by
         ``rev2`` since ``rev1``.
@@ -453,6 +480,18 @@
         # If error occurs run_git_command raises RepositoryError already
         self.run_git_command(cmd)
 
+    def pull(self, url):
+        """
+        Tries to pull changes from external location.
+        """
+        url = self._get_url(url)
+        cmd = ['pull']
+        cmd.append("--ff-only")
+        cmd.append(url)
+        cmd = ' '.join(cmd)
+        # If error occurs run_git_command raises RepositoryError already
+        self.run_git_command(cmd)
+
     @LazyProperty
     def workdir(self):
         """
--- a/rhodecode/lib/vcs/backends/hg/changeset.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/backends/hg/changeset.py	Thu May 10 20:27:45 2012 +0200
@@ -5,8 +5,9 @@
 from rhodecode.lib.vcs.conf import settings
 from rhodecode.lib.vcs.exceptions import  ChangesetDoesNotExistError, \
     ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
-from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, ChangedFileNodesGenerator, \
-    DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode
+from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
+    ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
+    RemovedFileNodesGenerator, RootNode, SubModuleNode
 
 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
 from rhodecode.lib.vcs.utils.lazy import LazyProperty
@@ -36,6 +37,10 @@
         return  safe_unicode(self._ctx.branch())
 
     @LazyProperty
+    def bookmarks(self):
+        return map(safe_unicode, self._ctx.bookmarks())
+
+    @LazyProperty
     def message(self):
         return safe_unicode(self._ctx.description())
 
@@ -159,6 +164,13 @@
                 " %r" % (self.revision, path))
         return self._ctx.filectx(path)
 
+    def _extract_submodules(self):
+        """
+        returns a dictionary with submodule information from substate file
+        of hg repository
+        """
+        return self._ctx.substate
+
     def get_file_mode(self, path):
         """
         Returns stat mode of the file at the given ``path``.
@@ -271,17 +283,27 @@
             raise ChangesetError("Directory does not exist for revision %r at "
                 " %r" % (self.revision, path))
         path = self._fix_path(path)
+
         filenodes = [FileNode(f, changeset=self) for f in self._file_paths
             if os.path.dirname(f) == path]
         dirs = path == '' and '' or [d for d in self._dir_paths
             if d and posixpath.dirname(d) == path]
         dirnodes = [DirNode(d, changeset=self) for d in dirs
             if os.path.dirname(d) == path]
+
+        als = self.repository.alias
+        for k, vals in self._extract_submodules().iteritems():
+            #vals = url,rev,type
+            loc = vals[0]
+            cs = vals[1]
+            dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
+                                          alias=als))
         nodes = dirnodes + filenodes
         # cache nodes
         for node in nodes:
             self.nodes[node.path] = node
         nodes.sort()
+
         return nodes
 
     def get_node(self, path):
--- a/rhodecode/lib/vcs/backends/hg/inmemory.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/backends/hg/inmemory.py	Thu May 10 20:27:45 2012 +0200
@@ -4,7 +4,7 @@
 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
 from rhodecode.lib.vcs.exceptions import RepositoryError
 
-from ...utils.hgcompat import memfilectx, memctx, hex
+from ...utils.hgcompat import memfilectx, memctx, hex, tolocal
 
 
 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
@@ -30,9 +30,9 @@
         self.check_integrity(parents)
 
         from .repository import MercurialRepository
-        if not isinstance(message, str) or not isinstance(author, str):
+        if not isinstance(message, unicode) or not isinstance(author, unicode):
             raise RepositoryError('Given message and author needs to be '
-                                  'an <str> instance')
+                                  'an <unicode> instance')
 
         if branch is None:
             branch = MercurialRepository.DEFAULT_BRANCH_NAME
@@ -70,7 +70,7 @@
                         copied=False)
 
             raise RepositoryError("Given path haven't been marked as added,"
-                "changed or removed (%s)" % path)
+                                  "changed or removed (%s)" % path)
 
         parents = [None, None]
         for i, parent in enumerate(self.parents):
@@ -89,9 +89,11 @@
             date=date,
             extra=kwargs)
 
+        loc = lambda u: tolocal(u.encode('utf-8'))
+
         # injecting given _repo params
-        commit_ctx._text = message
-        commit_ctx._user = author
+        commit_ctx._text = loc(message)
+        commit_ctx._user = loc(author)
         commit_ctx._date = date
 
         # TODO: Catch exceptions!
--- a/rhodecode/lib/vcs/nodes.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/nodes.py	Thu May 10 20:27:45 2012 +0200
@@ -8,19 +8,22 @@
     :created_on: Apr 8, 2010
     :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 """
+import os
 import stat
 import posixpath
 import mimetypes
 
+from pygments import lexers
+
 from rhodecode.lib.vcs.utils.lazy import LazyProperty
-from rhodecode.lib.vcs.utils import safe_unicode
+from rhodecode.lib.vcs.utils import safe_unicode, safe_str
 from rhodecode.lib.vcs.exceptions import NodeError
 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
-
-from pygments import lexers
+from rhodecode.lib.vcs.backends.base import EmptyChangeset
 
 
 class NodeKind:
+    SUBMODULE = -1
     DIR = 1
     FILE = 2
 
@@ -120,6 +123,10 @@
         return None
 
     @LazyProperty
+    def unicode_path(self):
+        return safe_unicode(self.path)
+
+    @LazyProperty
     def name(self):
         """
         Returns name of the node so if its path
@@ -205,6 +212,13 @@
         """
         return self.kind == NodeKind.DIR and self.path == ''
 
+    def is_submodule(self):
+        """
+        Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
+        otherwise.
+        """
+        return self.kind == NodeKind.SUBMODULE
+
     @LazyProperty
     def added(self):
         return self.state is NodeState.ADDED
@@ -557,3 +571,41 @@
 
     def __repr__(self):
         return '<%s>' % self.__class__.__name__
+
+
+class SubModuleNode(Node):
+    """
+    represents a SubModule of Git or SubRepo of Mercurial
+    """
+    is_binary = False
+    size = 0
+
+    def __init__(self, name, url=None, changeset=None, alias=None):
+        self.path = name
+        self.kind = NodeKind.SUBMODULE
+        self.alias = alias
+        # we have to use emptyChangeset here since this can point to svn/git/hg
+        # submodules we cannot get from repository
+        self.changeset = EmptyChangeset(str(changeset), alias=alias)
+        self.url = url or self._extract_submodule_url()
+
+    def __repr__(self):
+        return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
+                                 self.changeset.short_id)
+
+    def _extract_submodule_url(self):
+        if self.alias == 'git':
+            #TODO: find a way to parse gits submodule file and extract the
+            # linking URL
+            return self.path
+        if self.alias == 'hg':
+            return self.path
+
+    @LazyProperty
+    def name(self):
+        """
+        Returns name of the node so if its path
+        then only last part is returned.
+        """
+        org = safe_unicode(self.path.rstrip('/').split('/')[-1])
+        return u'%s @ %s' % (org, self.changeset.short_id)
--- a/rhodecode/lib/vcs/utils/hgcompat.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/lib/vcs/utils/hgcompat.py	Thu May 10 20:27:45 2012 +0200
@@ -1,6 +1,7 @@
-"""Mercurial libs compatibility
+"""
+Mercurial libs compatibility
+"""
 
-"""
 from mercurial import archival, merge as hg_merge, patch, ui
 from mercurial.commands import clone, nullid, pull
 from mercurial.context import memctx, memfilectx
@@ -10,3 +11,4 @@
 from mercurial.match import match
 from mercurial.mdiff import diffopts
 from mercurial.node import hex
+from mercurial.encoding import tolocal
--- a/rhodecode/model/comment.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/model/comment.py	Thu May 10 20:27:45 2012 +0200
@@ -29,7 +29,7 @@
 from pylons.i18n.translation import _
 from sqlalchemy.util.compat import defaultdict
 
-from rhodecode.lib.utils2 import extract_mentioned_users
+from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
 from rhodecode.lib import helpers as h
 from rhodecode.model import BaseModel
 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
@@ -67,7 +67,7 @@
         if text:
             repo = Repository.get(repo_id)
             cs = repo.scm_instance.get_changeset(revision)
-            desc = cs.message
+            desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
             author_email = cs.author_email
             comment = ChangesetComment()
             comment.repo = repo
@@ -83,14 +83,17 @@
             line = ''
             if line_no:
                 line = _('on line %s') % line_no
-            subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
-                                    {'commit_desc': desc, 'line': line},
-                             h.url('changeset_home', repo_name=repo.repo_name,
-                                   revision=revision,
-                                   anchor='comment-%s' % comment.comment_id,
-                                   qualified=True,
-                                   )
-                             )
+            subj = safe_unicode(
+                h.link_to('Re commit: %(commit_desc)s %(line)s' % \
+                          {'commit_desc': desc, 'line': line},
+                          h.url('changeset_home', repo_name=repo.repo_name,
+                                revision=revision,
+                                anchor='comment-%s' % comment.comment_id,
+                                qualified=True,
+                          )
+                )
+            )
+
             body = text
 
             # get the current participants of this changeset
@@ -139,7 +142,9 @@
             .filter(ChangesetComment.repo_id == repo_id)\
             .filter(ChangesetComment.revision == revision)\
             .filter(ChangesetComment.line_no != None)\
-            .filter(ChangesetComment.f_path != None).all()
+            .filter(ChangesetComment.f_path != None)\
+            .order_by(ChangesetComment.comment_id.asc())\
+            .all()
 
         paths = defaultdict(lambda: defaultdict(list))
 
--- a/rhodecode/model/db.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/model/db.py	Thu May 10 20:27:45 2012 +0200
@@ -645,7 +645,7 @@
     # SCM PROPERTIES
     #==========================================================================
 
-    def get_changeset(self, rev):
+    def get_changeset(self, rev=None):
         return get_changeset_safe(self.scm_instance, rev)
 
     @property
@@ -1233,7 +1233,8 @@
     @property
     def recipients(self):
         return [x.user for x in UserNotification.query()\
-                .filter(UserNotification.notification == self).all()]
+                .filter(UserNotification.notification == self)\
+                .order_by(UserNotification.user).all()]
 
     @classmethod
     def create(cls, created_by, subject, body, recipients, type_=None):
--- a/rhodecode/model/forms.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/model/forms.py	Thu May 10 20:27:45 2012 +0200
@@ -551,7 +551,7 @@
     )
 
     password = UnicodeString(
-        strip=True,
+        strip=False,
         min=3,
         not_empty=True,
         messages={
@@ -571,13 +571,13 @@
         username = All(UnicodeString(strip=True, min=1, not_empty=True),
                        ValidUsername(edit, old_data))
         if edit:
-            new_password = All(UnicodeString(strip=True, min=6, not_empty=False))
-            password_confirmation = All(UnicodeString(strip=True, min=6,
+            new_password = All(UnicodeString(strip=False, min=6, not_empty=False))
+            password_confirmation = All(UnicodeString(strip=False, min=6,
                                                       not_empty=False))
             admin = StringBoolean(if_missing=False)
         else:
-            password = All(UnicodeString(strip=True, min=6, not_empty=True))
-            password_confirmation = All(UnicodeString(strip=True, min=6,
+            password = All(UnicodeString(strip=False, min=6, not_empty=True))
+            password_confirmation = All(UnicodeString(strip=False, min=6,
                                                       not_empty=False))
 
         active = StringBoolean(if_missing=False)
@@ -632,8 +632,8 @@
         filter_extra_fields = True
         username = All(ValidUsername(edit, old_data),
                        UnicodeString(strip=True, min=1, not_empty=True))
-        password = All(UnicodeString(strip=True, min=6, not_empty=True))
-        password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
+        password = All(UnicodeString(strip=False, min=6, not_empty=True))
+        password_confirmation = All(UnicodeString(strip=False, min=6, not_empty=True))
         active = StringBoolean(if_missing=False)
         name = UnicodeString(strip=True, min=1, not_empty=False)
         lastname = UnicodeString(strip=True, min=1, not_empty=False)
@@ -754,7 +754,7 @@
     class _LdapSettingsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        pre_validators = [LdapLibValidator]
+        #pre_validators = [LdapLibValidator]
         ldap_active = StringBoolean(if_missing=False)
         ldap_host = UnicodeString(strip=True,)
         ldap_port = Number(strip=True,)
--- a/rhodecode/model/repo.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/model/repo.py	Thu May 10 20:27:45 2012 +0200
@@ -286,12 +286,12 @@
                 self.__create_repo(repo_name, form_data['repo_type'],
                                    form_data['repo_group'],
                                    form_data['clone_uri'])
+                log_create_repository(new_repo.get_dict(),
+                                      created_by=cur_user.username)
 
             # now automatically start following this repository as owner
             ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
                                                     cur_user.user_id)
-            log_create_repository(new_repo.get_dict(),
-                                  created_by=cur_user.username)
             return new_repo
         except:
             log.error(traceback.format_exc())
--- a/rhodecode/model/scm.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/model/scm.py	Thu May 10 20:27:45 2012 +0200
@@ -35,7 +35,7 @@
 
 from rhodecode import BACKENDS
 from rhodecode.lib import helpers as h
-from rhodecode.lib.utils2 import safe_str
+from rhodecode.lib.utils2 import safe_str, safe_unicode
 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
     action_logger, EmptyChangeset, REMOVED_REPO_PAT
@@ -343,12 +343,15 @@
 
         repo = dbrepo.scm_instance
         try:
-            extras = {'ip': '',
-                      'username': username,
-                      'action': 'push_remote',
-                      'repository': repo_name}
+            extras = {
+                'ip': '',
+                'username': username,
+                'action': 'push_remote',
+                'repository': repo_name,
+                'scm': repo.alias,
+            }
 
-            #inject ui extra param to log this action via push logger
+            # inject ui extra param to log this action via push logger
             for k, v in extras.items():
                 repo._repo.ui.setconfig('rhodecode_extras', k, v)
 
@@ -362,21 +365,25 @@
                       content, f_path):
 
         if repo.alias == 'hg':
-            from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
+            from rhodecode.lib.vcs.backends.hg import \
+                MercurialInMemoryChangeset as IMC
         elif repo.alias == 'git':
-            from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
+            from rhodecode.lib.vcs.backends.git import \
+                GitInMemoryChangeset as IMC
 
         # decoding here will force that we have proper encoded values
         # in any other case this will throw exceptions and deny commit
         content = safe_str(content)
-        message = safe_str(message)
         path = safe_str(f_path)
-        author = safe_str(author)
+        # message and author needs to be unicode
+        # proper backend should then translate that into required type
+        message = safe_unicode(message)
+        author = safe_unicode(author)
         m = IMC(repo)
         m.change(FileNode(path, content))
         tip = m.commit(message=message,
-                 author=author,
-                 parents=[cs], branch=cs.branch)
+                       author=author,
+                       parents=[cs], branch=cs.branch)
 
         new_cs = tip.short_id
         action = 'push_local:%s' % new_cs
@@ -403,21 +410,21 @@
                 type(content)
             ))
 
-        message = safe_str(message)
+        message = safe_unicode(message)
+        author = safe_unicode(author)
         path = safe_str(f_path)
-        author = safe_str(author)
         m = IMC(repo)
 
         if isinstance(cs, EmptyChangeset):
-            # Emptychangeset means we we're editing empty repository
+            # 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)
+                       author=author,
+                       parents=parents, branch=cs.branch)
         new_cs = tip.short_id
         action = 'push_local:%s' % new_cs
 
--- a/rhodecode/model/user.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/model/user.py	Thu May 10 20:27:45 2012 +0200
@@ -225,10 +225,8 @@
         from rhodecode.model.notification import NotificationModel
 
         try:
-            new_user = User()
-            for k, v in form_data.items():
-                if k != 'admin':
-                    setattr(new_user, k, v)
+            form_data['admin'] = False
+            new_user = self.create(form_data)
 
             self.sa.add(new_user)
             self.sa.flush()
@@ -398,145 +396,148 @@
                 rg_k = perm.UserRepoGroupToPerm.group.group_name
                 p = 'group.admin'
                 user.permissions[GK][rg_k] = p
+            return user
 
-        else:
-            #==================================================================
-            # set default permissions first for repositories and groups
-            #==================================================================
-            uid = user.user_id
+        #==================================================================
+        # set default permissions first for repositories and groups
+        #==================================================================
+        uid = user.user_id
 
-            # default global permissions
-            default_global_perms = self.sa.query(UserToPerm)\
-                .filter(UserToPerm.user_id == default_user_id)
+        # default global permissions
+        default_global_perms = self.sa.query(UserToPerm)\
+            .filter(UserToPerm.user_id == default_user_id)
 
-            for perm in default_global_perms:
-                user.permissions[GLOBAL].add(perm.permission.permission_name)
+        for perm in default_global_perms:
+            user.permissions[GLOBAL].add(perm.permission.permission_name)
 
-            # defaults for repositories, taken from default user
-            for perm in default_repo_perms:
-                r_k = perm.UserRepoToPerm.repository.repo_name
-                if perm.Repository.private and not (perm.Repository.user_id == uid):
-                    # disable defaults for private repos,
-                    p = 'repository.none'
-                elif perm.Repository.user_id == uid:
-                    # set admin if owner
-                    p = 'repository.admin'
-                else:
-                    p = perm.Permission.permission_name
+        # defaults for repositories, taken from default user
+        for perm in default_repo_perms:
+            r_k = perm.UserRepoToPerm.repository.repo_name
+            if perm.Repository.private and not (perm.Repository.user_id == uid):
+                # disable defaults for private repos,
+                p = 'repository.none'
+            elif perm.Repository.user_id == uid:
+                # set admin if owner
+                p = 'repository.admin'
+            else:
+                p = perm.Permission.permission_name
+
+            user.permissions[RK][r_k] = p
 
-                user.permissions[RK][r_k] = p
+        # defaults for repositories groups taken from default user permission
+        # on given group
+        for perm in default_repo_groups_perms:
+            rg_k = perm.UserRepoGroupToPerm.group.group_name
+            p = perm.Permission.permission_name
+            user.permissions[GK][rg_k] = p
+
+        #==================================================================
+        # overwrite defaults with user permissions if any found
+        #==================================================================
+
+        # user global permissions
+        user_perms = self.sa.query(UserToPerm)\
+                .options(joinedload(UserToPerm.permission))\
+                .filter(UserToPerm.user_id == uid).all()
+
+        for perm in user_perms:
+            user.permissions[GLOBAL].add(perm.permission.permission_name)
 
-            # defaults for repositories groups taken from default user permission
-            # on given group
-            for perm in default_repo_groups_perms:
-                rg_k = perm.UserRepoGroupToPerm.group.group_name
-                p = perm.Permission.permission_name
-                user.permissions[GK][rg_k] = p
+        # user explicit permissions for repositories
+        user_repo_perms = \
+         self.sa.query(UserRepoToPerm, Permission, Repository)\
+         .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
+         .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
+         .filter(UserRepoToPerm.user_id == uid)\
+         .all()
 
-            #==================================================================
-            # overwrite defaults with user permissions if any found
-            #==================================================================
+        for perm in user_repo_perms:
+            # set admin if owner
+            r_k = perm.UserRepoToPerm.repository.repo_name
+            if perm.Repository.user_id == uid:
+                p = 'repository.admin'
+            else:
+                p = perm.Permission.permission_name
+            user.permissions[RK][r_k] = p
 
-            # user global permissions
-            user_perms = self.sa.query(UserToPerm)\
-                    .options(joinedload(UserToPerm.permission))\
-                    .filter(UserToPerm.user_id == uid).all()
+        # USER GROUP
+        #==================================================================
+        # check if user is part of user groups for this repository and
+        # fill in (or replace with higher) permissions
+        #==================================================================
 
-            for perm in user_perms:
-                user.permissions[GLOBAL].add(perm.permission.permission_name)
+        # users group global
+        user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
+            .options(joinedload(UsersGroupToPerm.permission))\
+            .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
+                   UsersGroupMember.users_group_id))\
+            .filter(UsersGroupMember.user_id == uid).all()
+
+        for perm in user_perms_from_users_groups:
+            user.permissions[GLOBAL].add(perm.permission.permission_name)
 
-            # user explicit permissions for repositories
-            user_repo_perms = \
-             self.sa.query(UserRepoToPerm, Permission, Repository)\
-             .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
-             .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
-             .filter(UserRepoToPerm.user_id == uid)\
-             .all()
+        # users group for repositories permissions
+        user_repo_perms_from_users_groups = \
+         self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
+         .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
+         .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
+         .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
+         .filter(UsersGroupMember.user_id == uid)\
+         .all()
 
-            for perm in user_repo_perms:
-                # set admin if owner
-                r_k = perm.UserRepoToPerm.repository.repo_name
-                if perm.Repository.user_id == uid:
-                    p = 'repository.admin'
-                else:
-                    p = perm.Permission.permission_name
+        for perm in user_repo_perms_from_users_groups:
+            r_k = perm.UsersGroupRepoToPerm.repository.repo_name
+            p = perm.Permission.permission_name
+            cur_perm = user.permissions[RK][r_k]
+            # overwrite permission only if it's greater than permission
+            # given from other sources
+            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
                 user.permissions[RK][r_k] = p
 
-            #==================================================================
-            # check if user is part of user groups for this repository and
-            # fill in (or replace with higher) permissions
-            #==================================================================
-
-            # users group global
-            user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
-                .options(joinedload(UsersGroupToPerm.permission))\
-                .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
-                       UsersGroupMember.users_group_id))\
-                .filter(UsersGroupMember.user_id == uid).all()
-
-            for perm in user_perms_from_users_groups:
-                user.permissions[GLOBAL].add(perm.permission.permission_name)
+        # REPO GROUP
+        #==================================================================
+        # get access for this user for repos group and override defaults
+        #==================================================================
 
-            # users group for repositories permissions
-            user_repo_perms_from_users_groups = \
-             self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
-             .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
-             .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
-             .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
-             .filter(UsersGroupMember.user_id == uid)\
-             .all()
+        # user explicit permissions for repository
+        user_repo_groups_perms = \
+         self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
+         .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
+         .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
+         .filter(UserRepoGroupToPerm.user_id == uid)\
+         .all()
 
-            for perm in user_repo_perms_from_users_groups:
-                r_k = perm.UsersGroupRepoToPerm.repository.repo_name
-                p = perm.Permission.permission_name
-                cur_perm = user.permissions[RK][r_k]
-                # overwrite permission only if it's greater than permission
-                # given from other sources
-                if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
-                    user.permissions[RK][r_k] = p
-
-            #==================================================================
-            # get access for this user for repos group and override defaults
-            #==================================================================
+        for perm in user_repo_groups_perms:
+            rg_k = perm.UserRepoGroupToPerm.group.group_name
+            p = perm.Permission.permission_name
+            cur_perm = user.permissions[GK][rg_k]
+            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
+                user.permissions[GK][rg_k] = p
 
-            # user explicit permissions for repository
-            user_repo_groups_perms = \
-             self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
-             .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
-             .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
-             .filter(UserRepoGroupToPerm.user_id == uid)\
-             .all()
-
-            for perm in user_repo_groups_perms:
-                rg_k = perm.UserRepoGroupToPerm.group.group_name
-                p = perm.Permission.permission_name
-                cur_perm = user.permissions[GK][rg_k]
-                if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
-                    user.permissions[GK][rg_k] = p
+        # REPO GROUP + USER GROUP
+        #==================================================================
+        # check if user is part of user groups for this repo group and
+        # fill in (or replace with higher) permissions
+        #==================================================================
 
-            #==================================================================
-            # check if user is part of user groups for this repo group and
-            # fill in (or replace with higher) permissions
-            #==================================================================
+        # users group for repositories permissions
+        user_repo_group_perms_from_users_groups = \
+         self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
+         .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
+         .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
+         .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
+         .filter(UsersGroupMember.user_id == uid)\
+         .all()
 
-            # users group for repositories permissions
-            user_repo_group_perms_from_users_groups = \
-             self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
-             .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
-             .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
-             .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
-             .filter(UsersGroupMember.user_id == uid)\
-             .all()
-
-            for perm in user_repo_group_perms_from_users_groups:
-                g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
-                print perm, g_k
-                p = perm.Permission.permission_name
-                cur_perm = user.permissions[GK][g_k]
-                # overwrite permission only if it's greater than permission
-                # given from other sources
-                if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
-                    user.permissions[GK][g_k] = p
+        for perm in user_repo_group_perms_from_users_groups:
+            g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
+            print perm, g_k
+            p = perm.Permission.permission_name
+            cur_perm = user.permissions[GK][g_k]
+            # overwrite permission only if it's greater than permission
+            # given from other sources
+            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
+                user.permissions[GK][g_k] = p
 
         return user
 
--- a/rhodecode/public/css/style.css	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/public/css/style.css	Thu May 10 20:27:45 2012 +0200
@@ -2520,6 +2520,10 @@
 .right .logtags{
 	padding: 2px 2px 2px 2px;
 }
+.right .logtags .branchtag,.right .logtags .tagtag,.right .logtags .booktag{
+    margin: 0px 2px;
+}
+
 .right .logtags .branchtag,.logtags .branchtag {
   padding: 1px 3px 1px 3px;
   background-color: #bfbfbf;
@@ -2558,10 +2562,10 @@
     text-decoration: none;
     color: #ffffff;
 }
-.right .logbooks .bookbook,.logbooks .bookbook {
-  padding: 1px 3px 2px;
+.right .logbooks .bookbook,.logbooks .bookbook,.right .logtags .bookbook,.logtags .bookbook {
+  padding: 1px 3px 1px 3px;
   background-color: #46A546;
-  font-size: 9.75px;
+  font-size: 10px;
   font-weight: bold;
   color: #ffffff;
   text-transform: uppercase;
@@ -2570,10 +2574,10 @@
   -moz-border-radius: 3px;
   border-radius: 3px;
 }
-.right .logbooks .bookbook,.logbooks .bookbook a{
+.right .logbooks .bookbook,.logbooks .bookbook a,.right .logtags .bookbook,.logtags .bookbook a{
 	color: #ffffff;
 }
-.right .logbooks .bookbook,.logbooks .bookbook a:hover{
+.right .logbooks .bookbook,.logbooks .bookbook a:hover,.right .logtags .bookbook,.logtags .bookbook a:hover{
     text-decoration: none;
     color: #ffffff;
 }
@@ -2718,6 +2722,14 @@
 	text-align: left;
 }
 
+table.code-browser .submodule-dir {
+    background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
+    height: 16px;
+    padding-left: 20px;
+    text-align: left;
+}
+
+
 .box .search {
 	clear: both;
 	overflow: hidden;
@@ -3966,6 +3978,7 @@
 
 .comment .buttons {
 	float: right;
+	padding:2px 2px 0px 0px;
 }
 
 
@@ -3975,6 +3988,23 @@
 }
 
 /** comment inline form **/
+.comment-inline-form .overlay{
+	display: none;
+}
+.comment-inline-form .overlay.submitting{
+	display:block;
+    background: none repeat scroll 0 0 white;
+    font-size: 16px;
+    opacity: 0.5;
+    position: absolute;
+    text-align: center;
+    vertical-align: top;
+
+}
+.comment-inline-form .overlay.submitting .overlay-text{
+	width:100%;
+	margin-top:5%;
+}
 
 .comment-inline-form .clearfix{
     background: #EEE;
@@ -3987,6 +4017,7 @@
 div.comment-inline-form {
     margin-top: 5px;
     padding:2px 6px 8px 6px;
+
 }
 
 .comment-inline-form strong {
@@ -4047,6 +4078,10 @@
     margin: 3px 3px 5px 5px;
     background-color: #FAFAFA;
 }
+.inline-comments .add-comment {
+	padding: 2px 4px 8px 5px;
+}
+
 .inline-comments .comment-wrapp{
 	padding:1px;
 }
@@ -4078,8 +4113,15 @@
     font-size: 16px;
 }
 .inline-comments-button .add-comment{
-	margin:10px 5px !important;
-}
+	margin:2px 0px 8px 5px !important
+}
+
+
+.notification-paginator{
+    padding: 0px 0px 4px 16px;
+    float: left;    	
+}
+
 .notifications{
     border-radius: 4px 4px 4px 4px;
     -webkit-border-radius: 4px;
@@ -4113,16 +4155,24 @@
     float: left
 }
 .notification-list .container.unread{
-	
+	background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6);
 }
 .notification-header .gravatar{
-	
+    background: none repeat scroll 0 0 transparent;
+    padding: 0px 0px 0px 8px;	
 }
 .notification-header .desc.unread{
     font-weight: bold;
     font-size: 17px;
 }
-
+.notification-table{
+	border: 1px solid #ccc;
+    -webkit-border-radius: 6px 6px 6px 6px;
+    -moz-border-radius: 6px 6px 6px 6px;
+    border-radius: 6px 6px 6px 6px;
+    clear: both;
+    margin: 0px 20px 0px 20px;
+}
 .notification-header .delete-notifications{
     float: right;
     padding-top: 8px;
@@ -4134,6 +4184,11 @@
     padding:5px 0px 5px 38px;
 }
 
+.notification-body{
+	clear:both;
+	margin: 34px 2px 2px 8px
+}
+
 /****
   PERMS
 *****/
--- a/rhodecode/public/js/rhodecode.js	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/public/js/rhodecode.js	Thu May 10 20:27:45 2012 +0200
@@ -195,6 +195,34 @@
 	
 };
 
+var ajaxPOST = function(url,postData,success) {
+	// Set special header for ajax == HTTP_X_PARTIAL_XHR
+	YUC.initHeader('X-PARTIAL-XHR',true);
+	
+	var toQueryString = function(o) {
+	    if(typeof o !== 'object') {
+	        return false;
+	    }
+	    var _p, _qs = [];
+	    for(_p in o) {
+	        _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
+	    }
+	    return _qs.join('&');
+	};
+	
+    var sUrl = url;
+    var callback = {
+        success: success,
+        failure: function (o) {
+            alert("error");
+        },
+    };
+    var postData = toQueryString(postData);
+    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
+    return request;
+};
+
+
 /**
  * tooltip activate
  */
@@ -300,33 +328,25 @@
 	}	
 };
 
-var ajaxPOST = function(url,postData,success) {
-    var sUrl = url;
-    var callback = {
-        success: success,
-        failure: function (o) {
-            alert("error");
-        },
-    };
-    var postData = postData;
-    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
+var tableTr = function(cls,body){
+	var tr = document.createElement('tr');
+	YUD.addClass(tr, cls);
+	
+	
+	var cont = new YAHOO.util.Element(body);
+	var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
+	tr.id = 'comment-tr-{0}'.format(comment_id);
+	tr.innerHTML = '<td class="lineno-inline new-inline"></td>'+
+    				 '<td class="lineno-inline old-inline"></td>'+ 
+                     '<td>{0}</td>'.format(body);
+	return tr;
 };
 
-
 /** comments **/
 var removeInlineForm = function(form) {
 	form.parentNode.removeChild(form);
 };
 
-var tableTr = function(cls,body){
-	var form = document.createElement('tr');
-	YUD.addClass(form, cls);
-	form.innerHTML = '<td class="lineno-inline new-inline"></td>'+
-    				 '<td class="lineno-inline old-inline"></td>'+ 
-                     '<td>{0}</td>'.format(body);
-	return form;
-};
-
 var createInlineForm = function(parent_tr, f_path, line) {
 	var tmpl = YUD.get('comment-inline-form-template').innerHTML;
 	tmpl = tmpl.format(f_path, line);
@@ -337,12 +357,27 @@
 	var form_hide_button = new YAHOO.util.Element(form.getElementsByClassName('hide-inline-form')[0]);
 	form_hide_button.on('click', function(e) {
 		var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
+		if(YUD.hasClass(newtr.nextElementSibling,'inline-comments-button')){
+			YUD.setStyle(newtr.nextElementSibling,'display','');
+		}
 		removeInlineForm(newtr);
 		YUD.removeClass(parent_tr, 'form-open');
+		
 	});
+	
 	return form
 };
+
+/**
+ * Inject inline comment for on given TR this tr should be always an .line
+ * tr containing the line. Code will detect comment, and always put the comment
+ * block at the very bottom
+ */
 var injectInlineForm = function(tr){
+	  if(!YUD.hasClass(tr, 'line')){
+		  return
+	  }
+	  var submit_url = AJAX_COMMENT_URL;
 	  if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(tr,'no-comment')){
 		  return
 	  }	
@@ -350,20 +385,96 @@
 	  var node = tr.parentNode.parentNode.parentNode.getElementsByClassName('full_f_path')[0];
 	  var f_path = YUD.getAttribute(node,'path');
 	  var lineno = getLineNo(tr);
-	  var form = createInlineForm(tr, f_path, lineno);
-	  var target_tr = tr;
-	  if(YUD.hasClass(YUD.getNextSibling(tr),'inline-comments')){
-		  target_tr = YUD.getNextSibling(tr);
-	  }
-	  YUD.insertAfter(form,target_tr);
+	  var form = createInlineForm(tr, f_path, lineno, submit_url);
+	  
+	  var parent = tr;
+	  while (1){
+		  var n = parent.nextElementSibling;
+		  // next element are comments !
+		  if(YUD.hasClass(n,'inline-comments')){
+			  parent = n;
+		  }
+		  else{
+			  break;
+		  }
+	  }	  
+	  YUD.insertAfter(form,parent);
+	  
 	  YUD.get('text_'+lineno).focus();
+	  var f = YUD.get(form);
+	  
+	  var overlay = f.getElementsByClassName('overlay')[0];
+	  var _form = f.getElementsByClassName('inline-form')[0];
+	  
+	  form.on('submit',function(e){
+		  YUE.preventDefault(e);
+		  
+		  //ajax submit
+		  var text = YUD.get('text_'+lineno).value;
+		  var postData = {
+	            'text':text,
+	            'f_path':f_path,
+	            'line':lineno
+		  };
+		  
+		  if(lineno === undefined){
+			  alert('missing line !');
+			  return
+		  }
+		  if(f_path === undefined){
+			  alert('missing file path !');
+			  return
+		  }
+		  
+		  if(text == ""){
+			  return
+		  }
+		  
+		  var success = function(o){
+			  YUD.removeClass(tr, 'form-open');
+			  removeInlineForm(f);			  
+			  var json_data = JSON.parse(o.responseText);
+	          renderInlineComment(json_data);
+		  };
+
+		  if (YUD.hasClass(overlay,'overlay')){
+			  var w = _form.offsetWidth;
+			  var h = _form.offsetHeight;
+			  YUD.setStyle(overlay,'width',w+'px');
+			  YUD.setStyle(overlay,'height',h+'px');
+		  }		  
+		  YUD.addClass(overlay, 'submitting');		  
+		  
+		  ajaxPOST(submit_url, postData, success);
+	  });
+	  
 	  tooltip_activate();
 };
 
-var createInlineAddButton = function(tr,label){
-	var html = '<div class="add-comment"><span class="ui-btn">{0}</span></div>'.format(label);
-        
-	var add = new YAHOO.util.Element(tableTr('inline-comments-button',html));
+var deleteComment = function(comment_id){
+	var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',comment_id);
+    var postData = {'_method':'delete'};
+    var success = function(o){
+        var n = YUD.get('comment-tr-'+comment_id);
+        var root = n.previousElementSibling.previousElementSibling;
+        n.parentNode.removeChild(n);
+
+        // scann nodes, and attach add button to last one
+        placeAddButton(root);
+    }
+    ajaxPOST(url,postData,success);
+}
+
+
+var createInlineAddButton = function(tr){
+
+	var label = TRANSLATION_MAP['add another comment'];
+	
+	var html_el = document.createElement('div');
+	YUD.addClass(html_el, 'add-comment');
+	html_el.innerHTML = '<span class="ui-btn">{0}</span>'.format(label);
+	
+	var add = new YAHOO.util.Element(html_el);
 	add.on('click', function(e) {
 		injectInlineForm(tr);
 	});
@@ -384,6 +495,103 @@
 	return line
 };
 
+var placeAddButton = function(target_tr){
+	if(!target_tr){
+		return
+	}
+	var last_node = target_tr;
+    //scann	
+	  while (1){
+		  var n = last_node.nextElementSibling;
+		  // next element are comments !
+		  if(YUD.hasClass(n,'inline-comments')){
+			  last_node = n;
+			  //also remove the comment button from previos
+			  var comment_add_buttons = last_node.getElementsByClassName('add-comment');
+			  for(var i=0;i<comment_add_buttons.length;i++){
+				  var b = comment_add_buttons[i];
+				  b.parentNode.removeChild(b);
+			  }
+		  }
+		  else{
+			  break;
+		  }
+	  }
+	  
+    var add = createInlineAddButton(target_tr);
+    // get the comment div
+    var comment_block = last_node.getElementsByClassName('comment')[0];
+    // attach add button
+    YUD.insertAfter(add,comment_block);	
+}
+
+/**
+ * Places the inline comment into the changeset block in proper line position
+ */
+var placeInline = function(target_container,lineno,html){
+	  var lineid = "{0}_{1}".format(target_container,lineno);
+	  var target_line = YUD.get(lineid);
+	  var comment = new YAHOO.util.Element(tableTr('inline-comments',html))
+	  
+	  // check if there are comments already !
+	  var parent = target_line.parentNode;
+	  var root_parent = parent;
+	  while (1){
+		  var n = parent.nextElementSibling;
+		  // next element are comments !
+		  if(YUD.hasClass(n,'inline-comments')){
+			  parent = n;
+		  }
+		  else{
+			  break;
+		  }
+	  }
+	  // put in the comment at the bottom
+	  YUD.insertAfter(comment,parent);
+	  
+	  // scann nodes, and attach add button to last one
+      placeAddButton(root_parent);
+
+	  return target_line;
+}
+
+/**
+ * make a single inline comment and place it inside
+ */
+var renderInlineComment = function(json_data){
+    try{
+	  var html =  json_data['rendered_text'];
+	  var lineno = json_data['line_no'];
+	  var target_id = json_data['target_id'];
+	  placeInline(target_id, lineno, html);
+
+    }catch(e){
+  	  console.log(e);
+    }
+}
+
+/**
+ * Iterates over all the inlines, and places them inside proper blocks of data
+ */
+var renderInlineComments = function(file_comments){
+	for (f in file_comments){
+        // holding all comments for a FILE
+		var box = file_comments[f];
+
+		var target_id = YUD.getAttribute(box,'target_id');
+		// actually comments with line numbers
+        var comments = box.children;
+        for(var i=0; i<comments.length; i++){
+        	var data = {
+        		'rendered_text': comments[i].outerHTML,
+        		'line_no': YUD.getAttribute(comments[i],'line'),
+        		'target_id': target_id
+        	}
+        	renderInlineComment(data);
+        }
+    }	
+}
+
 
 var fileBrowserListeners = function(current_url, node_list_url, url_base,
 									truncated_lbl, nomatch_lbl){
--- a/rhodecode/templates/admin/notifications/notifications.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/admin/notifications/notifications.html	Thu May 10 20:27:45 2012 +0200
@@ -25,7 +25,7 @@
         ##</ul>
     </div>
     %if c.notifications:
-      <div style="padding:10px 15px;text-align: right">
+      <div style="padding:14px 18px;text-align: right;float:right">
       <span id='mark_all_read' class="ui-btn">${_('Mark all read')}</span>
       </div>
     %endif
@@ -39,15 +39,18 @@
  var notification_id = e.currentTarget.id;
  deleteNotification(url_del,notification_id)
 })
- YUE.on('mark_all_read','click',function(e){
-	    var url = "${h.url('notifications_mark_all_read')}";
-	    ypjax(url,'notification_data',function(){
-	    	YUD.get('notification_counter').innerHTML=0;
-	    	YUE.on(YUQ('.delete-notification'),'click',function(e){
-	    		 var notification_id = e.currentTarget.id;
-	    		 deleteNotification(url_del,notification_id)
-	    	})
-	    });
- })
+YUE.on('mark_all_read','click',function(e){
+    var url = "${h.url('notifications_mark_all_read')}";
+    ypjax(url,'notification_data',function(){
+    	var notification_counter = YUD.get('notification_counter');
+    	if(notification_counter){
+    		notification_counter.innerHTML=0;
+    	}
+    	YUE.on(YUQ('.delete-notification'),'click',function(e){
+    		 var notification_id = e.currentTarget.id;
+    		 deleteNotification(url_del,notification_id)
+    	})
+    });
+})
 </script>
 </%def>
--- a/rhodecode/templates/admin/notifications/notifications_data.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/admin/notifications/notifications_data.html	Thu May 10 20:27:45 2012 +0200
@@ -3,26 +3,37 @@
 <%
 unread = lambda n:{False:'unread'}.get(n)
 %>
-<div class="table">
-  <div class="notification-list">
-  %for notification in c.notifications:
-    <div id="notification_${notification.notification.notification_id}" class="container ${unread(notification.read)}">
-      <div class="notification-header">
-        <div class="gravatar">
-            <img alt="gravatar" src="${h.gravatar_url(h.email(notification.notification.created_by_user.email),24)}"/>
-        </div>
-        <div class="desc ${unread(notification.read)}">
-        <a href="${url('notification', notification_id=notification.notification.notification_id)}">${notification.notification.description}</a>
-        </div>
-        <div class="delete-notifications">
-          <span id="${notification.notification.notification_id}" class="delete-notification delete_icon action"></span>
-        </div>
-      </div>
-      <div class="notification-subject">${h.literal(notification.notification.subject)}</div>
-    </div>
-  %endfor
+<div class="notification-paginator">
+  <div class="pagination-wh pagination-left">
+  ${c.notifications.pager('$link_previous ~2~ $link_next')}
   </div>
 </div>
+
+<div class="notification-list  notification-table">
+%for notification in c.notifications:
+  <div id="notification_${notification.notification.notification_id}" class="container ${unread(notification.read)}">
+    <div class="notification-header">
+      <div class="gravatar">
+          <img alt="gravatar" src="${h.gravatar_url(h.email(notification.notification.created_by_user.email),24)}"/>
+      </div>
+      <div class="desc ${unread(notification.read)}">
+      <a href="${url('notification', notification_id=notification.notification.notification_id)}">${notification.notification.description}</a>
+      </div>
+      <div class="delete-notifications">
+        <span id="${notification.notification.notification_id}" class="delete-notification delete_icon action"></span>
+      </div>
+    </div>
+    <div class="notification-subject">${h.literal(notification.notification.subject)}</div>
+  </div>
+%endfor
+</div>
+
+<div class="notification-paginator">
+  <div class="pagination-wh pagination-left">
+  ${c.notifications.pager('$link_previous ~2~ $link_next')}
+  </div>
+</div>
+
 %else:
     <div class="table">${_('No notifications here yet')}</div>
 %endif
--- a/rhodecode/templates/admin/notifications/show_notification.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/admin/notifications/show_notification.html	Thu May 10 20:27:45 2012 +0200
@@ -39,7 +39,7 @@
             <span id="${c.notification.notification_id}" class="delete-notification delete_icon action"></span>
           </div>
         </div>
-        <div>${h.rst_w_mentions(c.notification.body)}</div>
+        <div class="notification-body">${h.rst_w_mentions(c.notification.body)}</div>
       </div>
     </div>
 </div>
--- a/rhodecode/templates/admin/settings/settings.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/admin/settings/settings.html	Thu May 10 20:27:45 2012 +0200
@@ -210,5 +210,37 @@
     </div>
     ${h.end_form()}
 
+    <h3>${_('System Info and Packages')}</h3>
+    <div class="form">
+    <div>
+        <h5 id="expand_modules" style="cursor: pointer">&darr; ${_('show')} &darr;</h5>
+    </div>
+      <div id="expand_modules_table"  style="display:none">
+      <h5>Python - ${c.py_version}</h5>
+      <h5>System - ${c.platform}</h5>
+
+      <table class="table" style="margin:0px 0px 0px 20px">
+          <colgroup>
+              <col style="width:220px">
+          </colgroup>
+          <tbody>
+              %for key, value in c.modules:
+                  <tr>
+                      <th style="text-align: right;padding-right:5px;">${key}</th>
+                      <td>${value}</td>
+                  </tr>
+              %endfor
+          </tbody>
+      </table>
+      </div>
+    </div>
+
+    <script type="text/javascript">
+    YUE.on('expand_modules','click',function(e){
+    	YUD.setStyle('expand_modules_table','display','');
+    	YUD.setStyle('expand_modules','display','none');
+    })
+    </script>
+
 </div>
 </%def>
--- a/rhodecode/templates/base/root.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/base/root.html	Thu May 10 20:27:45 2012 +0200
@@ -47,9 +47,13 @@
 
             <script type="text/javascript">
             var follow_base_url  = "${h.url('toggle_following')}";
-            var stop_follow_text = "${_('Stop following this repository')}";
-            var start_follow_text = "${_('Start following this repository')}";
 
+            //JS translations map
+            var TRANSLATION_MAP = {
+            	'add another comment':'${_("add another comment")}',
+                'Stop following this repository':"${_('Stop following this repository')}",
+                'Start following this repository':"${_('Start following this repository')}",
+            };
 
             var onSuccessFollow = function(target){
                 var f = YUD.get(target.id);
@@ -57,7 +61,7 @@
 
                 if(f.getAttribute('class')=='follow'){
                     f.setAttribute('class','following');
-                    f.setAttribute('title',stop_follow_text);
+                    f.setAttribute('title',TRANSLATION_MAP['Stop following this repository']);
 
                     if(f_cnt){
                         var cnt = Number(f_cnt.innerHTML)+1;
@@ -66,7 +70,7 @@
                 }
                 else{
                     f.setAttribute('class','follow');
-                    f.setAttribute('title',start_follow_text);
+                    f.setAttribute('title',TRANSLATION_MAP['Start following this repository']);
                     if(f_cnt){
                         var cnt = Number(f_cnt.innerHTML)+1;
                         f_cnt.innerHTML = cnt;
@@ -133,13 +137,13 @@
      ## IE hacks
       <!--[if IE 7]>
       <script>YUD.addClass(document.body,'ie7')</script>
-      <![endif]-->            
+      <![endif]-->
       <!--[if IE 8]>
       <script>YUD.addClass(document.body,'ie8')</script>
       <![endif]-->
       <!--[if IE 9]>
       <script>YUD.addClass(document.body,'ie9')</script>
-      <![endif]-->    
+      <![endif]-->
 
       ${next.body()}
     </body>
--- a/rhodecode/templates/changelog/changelog.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/changelog/changelog.html	Thu May 10 20:27:45 2012 +0200
@@ -63,7 +63,7 @@
                             <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
 						</div>
 						<div class="right">
-									<div id="${cs.raw_id}_changes_info" class="changes">
+									<div class="changes">
                                         <div id="${cs.raw_id}"  style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
                                         <div class="comments-container">
                                         %if len(c.comments.get(cs.raw_id,[])) > 0:
@@ -91,10 +91,18 @@
 									%if len(cs.parents)>1:
 									<span class="merge">${_('merge')}</span>
 									%endif
-									%if h.is_hg(c.rhodecode_repo) and cs.branch:
+									%if cs.branch:
 									<span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
-									   ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
+									   ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
+                                    </span>
 									%endif
+                                    %if h.is_hg(c.rhodecode_repo):
+                                      %for book in cs.bookmarks:
+                                      <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
+                                         ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
+                                      </span>                   
+                                      %endfor                
+                                    %endif
 									%for tag in cs.tags:
 										<span class="tagtag"  title="${'%s %s' % (_('tag'),tag)}">
 										${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
@@ -177,7 +185,7 @@
                     	var id = e.currentTarget.id
                     	var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
                     	var url = url.replace('__CS__',id);
-                    	ypjax(url,id+'_changes_info',function(){tooltip_activate()});
+                    	ypjax(url,id,function(){tooltip_activate()});
                     });
 
                     // change branch filter
--- a/rhodecode/templates/changeset/changeset.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/changeset/changeset.html	Thu May 10 20:27:45 2012 +0200
@@ -81,8 +81,11 @@
                  %if len(c.changeset.parents)>1:
                  <span class="merge">${_('merge')}</span>
                  %endif
-		             <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
-		             ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
+		             %if c.changeset.branch:
+                     <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
+		             ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
+                     </span>
+                     %endif
 		             %for tag in c.changeset.tags:
 		                 <span class="tagtag"  title="${'%s %s' % (_('tag'),tag)}">
 		                 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
@@ -122,22 +125,12 @@
     <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
     ${comment.comment_inline_form(c.changeset)}
 
+    ## render comments
     ${comment.comments(c.changeset)}
-
     <script type="text/javascript">
-      var deleteComment = function(comment_id){
-
-          var url = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}".replace('__COMMENT_ID__',comment_id);
-          var postData = '_method=delete';
-          var success = function(o){
-              var n = YUD.get('comment-'+comment_id);
-              n.parentNode.removeChild(n);
-          }
-          ajaxPOST(url,postData,success);
-      }
-
       YUE.onDOMReady(function(){
-
+    	  AJAX_COMMENT_URL = "${url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id)}";
+    	  AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}"
           YUE.on(YUQ('.show-inline-comments'),'change',function(e){
               var show = 'none';
               var target = e.currentTarget;
@@ -162,28 +155,7 @@
 
           // inject comments into they proper positions
           var file_comments = YUQ('.inline-comment-placeholder');
-
-          for (f in file_comments){
-              var box = file_comments[f];
-              var inlines = box.children;
-              for(var i=0; i<inlines.length; i++){
-                  try{
-
-                    var inline = inlines[i];
-                    var lineno = YUD.getAttribute(inlines[i],'line');
-                    var lineid = "{0}_{1}".format(YUD.getAttribute(inline,'target_id'),lineno);
-                    var target_line = YUD.get(lineid);
-
-                    var add = createInlineAddButton(target_line.parentNode,'${_("add another comment")}');
-                    YUD.insertAfter(add,target_line.parentNode);
-
-                    var comment = new YAHOO.util.Element(tableTr('inline-comments',inline.innerHTML))
-                    YUD.insertAfter(comment,target_line.parentNode);
-                  }catch(e){
-                	  console.log(e);
-                  }
-              }
-          }
+          renderInlineComments(file_comments);
       })
 
     </script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/changeset/changeset_comment_block.html	Thu May 10 20:27:45 2012 +0200
@@ -0,0 +1,2 @@
+<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
+${comment.comment_block(c.co)}
--- a/rhodecode/templates/changeset/changeset_file_comment.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/changeset/changeset_file_comment.html	Thu May 10 20:27:45 2012 +0200
@@ -4,7 +4,7 @@
 ## ${comment.comment_block(co)}
 ##
 <%def name="comment_block(co)">
-  <div class="comment" id="comment-${co.comment_id}">
+  <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
     <div class="comment-wrapp">
   	<div class="meta">
   		<span class="user">
@@ -32,7 +32,8 @@
 <div id='comment-inline-form-template' style="display:none">
   <div class="comment-inline-form">
   %if c.rhodecode_user.username != 'default':
-      ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id))}
+    <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
+      ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id),class_='inline-form')}
       <div class="clearfix">
           <div class="comment-help">${_('Commenting on line')} {1}. ${_('Comments parsed using')}
           <a href="${h.url('rst_help')}">RST</a> ${_('syntax')} ${_('with')}
@@ -43,7 +44,7 @@
       <div class="comment-button">
       <input type="hidden" name="f_path" value="{0}">
       <input type="hidden" name="line" value="{1}">
-      ${h.submit('save', _('Comment'), class_='ui-btn')}
+      ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
       ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
       </div>
       ${h.end_form()}
@@ -64,25 +65,31 @@
 </%def>
 
 
-<%def name="comments(changeset)">
-
-<div class="comments">
+<%def name="inlines(changeset)">
     <div class="comments-number">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
-
     %for path, lines in c.inline_comments:
-        <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.FID(changeset.raw_id,path)}">
         % for line,comments in lines.iteritems():
-            <div class="inline-comment-placeholder-line" line="${line}" target_id="${h.safeid(h.safe_unicode(path))}">
+            <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
             %for co in comments:
                 ${comment_block(co)}
             %endfor
             </div>
         %endfor
-        </div>
     %endfor
 
+</%def>
+
+<%def name="comments(changeset)">
+
+<div class="comments">
+    <div id="inline-comments-container">
+     ${inlines(changeset)}
+    </div>
+
     %for co in c.comments:
-        ${comment_block(co)}
+        <div id="comment-tr-${co.comment_id}">
+          ${comment_block(co)}
+        </div>
     %endfor
     %if c.rhodecode_user.username != 'default':
     <div class="comment-form">
--- a/rhodecode/templates/files/files_annotate.html	Mon Apr 23 18:31:51 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-<%inherit file="/base/base.html"/>
-
-<%def name="title()">
-    ${c.repo_name} ${_('File annotate')} - ${c.rhodecode_name}
-</%def>
-
-<%def name="breadcrumbs_links()">
-    ${h.link_to(u'Home',h.url('/'))}
-    &raquo;
-    ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
-    &raquo;
-    ${_('annotate')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)}
-</%def>
-
-<%def name="page_nav()">
-		${self.menu('files')}
-</%def>
-<%def name="main()">
-<div class="box">
-    <!-- box / title -->
-    <div class="title">
-        ${self.breadcrumbs()}
-        <ul class="links">
-            <li>
-              <span style="text-transform: uppercase;"><a href="#">${_('branch')}: ${c.cs.branch}</a></span>
-            </li>
-        </ul>
-    </div>
-    <div class="table">
-		<div id="files_data">
-			<h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}</h3>
-			<dl>
-			    <dt style="padding-top:10px;font-size:16px">${_('History')}</dt>
-			    <dd>
-			        <div>
-			        ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
-			        ${h.hidden('diff2',c.file.changeset.raw_id)}
-			        ${h.select('diff1',c.file.changeset.raw_id,c.file_history)}
-			        ${h.submit('diff','diff to revision',class_="ui-btn")}
-			        ${h.submit('show_rev','show at revision',class_="ui-btn")}
-			        ${h.end_form()}
-			        </div>
-			    </dd>
-			</dl>
-			<div id="body" class="codeblock">
-                <div class="code-header">
-                    <div class="stats">
-                        <div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
-                        <div class="left item">${h.link_to("r%s:%s" % (c.file.changeset.revision,h.short_id(c.file.changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id))}</div>
-                        <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
-                        <div class="left item last">${c.file.mimetype}</div>
-                        <div class="buttons">
-                          ${h.link_to(_('show source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
-                          ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
-                          ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
-                          % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
-                           % if not c.file.is_binary:
-                            ${h.link_to(_('edit'),h.url('files_edit_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
-                           % endif
-                          % endif
-                        </div>
-                    </div>
-                    <div class="author">
-                        <div class="gravatar">
-                            <img alt="gravatar" src="${h.gravatar_url(h.email(c.cs.author),16)}"/>
-                        </div>
-                        <div title="${c.cs.author}" class="user">${h.person(c.cs.author)}</div>
-                    </div>
-                    <div class="commit">${c.file.last_changeset.message}</div>
-                </div>
-				<div class="code-body">
-			       %if c.file.is_binary:
-			           ${_('Binary file (%s)') % c.file.mimetype}
-			       %else:
-					% if c.file.size < c.cut_off_limit:
-						${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
-					%else:
-						${_('File is too big to display')} ${h.link_to(_('show as raw'),
-						h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.revision,f_path=c.f_path))}
-					%endif
-			       <script type="text/javascript">
-			           function highlight_lines(lines){
-			               for(pos in lines){
-			                 YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
-			               }
-			           }
-			           page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
-			           if (page_highlights.length == 2){
-			              highlight_ranges  = page_highlights[1].split(",");
-
-			              var h_lines = [];
-			              for (pos in highlight_ranges){
-			                   var _range = highlight_ranges[pos].split('-');
-			                   if(_range.length == 2){
-			                       var start = parseInt(_range[0]);
-			                       var end = parseInt(_range[1]);
-			                       if (start < end){
-			                           for(var i=start;i<=end;i++){
-			                               h_lines.push(i);
-			                           }
-			                       }
-			                   }
-			                   else{
-			                       h_lines.push(parseInt(highlight_ranges[pos]));
-			                   }
-			             }
-			           highlight_lines(h_lines);
-
-			           //remember original location
-			           var old_hash  = location.href.substring(location.href.indexOf('#'));
-
-			           // this makes a jump to anchor moved by 3 posstions for padding
-			           window.location.hash = '#L'+Math.max(parseInt(h_lines[0])-3,1);
-
-			           //sets old anchor
-			           window.location.hash = old_hash;
-
-			           }
-			       </script>
-				   %endif
-				</div>
-			</div>
-            <script type="text/javascript">
-            YAHOO.util.Event.onDOMReady(function(){
-                YUE.on('show_rev','click',function(e){
-                    YAHOO.util.Event.preventDefault(e);
-                    var cs = YAHOO.util.Dom.get('diff1').value;
-                    var url = "${h.url('files_annotate_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
-                    window.location = url;
-                    });
-               });
-            </script>
-		</div>
-    </div>
-</div>
-</%def>
--- a/rhodecode/templates/files/files_browser.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/files/files_browser.html	Thu May 10 20:27:45 2012 +0200
@@ -70,7 +70,11 @@
 		    %for cnt,node in enumerate(c.file):
 				<tr class="parity${cnt%2}">
 		             <td>
-                        ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
+                        %if node.is_submodule():
+                           ${h.link_to(node.name,node.url or '#',class_="submodule-dir ypjax-link")}
+                        %else:
+                          ${h.link_to(node.name, h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
+                        %endif:
 		             </td>
 		             <td>
 		             %if node.is_file():
--- a/rhodecode/templates/files/files_edit.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/files/files_edit.html	Thu May 10 20:27:45 2012 +0200
@@ -56,7 +56,7 @@
                       % endif
                     </div>
                 </div>
-                <div class="commit">${_('Editing file')}: ${c.file.path}</div>
+                <div class="commit">${_('Editing file')}: ${c.file.unicode_path}</div>
             </div>
 			    <pre id="editor_pre"></pre>
 				<textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
--- a/rhodecode/templates/files/files_source.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/files/files_source.html	Thu May 10 20:27:45 2012 +0200
@@ -16,11 +16,15 @@
 	<div class="code-header">
         <div class="stats">
             <div class="left img"><img src="${h.url('/images/icons/file.png')}"/></div>
-            <div class="left item"><pre>${h.link_to("r%s:%s" % (c.file.changeset.revision,h.short_id(c.file.changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id))}</pre></div>
+            <div class="left item"><pre class="tooltip" title="${c.file.changeset.date}">${h.link_to("r%s:%s" % (c.file.changeset.revision,h.short_id(c.file.changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id))}</pre></div>
             <div class="left item"><pre>${h.format_byte_size(c.file.size,binary=True)}</pre></div>
             <div class="left item last"><pre>${c.file.mimetype}</pre></div>
             <div class="buttons">
-              ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
+              %if c.annotate:
+                ${h.link_to(_('show source'),    h.url('files_home',         repo_name=c.repo_name,revision=c.file.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
+              %else:
+                ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
+              %endif
               ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
               ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id,f_path=c.f_path),class_="ui-btn")}
               % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
@@ -43,60 +47,66 @@
 	       ${_('Binary file (%s)') % c.file.mimetype}
 	   %else:
 		% if c.file.size < c.cut_off_limit:
-			${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
+            %if c.annotate:
+              ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
+            %else:
+			  ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
+            %endif
 		%else:
 			${_('File is too big to display')} ${h.link_to(_('show as raw'),
 			h.url('files_raw_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id,f_path=c.f_path))}
 		%endif
-       <script type="text/javascript">
-           function highlight_lines(lines){
-               for(pos in lines){
-                 YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
-               }
-           }
-           page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
-           if (page_highlights.length == 2){
-              highlight_ranges  = page_highlights[1].split(",");
-
-              var h_lines = [];
-              for (pos in highlight_ranges){
-                   var _range = highlight_ranges[pos].split('-');
-                   if(_range.length == 2){
-                       var start = parseInt(_range[0]);
-                       var end = parseInt(_range[1]);
-                       if (start < end){
-                           for(var i=start;i<=end;i++){
-                               h_lines.push(i);
-                           }
-                       }
-                   }
-                   else{
-                       h_lines.push(parseInt(highlight_ranges[pos]));
-                   }
-             }
-           highlight_lines(h_lines);
-
-           //remember original location
-           var old_hash  = location.href.substring(location.href.indexOf('#'));
-
-           // this makes a jump to anchor moved by 3 posstions for padding
-           window.location.hash = '#L'+Math.max(parseInt(h_lines[0])-3,1);
-
-           //sets old anchor
-           window.location.hash = old_hash;
-
-           }
-       </script>
      %endif
 	</div>
 </div>
 
 <script type="text/javascript">
 YUE.onDOMReady(function(){
+    function highlight_lines(lines){
+        for(pos in lines){
+          YUD.setStyle('L'+lines[pos],'background-color','#FFFFBE');
+        }
+    }
+    page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
+    if (page_highlights.length == 2){
+       highlight_ranges  = page_highlights[1].split(",");
+
+       var h_lines = [];
+       for (pos in highlight_ranges){
+            var _range = highlight_ranges[pos].split('-');
+            if(_range.length == 2){
+                var start = parseInt(_range[0]);
+                var end = parseInt(_range[1]);
+                if (start < end){
+                    for(var i=start;i<=end;i++){
+                        h_lines.push(i);
+                    }
+                }
+            }
+            else{
+                h_lines.push(parseInt(highlight_ranges[pos]));
+            }
+      }
+    highlight_lines(h_lines);
+
+    //remember original location
+    var old_hash  = location.href.substring(location.href.indexOf('#'));
+
+    // this makes a jump to anchor moved by 3 posstions for padding
+    window.location.hash = '#L'+Math.max(parseInt(h_lines[0])-3,1);
+
+    //sets old anchor
+    window.location.hash = old_hash;
+
+    }
     YUE.on('show_rev','click',function(e){
     	YUE.preventDefault(e);
         var cs = YUD.get('diff1').value;
-        var url = "${h.url('files_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
+        %if c.annotate:
+          var url = "${h.url('files_annotate_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
+        %else:
+          var url = "${h.url('files_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs);
+        %endif
         window.location = url;
     });
     YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}"))
--- a/rhodecode/templates/files/files_ypjax.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/files/files_ypjax.html	Thu May 10 20:27:45 2012 +0200
@@ -1,6 +1,9 @@
 %if c.file:
     <h3 class="files_location">
         ${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}
+        %if c.annotate:
+        - ${_('annotation')}
+        %endif
     </h3>
         %if c.file.is_dir():
             <%include file='files_browser.html'/>
--- a/rhodecode/templates/forks/fork.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/forks/fork.html	Thu May 10 20:27:45 2012 +0200
@@ -65,7 +65,7 @@
                     <label for="private">${_('Copy permissions')}:</label>
                 </div>
                 <div class="checkboxes">
-                    ${h.checkbox('copy_permissions',value="True")}
+                    ${h.checkbox('copy_permissions',value="True", checked="checked")}
                 </div>
              </div>
             <div class="field">
--- a/rhodecode/templates/index_base.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/index_base.html	Thu May 10 20:27:45 2012 +0200
@@ -119,7 +119,7 @@
         </div>
     </div>
     <script>
-      YUD.get('repo_count').innerHTML = ${cnt};
+      YUD.get('repo_count').innerHTML = ${cnt+1};
       var func = function(node){
           return node.parentNode.parentNode.parentNode.parentNode;
       }
--- a/rhodecode/templates/shortlog/shortlog_data.html	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/templates/shortlog/shortlog_data.html	Thu May 10 20:27:45 2012 +0200
@@ -15,7 +15,7 @@
             <div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div>
         </td>
         <td>
-            ${h.link_to(h.truncate(cs.message,50),
+            ${h.link_to(h.truncate(cs.message,50) or _('No commit message'),
             h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
             title=cs.message)}
         </td>
@@ -25,11 +25,11 @@
 		<td title="${cs.author}">${h.person(cs.author)}</td>
 		<td>
 			<span class="logtags">
+                %if cs.branch:
 				<span class="branchtag">
-                %if h.is_hg(c.rhodecode_repo):
                     ${cs.branch}
+                </span>
                 %endif
-                </span>
 			</span>
 		</td>
 		<td>
@@ -73,7 +73,7 @@
     ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
     ${c.rhodecode_repo.alias} add README # add first file
     ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
-    ${c.rhodecode_repo.alias} push # push changes back
+    ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
 </pre>
 
 <h4>${_('Existing repository?')}</h4>
--- a/rhodecode/tests/__init__.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/__init__.py	Thu May 10 20:27:45 2012 +0200
@@ -21,13 +21,15 @@
 from routes.util import URLGenerator
 from webtest import TestApp
 
+from rhodecode import is_windows
 from rhodecode.model.meta import Session
 from rhodecode.model.db import User
 
 import pylons.test
 
 os.environ['TZ'] = 'UTC'
-time.tzset()
+if not is_windows:
+    time.tzset()
 
 log = logging.getLogger(__name__)
 
@@ -71,6 +73,7 @@
 HG_FORK = 'vcs_test_hg_fork'
 GIT_FORK = 'vcs_test_git_fork'
 
+
 class TestController(TestCase):
 
     def __init__(self, *args, **kwargs):
--- a/rhodecode/tests/functional/test_changeset_comments.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/functional/test_changeset_comments.py	Thu May 10 20:27:45 2012 +0200
@@ -75,11 +75,15 @@
                                 repo_name=HG_REPO, revision=rev))
         #test DB
         self.assertEqual(ChangesetComment.query().count(), 1)
-        self.assertTrue('''<div class="comments-number">0 comment(s)'''
-                        ''' (%s inline)</div>''' % 1 in response.body)
-        self.assertTrue('''<div class="inline-comment-placeholder-line"'''
-                        ''' line="n1" target_id="vcswebsimplevcsviews'''
-                        '''repositorypy">''' in response.body)
+        response.mustcontain(
+            '''<div class="comments-number">0 comment(s)'''
+            ''' (%s inline)</div>''' % 1
+        )
+        response.mustcontain(
+            '''<div style="display:none" class="inline-comment-placeholder" '''
+            '''path="vcs/web/simplevcs/views/repository.py" '''
+            '''target_id="vcswebsimplevcsviewsrepositorypy">'''
+        )
 
         self.assertEqual(Notification.query().count(), 1)
         self.assertEqual(ChangesetComment.query().count(), 1)
--- a/rhodecode/tests/functional/test_files.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/functional/test_files.py	Thu May 10 20:27:45 2012 +0200
@@ -129,10 +129,11 @@
 
     def test_file_annotation(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='annotate',
+        response = self.app.get(url(controller='files', action='index',
                                     repo_name=HG_REPO,
                                     revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
-                                    f_path='vcs/nodes.py'))
+                                    f_path='vcs/nodes.py',
+                                    annotate=True))
 
 
         response.mustcontain("""<optgroup label="Changesets">
@@ -196,11 +197,13 @@
                                         repo_name=HG_REPO,
                                         fname=fname))
 
-            assert response.status == '200 OK', 'wrong response code'
-            assert response.response._headers.items() == [('Pragma', 'no-cache'),
-                                                  ('Cache-Control', 'no-cache'),
-                                                  ('Content-Type', '%s; charset=utf-8' % info[0]),
-                                                  ('Content-Disposition', 'attachment; filename=%s' % filename), ], 'wrong headers'
+            self.assertEqual(response.status, '200 OK')
+            self.assertEqual(response.response._headers.items(),
+             [('Pragma', 'no-cache'),
+              ('Cache-Control', 'no-cache'),
+              ('Content-Type', '%s; charset=utf-8' % info[0]),
+              ('Content-Disposition', 'attachment; filename=%s' % filename),]
+            )
 
     def test_archival_wrong_ext(self):
         self.log_user()
@@ -211,8 +214,7 @@
             response = self.app.get(url(controller='files', action='archivefile',
                                         repo_name=HG_REPO,
                                         fname=fname))
-            assert 'Unknown archive type' in response.body
-
+            response.mustcontain('Unknown archive type')
 
     def test_archival_wrong_revision(self):
         self.log_user()
@@ -223,7 +225,7 @@
             response = self.app.get(url(controller='files', action='archivefile',
                                         repo_name=HG_REPO,
                                         fname=fname))
-            assert 'Unknown revision' in response.body
+            response.mustcontain('Unknown revision')
 
     #==========================================================================
     # RAW FILE
@@ -235,8 +237,8 @@
                                     revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
                                     f_path='vcs/nodes.py'))
 
-        assert response.content_disposition == "attachment; filename=nodes.py"
-        assert response.content_type == "text/x-python"
+        self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
+        self.assertEqual(response.content_type, "text/x-python")
 
     def test_raw_file_wrong_cs(self):
         self.log_user()
@@ -276,7 +278,7 @@
                                     revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
                                     f_path='vcs/nodes.py'))
 
-        assert response.content_type == "text/plain"
+        self.assertEqual(response.content_type, "text/plain")
 
     def test_raw_wrong_cs(self):
         self.log_user()
--- a/rhodecode/tests/functional/test_forks.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/functional/test_forks.py	Thu May 10 20:27:45 2012 +0200
@@ -1,9 +1,25 @@
 from rhodecode.tests import *
 
 from rhodecode.model.db import Repository
+from rhodecode.model.repo import RepoModel
+from rhodecode.model.user import UserModel
+
 
 class TestForksController(TestController):
 
+    def setUp(self):
+        self.username = u'forkuser'
+        self.password = u'qweqwe'
+        self.u1 = UserModel().create_or_update(
+            username=self.username, password=self.password,
+            email=u'fork_king@rhodecode.org', name=u'u1', lastname=u'u1'
+        )
+        self.Session.commit()
+
+    def tearDown(self):
+        self.Session.delete(self.u1)
+        self.Session.commit()
+
     def test_index(self):
         self.log_user()
         repo_name = HG_REPO
@@ -12,7 +28,6 @@
 
         self.assertTrue("""There are no forks yet""" in response.body)
 
-
     def test_index_with_fork(self):
         self.log_user()
 
@@ -34,7 +49,6 @@
         response = self.app.get(url(controller='forks', action='forks',
                                     repo_name=repo_name))
 
-
         self.assertTrue("""<a href="/%s/summary">"""
                          """vcs_test_hg_fork</a>""" % fork_name
                          in response.body)
@@ -42,9 +56,6 @@
         #remove this fork
         response = self.app.delete(url('repo', repo_name=fork_name))
 
-
-
-
     def test_z_fork_create(self):
         self.log_user()
         fork_name = HG_FORK
@@ -71,11 +82,9 @@
         self.assertEqual(fork_repo.repo_name, fork_name)
         self.assertEqual(fork_repo.fork.repo_name, repo_name)
 
-
         #test if fork is visible in the list ?
         response = response.follow()
 
-
         # check if fork is marked as fork
         # wait for cache to expire
         import time
@@ -84,3 +93,41 @@
                                     repo_name=fork_name))
 
         self.assertTrue('Fork of %s' % repo_name in response.body)
+
+    def test_zz_fork_permission_page(self):
+        usr = self.log_user(self.username, self.password)['user_id']
+        repo_name = HG_REPO
+
+        forks = self.Session.query(Repository)\
+            .filter(Repository.fork_id != None)\
+            .all()
+        self.assertEqual(1, len(forks))
+
+        # set read permissions for this
+        RepoModel().grant_user_permission(repo=forks[0],
+                                          user=usr,
+                                          perm='repository.read')
+        self.Session.commit()
+
+        response = self.app.get(url(controller='forks', action='forks',
+                                    repo_name=repo_name))
+
+        response.mustcontain('<div style="padding:5px 3px 3px 42px;">fork of vcs test</div>')
+
+    def test_zzz_fork_permission_page(self):
+        usr = self.log_user(self.username, self.password)['user_id']
+        repo_name = HG_REPO
+
+        forks = self.Session.query(Repository)\
+            .filter(Repository.fork_id != None)\
+            .all()
+        self.assertEqual(1, len(forks))
+
+        # set none
+        RepoModel().grant_user_permission(repo=forks[0],
+                                          user=usr, perm='repository.none')
+        self.Session.commit()
+        # fork shouldn't be there
+        response = self.app.get(url(controller='forks', action='forks',
+                                    repo_name=repo_name))
+        response.mustcontain('There are no forks yet')
--- a/rhodecode/tests/functional/test_login.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/functional/test_login.py	Thu May 10 20:27:45 2012 +0200
@@ -54,7 +54,6 @@
         self.assertEqual(response.status, '200 OK')
         self.assertTrue('Users administration' in response.body)
 
-
     def test_login_short_password(self):
         response = self.app.post(url(controller='login', action='index'),
                                  {'username':'test_admin',
@@ -101,7 +100,7 @@
                                              'lastname':'test'})
 
         self.assertEqual(response.status , '200 OK')
-        assert 'This e-mail address is already taken' in response.body
+        response.mustcontain('This e-mail address is already taken')
 
     def test_register_err_same_email_case_sensitive(self):
         response = self.app.post(url(controller='login', action='register'),
@@ -112,7 +111,7 @@
                                              'name':'test',
                                              'lastname':'test'})
         self.assertEqual(response.status , '200 OK')
-        assert 'This e-mail address is already taken' in response.body
+        response.mustcontain('This e-mail address is already taken')
 
     def test_register_err_wrong_data(self):
         response = self.app.post(url(controller='login', action='register'),
@@ -123,9 +122,8 @@
                                              'name':'test',
                                              'lastname':'test'})
         self.assertEqual(response.status , '200 OK')
-        assert 'An email address must contain a single @' in response.body
-        assert 'Enter a value 6 characters long or more' in response.body
-
+        response.mustcontain('An email address must contain a single @')
+        response.mustcontain('Enter a value 6 characters long or more')
 
     def test_register_err_username(self):
         response = self.app.post(url(controller='login', action='register'),
@@ -137,11 +135,11 @@
                                              'lastname':'test'})
 
         self.assertEqual(response.status , '200 OK')
-        assert 'An email address must contain a single @' in response.body
-        assert ('Username may only contain '
+        response.mustcontain('An email address must contain a single @')
+        response.mustcontain('Username may only contain '
                 'alphanumeric characters underscores, '
                 'periods or dashes and must begin with '
-                'alphanumeric character') in response.body
+                'alphanumeric character')
 
     def test_register_err_case_sensitive(self):
         response = self.app.post(url(controller='login', action='register'),
@@ -156,8 +154,6 @@
         self.assertTrue('An email address must contain a single @' in response.body)
         self.assertTrue('This username already exists' in response.body)
 
-
-
     def test_register_special_chars(self):
         response = self.app.post(url(controller='login', action='register'),
                                             {'username':'xxxaxn',
@@ -170,7 +166,6 @@
         self.assertEqual(response.status , '200 OK')
         self.assertTrue('Invalid characters in password' in response.body)
 
-
     def test_register_password_mismatch(self):
         response = self.app.post(url(controller='login', action='register'),
                                             {'username':'xs',
@@ -180,8 +175,8 @@
                                              'name':'test',
                                              'lastname':'test'})
 
-        self.assertEqual(response.status , '200 OK')
-        assert 'Passwords do not match' in response.body
+        self.assertEqual(response.status, '200 OK')
+        response.mustcontain('Passwords do not match')
 
     def test_register_ok(self):
         username = 'test_regular4'
@@ -196,28 +191,32 @@
                                              'password_confirmation':password,
                                              'email':email,
                                              'name':name,
-                                             'lastname':lastname})
-        self.assertEqual(response.status , '302 Found')
-        assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration'
+                                             'lastname':lastname,
+                                             'admin':True}) # This should be overriden
+        self.assertEqual(response.status, '302 Found')
+        self.checkSessionFlash(response, 'You have successfully registered into rhodecode')
 
         ret = self.Session.query(User).filter(User.username == 'test_regular4').one()
-        assert ret.username == username , 'field mismatch %s %s' % (ret.username, username)
-        assert check_password(password, ret.password) == True , 'password mismatch'
-        assert ret.email == email , 'field mismatch %s %s' % (ret.email, email)
-        assert ret.name == name , 'field mismatch %s %s' % (ret.name, name)
-        assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname)
-
+        self.assertEqual(ret.username, username)
+        self.assertEqual(check_password(password, ret.password), True)
+        self.assertEqual(ret.email, email)
+        self.assertEqual(ret.name, name)
+        self.assertEqual(ret.lastname, lastname)
+        self.assertNotEqual(ret.api_key, None)
+        self.assertEqual(ret.admin, False)
 
     def test_forgot_password_wrong_mail(self):
-        response = self.app.post(url(controller='login', action='password_reset'),
-                                            {'email':'marcin@wrongmail.org', })
+        response = self.app.post(
+                        url(controller='login', action='password_reset'),
+                            {'email': 'marcin@wrongmail.org',}
+        )
 
-        assert "This e-mail address doesn't exist" in response.body, 'Missing error message about wrong email'
+        response.mustcontain("This e-mail address doesn't exist")
 
     def test_forgot_password(self):
         response = self.app.get(url(controller='login',
                                     action='password_reset'))
-        self.assertEqual(response.status , '200 OK')
+        self.assertEqual(response.status, '200 OK')
 
         username = 'test_password_reset_1'
         password = 'qweqwe'
--- a/rhodecode/tests/mem_watch	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/mem_watch	Thu May 10 20:27:45 2012 +0200
@@ -1,1 +1,1 @@
-ps -eo size,pid,user,command --sort -size | awk '{ hr=$1/1024 ; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }'|grep paster
+ps -eo size,pid,user,command --sort -size | awk '{ hr=$1/1024 ; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }'|grep [p]aster
--- a/rhodecode/tests/rhodecode_crawler.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/rhodecode_crawler.py	Thu May 10 20:27:45 2012 +0200
@@ -32,30 +32,68 @@
 import urllib
 import urllib2
 import time
-
+import os
+import sys
 from os.path import join as jn
+from os.path import dirname as dn
+
+__here__ = os.path.abspath(__file__)
+__root__ = dn(dn(dn(__here__)))
+sys.path.append(__root__)
+
 from rhodecode.lib import vcs
+from rhodecode.lib.compat import OrderedSet
+from rhodecode.lib.vcs.exceptions import RepositoryError
 
-BASE_URI = 'http://127.0.0.1:5000/%s'
-PROJECT = 'CPython'
+PASES = 3
+HOST = 'http://127.0.0.1'
+PORT = 5000
+BASE_URI = '%s:%s/' % (HOST, PORT)
+
+if len(sys.argv) == 2:
+    BASE_URI = sys.argv[1]
+
+if not BASE_URI.endswith('/'):
+    BASE_URI += '/'
+
+print 'Crawling @ %s' % BASE_URI
+BASE_URI += '%s'
 PROJECT_PATH = jn('/', 'home', 'marcink', 'hg_repos')
+PROJECTS = [
+    'linux-magx-pbranch',
+    'CPython',
+    'rhodecode_tip',
+]
 
 
 cj = cookielib.FileCookieJar('/tmp/rc_test_cookie.txt')
 o = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
 o.addheaders = [
-                     ('User-agent', 'rhodecode-crawler'),
-                     ('Accept-Language', 'en - us, en;q = 0.5')
-                    ]
+    ('User-agent', 'rhodecode-crawler'),
+    ('Accept-Language', 'en - us, en;q = 0.5')
+]
 
 urllib2.install_opener(o)
 
 
-def test_changelog_walk(pages=100):
+def _get_repo(proj):
+    if isinstance(proj, basestring):
+        repo = vcs.get_repo(jn(PROJECT_PATH, proj))
+        proj = proj
+    else:
+        repo = proj
+        proj = repo.name
+
+    return repo, proj
+
+
+def test_changelog_walk(proj, pages=100):
+    repo, proj = _get_repo(proj)
+
     total_time = 0
     for i in range(1, pages):
 
-        page = '/'.join((PROJECT, 'changelog',))
+        page = '/'.join((proj, 'changelog',))
 
         full_uri = (BASE_URI % page) + '?' + urllib.urlencode({'page':i})
         s = time.time()
@@ -69,19 +107,21 @@
     print 'average on req', total_time / float(pages)
 
 
-def test_changeset_walk(limit=None):
-    print 'processing', jn(PROJECT_PATH, PROJECT)
+def test_changeset_walk(proj, limit=None):
+    repo, proj = _get_repo(proj)
+
+    print 'processing', jn(PROJECT_PATH, proj)
     total_time = 0
 
-    repo = vcs.get_repo(jn(PROJECT_PATH, PROJECT))
     cnt = 0
     for i in repo:
         cnt += 1
-        raw_cs = '/'.join((PROJECT, 'changeset', i.raw_id))
+        raw_cs = '/'.join((proj, 'changeset', i.raw_id))
         if limit and limit == cnt:
             break
 
         full_uri = (BASE_URI % raw_cs)
+        print '%s visiting %s\%s' % (cnt, full_uri, i)
         s = time.time()
         f = o.open(full_uri)
         size = len(f.read())
@@ -93,14 +133,11 @@
     print 'average on req', total_time / float(cnt)
 
 
-def test_files_walk(limit=100):
-    print 'processing', jn(PROJECT_PATH, PROJECT)
-    total_time = 0
+def test_files_walk(proj, limit=100):
+    repo, proj = _get_repo(proj)
 
-    repo = vcs.get_repo(jn(PROJECT_PATH, PROJECT))
-
-    from rhodecode.lib.compat import OrderedSet
-    from rhodecode.lib.vcs.exceptions import RepositoryError
+    print 'processing', jn(PROJECT_PATH, proj)
+    total_time = 0
 
     paths_ = OrderedSet([''])
     try:
@@ -124,22 +161,24 @@
         if limit and limit == cnt:
             break
 
-        file_path = '/'.join((PROJECT, 'files', 'tip', f))
-
+        file_path = '/'.join((proj, 'files', 'tip', f))
         full_uri = (BASE_URI % file_path)
+        print '%s visiting %s' % (cnt, full_uri)
         s = time.time()
         f = o.open(full_uri)
         size = len(f.read())
         e = time.time() - s
         total_time += e
-        print '%s visited %s size:%s req:%s ms' % (cnt, full_uri, size, e)
+        print '%s visited OK size:%s req:%s ms' % (cnt, size, e)
 
     print 'total_time', total_time
     print 'average on req', total_time / float(cnt)
 
-
-test_changelog_walk(40)
-time.sleep(2)
-test_changeset_walk(limit=100)
-time.sleep(2)
-test_files_walk(100)
+if __name__ == '__main__':
+    for path in PROJECTS:
+        repo = vcs.get_repo(jn(PROJECT_PATH, path))
+        for i in range(PASES):
+            print 'PASS %s/%s' % (i, PASES)
+            test_changelog_walk(repo, pages=80)
+            test_changeset_walk(repo, limit=100)
+            test_files_walk(repo, limit=100)
--- a/rhodecode/tests/test_libs.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/test_libs.py	Thu May 10 20:27:45 2012 +0200
@@ -103,9 +103,16 @@
 
     def test_mention_extractor(self):
         from rhodecode.lib.utils2 import extract_mentioned_users
-        sample = ("@first hi there @marcink here's my email marcin@email.com "
-                  "@lukaszb check it pls @ ttwelve @D[] @one@two@three "
-                  "@MARCIN    @maRCiN @2one_more22")
-        s = ['2one_more22', 'D', 'MARCIN', 'first', 'lukaszb',
-             'maRCiN', 'marcink', 'one']
+        sample = (
+            "@first hi there @marcink here's my email marcin@email.com "
+            "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three "
+            "@MARCIN    @maRCiN @2one_more22 @john please see this http://org.pl "
+            "@marian.user just do it @marco-polo and next extract @marco_polo "
+            "user.dot  hej ! not-needed maril@domain.org"
+        )
+
+        s = sorted([
+        'first', 'marcink', 'lukaszb', 'one_more22', 'MARCIN', 'maRCiN', 'john',
+        'marian.user', 'marco-polo', 'marco_polo'
+        ], key=lambda k: k.lower())
         self.assertEqual(s, extract_mentioned_users(sample))
--- a/rhodecode/tests/test_models.py	Mon Apr 23 18:31:51 2012 +0200
+++ b/rhodecode/tests/test_models.py	Thu May 10 20:27:45 2012 +0200
@@ -5,7 +5,8 @@
 from rhodecode.model.repos_group import ReposGroupModel
 from rhodecode.model.repo import RepoModel
 from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
-    UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm
+    UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm,\
+    Repository
 from sqlalchemy.exc import IntegrityError
 from rhodecode.model.user import UserModel
 
@@ -153,24 +154,23 @@
         self.assertTrue(self.__check_path('g2', 'g1'))
 
         # test repo
-        self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
-
+        self.assertEqual(r.repo_name, RepoGroup.url_sep().join(['g2', 'g1', r.just_name]))
 
     def test_move_to_root(self):
         g1 = _make_group('t11')
         Session.commit()
-        g2 = _make_group('t22',parent_id=g1.group_id)
+        g2 = _make_group('t22', parent_id=g1.group_id)
         Session.commit()
 
-        self.assertEqual(g2.full_path,'t11/t22')
+        self.assertEqual(g2.full_path, 't11/t22')
         self.assertTrue(self.__check_path('t11', 't22'))
 
         g2 = self.__update_group(g2.group_id, 'g22', parent_id=None)
         Session.commit()
 
-        self.assertEqual(g2.group_name,'g22')
+        self.assertEqual(g2.group_name, 'g22')
         # we moved out group from t1 to '' so it's full path should be 'g2'
-        self.assertEqual(g2.full_path,'g22')
+        self.assertEqual(g2.full_path, 'g22')
         self.assertFalse(self.__check_path('t11', 't22'))
         self.assertTrue(self.__check_path('g22'))
 
@@ -620,7 +620,7 @@
         # add repo to group
         form_data = {
             'repo_name':HG_REPO,
-            'repo_name_full':os.path.join(self.g1.group_name,HG_REPO),
+            'repo_name_full':RepoGroup.url_sep().join([self.g1.group_name,HG_REPO]),
             'repo_type':'hg',
             'clone_uri':'',
             'repo_group':self.g1.group_id,