changeset 3701:8155dd1837ff beta

merged rc1 into dev
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 07 Apr 2013 18:42:41 +0200
parents b185c4a56f9f (diff) 49b34e3c0812 (current diff)
children a79bb3277b32
files
diffstat 19 files changed, 1286 insertions(+), 1154 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/config/routing.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/config/routing.py	Sun Apr 07 18:42:41 2013 +0200
@@ -500,6 +500,11 @@
                 controller='changeset', revision='tip', action='comment',
                 conditions=dict(function=check_repo))
 
+    rmap.connect('changeset_comment_preview',
+                 '/{repo_name:.*?}/changeset/comment/preview',
+                controller='changeset', action='preview_comment',
+                conditions=dict(function=check_repo, method=["POST"]))
+
     rmap.connect('changeset_comment_delete',
                  '/{repo_name:.*?}/changeset/comment/{comment_id}/delete',
                 controller='changeset', action='delete_comment',
--- a/rhodecode/controllers/admin/repos.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/controllers/admin/repos.py	Sun Apr 07 18:42:41 2013 +0200
@@ -40,7 +40,7 @@
     HasPermissionAnyDecorator, HasRepoPermissionAllDecorator, NotAnonymous,\
     HasPermissionAny, HasReposGroupPermissionAny, HasRepoPermissionAnyDecorator
 from rhodecode.lib.base import BaseRepoController, render
-from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
+from rhodecode.lib.utils import action_logger, repo_name_slug
 from rhodecode.lib.helpers import get_token
 from rhodecode.model.meta import Session
 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup,\
@@ -262,7 +262,7 @@
         try:
             form_result = _form.to_python(dict(request.POST))
             repo = repo_model.update(repo_name, **form_result)
-            invalidate_cache('get_repo_cached_%s' % repo_name)
+            ScmModel().mark_for_invalidation(repo_name)
             h.flash(_('Repository %s updated successfully') % repo_name,
                     category='success')
             changed_name = repo.repo_name
@@ -315,7 +315,7 @@
             repo_model.delete(repo, forks=handle_forks)
             action_logger(self.rhodecode_user, 'admin_deleted_repo',
                   repo_name, self.ip_addr, self.sa)
-            invalidate_cache('get_repo_cached_%s' % repo_name)
+            ScmModel().mark_for_invalidation(repo_name)
             h.flash(_('Deleted repository %s') % repo_name, category='success')
             Session().commit()
         except AttachedForksError:
--- a/rhodecode/controllers/admin/settings.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/controllers/admin/settings.py	Sun Apr 07 18:42:41 2013 +0200
@@ -41,8 +41,8 @@
     HasReposGroupPermissionAll, HasReposGroupPermissionAny, AuthUser
 from rhodecode.lib.base import BaseController, render
 from rhodecode.lib.celerylib import tasks, run_task
-from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
-    set_rhodecode_config, repo_name_slug, check_git_version
+from rhodecode.lib.utils import repo2db_mapper, set_rhodecode_config, \
+    check_git_version
 from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
     RhodeCodeSetting, PullRequest, PullRequestReviewers
 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
@@ -55,7 +55,6 @@
 from rhodecode.model.meta import Session
 from rhodecode.lib.utils2 import str2bool, safe_unicode
 from rhodecode.lib.compat import json
-from webob.exc import HTTPForbidden
 log = logging.getLogger(__name__)
 
 
@@ -119,7 +118,7 @@
             initial = ScmModel().repo_scan()
             log.debug('invalidating all repositories')
             for repo_name in initial.keys():
-                invalidate_cache('get_repo_cached_%s' % repo_name)
+                ScmModel().mark_for_invalidation(repo_name)
 
             added, removed = repo2db_mapper(initial, rm_obsolete)
             _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
--- a/rhodecode/controllers/api/api.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/controllers/api/api.py	Sun Apr 07 18:42:41 2013 +0200
@@ -221,7 +221,6 @@
 
         try:
             invalidated_keys = ScmModel().mark_for_invalidation(repo.repo_name)
-            Session().commit()
             return ('Cache for repository `%s` was invalidated: '
                     'invalidated cache keys: %s' % (repoid, invalidated_keys))
         except Exception:
--- a/rhodecode/controllers/changeset.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/controllers/changeset.py	Sun Apr 07 18:42:41 2013 +0200
@@ -37,7 +37,8 @@
     ChangesetDoesNotExistError
 
 import rhodecode.lib.helpers as h
-from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
+    NotAnonymous
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.utils import action_logger
 from rhodecode.lib.compat import OrderedDict
@@ -320,6 +321,7 @@
     def changeset_download(self, revision):
         return self.index(revision, method='download')
 
+    @NotAnonymous()
     @jsonify
     def comment(self, repo_name, revision):
         status = request.POST.get('changeset_status')
@@ -382,6 +384,16 @@
 
         return data
 
+    @NotAnonymous()
+    def preview_comment(self):
+        if not request.environ.get('HTTP_X_PARTIAL_XHR'):
+            raise HTTPBadRequest()
+        text = request.POST.get('text')
+        if text:
+            return h.rst_w_mentions(text)
+        return ''
+
+    @NotAnonymous()
     @jsonify
     def delete_comment(self, repo_name, comment_id):
         co = ChangesetComment.get(comment_id)
--- a/rhodecode/lib/base.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/lib/base.py	Sun Apr 07 18:42:41 2013 +0200
@@ -21,7 +21,7 @@
     safe_str, safe_int
 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
     HasPermissionAnyMiddleware, CookieStoreWrapper
-from rhodecode.lib.utils import get_repo_slug, invalidate_cache
+from rhodecode.lib.utils import get_repo_slug
 from rhodecode.model import meta
 
 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
@@ -149,7 +149,7 @@
 
         :param repo_name: full repo name, also a cache key
         """
-        invalidate_cache('get_repo_cached_%s' % repo_name)
+        ScmModel().mark_for_invalidation(repo_name)
 
     def _check_permission(self, action, user, repo_name, ip_addr=None):
         """
--- a/rhodecode/lib/utils.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/lib/utils.py	Sun Apr 07 18:42:41 2013 +0200
@@ -356,19 +356,6 @@
         config[k] = v
 
 
-def invalidate_cache(cache_key, *args):
-    """
-    Puts cache invalidation task into db for
-    further global cache invalidation
-    """
-
-    from rhodecode.model.scm import ScmModel
-
-    if cache_key.startswith('get_repo_cached_'):
-        name = cache_key.split('get_repo_cached_')[-1]
-        ScmModel().mark_for_invalidation(name)
-
-
 def map_groups(path):
     """
     Given a full path to a repository, create all nested groups that this
@@ -480,9 +467,9 @@
                 log.debug("Removing non-existing repository found in db `%s`" %
                           repo.repo_name)
                 try:
-                    sa.delete(repo)
+                    removed.append(repo.repo_name)
+                    RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
                     sa.commit()
-                    removed.append(repo.repo_name)
                 except Exception:
                     #don't hold further removals on error
                     log.error(traceback.format_exc())
--- a/rhodecode/lib/vcs/backends/base.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/lib/vcs/backends/base.py	Sun Apr 07 18:42:41 2013 +0200
@@ -80,6 +80,10 @@
     def __len__(self):
         return self.count()
 
+    def __eq__(self, other):
+        same_instance = isinstance(other, self.__class__)
+        return same_instance and getattr(other, 'path', None) == self.path
+
     @LazyProperty
     def alias(self):
         for k, v in settings.BACKENDS.items():
--- a/rhodecode/model/db.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/model/db.py	Sun Apr 07 18:42:41 2013 +0200
@@ -1147,14 +1147,14 @@
 
     def set_invalidate(self):
         """
-        set a cache for invalidation for this instance
+        Mark caches of this repo as invalid.
         """
         CacheInvalidation.set_invalidate(repo_name=self.repo_name)
 
     def scm_instance_no_cache(self):
         return self.__get_instance()
 
-    @LazyProperty
+    @property
     def scm_instance(self):
         import rhodecode
         full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
@@ -1167,7 +1167,6 @@
         def _c(repo_name):
             return self.__get_instance()
         rn = self.repo_name
-        log.debug('Getting cached instance of repo')
 
         if cache_map:
             # get using prefilled cache_map
@@ -1181,8 +1180,11 @@
 
         if invalidate_repo is not None:
             region_invalidate(_c, None, rn)
+            log.debug('Cache for %s invalidated, getting new object' % (rn))
             # update our cache
             CacheInvalidation.set_valid(invalidate_repo.cache_key)
+        else:
+            log.debug('Getting obj for %s from cache' % (rn))
         return _c(rn)
 
     def __get_instance(self):
@@ -1636,61 +1638,58 @@
     cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
     # cache_key as created by _get_cache_key
     cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    # cache_args is usually a repo_name, possibly with _README/_RSS/_ATOM suffix
+    # cache_args is a repo_name
     cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    # instance sets cache_active True when it is caching, other instances set cache_active to False to invalidate
+    # instance sets cache_active True when it is caching,
+    # other instances set cache_active to False to indicate that this cache is invalid
     cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
 
-    def __init__(self, cache_key, cache_args=''):
+    def __init__(self, cache_key, repo_name=''):
         self.cache_key = cache_key
-        self.cache_args = cache_args
+        self.cache_args = repo_name
         self.cache_active = False
 
     def __unicode__(self):
-        return u"<%s('%s:%s')>" % (self.__class__.__name__,
-                                  self.cache_id, self.cache_key)
+        return u"<%s('%s:%s[%s]')>" % (self.__class__.__name__,
+                            self.cache_id, self.cache_key, self.cache_active)
+
+    def _cache_key_partition(self):
+        prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
+        return prefix, repo_name, suffix
 
     def get_prefix(self):
         """
-        Guess prefix that might have been used in _get_cache_key to generate self.cache_key .
-        Only used for informational purposes in repo_edit.html .
+        get prefix that might have been used in _get_cache_key to
+        generate self.cache_key. Only used for informational purposes
+        in repo_edit.html.
         """
-        _split = self.cache_key.split(self.cache_args, 1)
-        if len(_split) == 2:
-            return _split[0]
-        return ''
+        # prefix, repo_name, suffix
+        return self._cache_key_partition()[0]
+
+    def get_suffix(self):
+        """
+        get suffix that might have been used in _get_cache_key to
+        generate self.cache_key. Only used for informational purposes
+        in repo_edit.html.
+        """
+        # prefix, repo_name, suffix
+        return self._cache_key_partition()[2]
 
     @classmethod
     def _get_cache_key(cls, key):
         """
         Wrapper for generating a unique cache key for this instance and "key".
+        key must / will start with a repo_name which will be stored in .cache_args .
         """
         import rhodecode
         prefix = rhodecode.CONFIG.get('instance_id', '')
         return "%s%s" % (prefix, key)
 
     @classmethod
-    def _get_or_create_inv_obj(cls, key, repo_name, commit=True):
-        inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
-        if not inv_obj:
-            try:
-                inv_obj = CacheInvalidation(key, repo_name)
-                Session().add(inv_obj)
-                if commit:
-                    Session().commit()
-            except Exception:
-                log.error(traceback.format_exc())
-                Session().rollback()
-        return inv_obj
-
-    @classmethod
     def invalidate(cls, key):
         """
-        Returns Invalidation object if this given key should be invalidated
-        None otherwise. `cache_active = False` means that this cache
-        state is not valid and needs to be invalidated
-
-        :param key:
+        Returns Invalidation object if the local cache with the given key is invalid,
+        None otherwise.
         """
         repo_name = key
         repo_name = remove_suffix(repo_name, '_README')
@@ -1698,33 +1697,35 @@
         repo_name = remove_suffix(repo_name, '_ATOM')
 
         cache_key = cls._get_cache_key(key)
-        inv = cls._get_or_create_inv_obj(cache_key, repo_name)
+        inv_obj = Session().query(cls).filter(cls.cache_key == cache_key).scalar()
+        if not inv_obj:
+            try:
+                inv_obj = CacheInvalidation(cache_key, repo_name)
+                Session().add(inv_obj)
+                Session().commit()
+            except Exception:
+                log.error(traceback.format_exc())
+                Session().rollback()
+                return
 
-        if inv and not inv.cache_active:
-            return inv
+        if not inv_obj.cache_active:
+            # `cache_active = False` means that this cache
+            # no longer is valid
+            return inv_obj
 
     @classmethod
-    def set_invalidate(cls, key=None, repo_name=None):
+    def set_invalidate(cls, repo_name):
         """
-        Mark this Cache key for invalidation, either by key or whole
-        cache sets based on repo_name
-
-        :param key:
+        Mark all caches of a repo as invalid in the database.
         """
         invalidated_keys = []
-        if key:
-            assert not repo_name
-            cache_key = cls._get_cache_key(key)
-            inv_objs = Session().query(cls).filter(cls.cache_key == cache_key).all()
-        else:
-            assert repo_name
-            inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
+        inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
 
         try:
             for inv_obj in inv_objs:
+                log.debug('marking %s key for invalidation based on repo_name=%s'
+                          % (inv_obj, safe_str(repo_name)))
                 inv_obj.cache_active = False
-                log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
-                  % (inv_obj, key, safe_str(repo_name)))
                 invalidated_keys.append(inv_obj.cache_key)
                 Session().add(inv_obj)
             Session().commit()
@@ -1734,13 +1735,11 @@
         return invalidated_keys
 
     @classmethod
-    def set_valid(cls, key):
+    def set_valid(cls, cache_key):
         """
         Mark this cache key as active and currently cached
-
-        :param key:
         """
-        inv_obj = cls.query().filter(cls.cache_key == key).scalar()
+        inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
         inv_obj.cache_active = True
         Session().add(inv_obj)
         Session().commit()
--- a/rhodecode/model/repo.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/model/repo.py	Sun Apr 07 18:42:41 2013 +0200
@@ -466,7 +466,7 @@
         from rhodecode.lib.celerylib import tasks, run_task
         run_task(tasks.create_repo_fork, form_data, cur_user)
 
-    def delete(self, repo, forks=None):
+    def delete(self, repo, forks=None, fs_remove=True):
         """
         Delete given repository, forks parameter defines what do do with
         attached forks. Throws AttachedForksError if deleted repo has attached
@@ -474,6 +474,7 @@
 
         :param repo:
         :param forks: str 'delete' or 'detach'
+        :param fs_remove: remove(archive) repo from filesystem
         """
         repo = self._get_repo(repo)
         if repo:
@@ -491,7 +492,10 @@
             owner = repo.user
             try:
                 self.sa.delete(repo)
-                self.__delete_repo(repo)
+                if fs_remove:
+                    self.__delete_repo(repo)
+                else:
+                    log.debug('skipping removal from filesystem')
                 log_delete_repository(old_repo_dict,
                                       deleted_by=owner.username)
             except Exception:
--- a/rhodecode/model/scm.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/model/scm.py	Sun Apr 07 18:42:41 2013 +0200
@@ -297,12 +297,11 @@
 
     def mark_for_invalidation(self, repo_name):
         """
-        Puts cache invalidation task into db for
-        further global cache invalidation
+        Mark caches of this repo invalid in the database.
 
-        :param repo_name: this repo that should invalidation take place
+        :param repo_name: the repo for which caches should be marked invalid
         """
-        invalidated_keys = CacheInvalidation.set_invalidate(repo_name=repo_name)
+        invalidated_keys = CacheInvalidation.set_invalidate(repo_name)
         repo = Repository.get_by_repo_name(repo_name)
         if repo:
             repo.update_changeset_cache()
--- a/rhodecode/public/css/style.css	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/public/css/style.css	Sun Apr 07 18:42:41 2013 +0200
@@ -218,6 +218,8 @@
 
 a.permalink {
     visibility: hidden;
+    position: absolute;
+    margin: 3px 4px;
 }
 
 a.permalink:hover {
@@ -2521,6 +2523,16 @@
     text-align: left;
 }
 
+#graph_content .container .checkbox {
+    width: 12px;
+    font-size: 0.85em;
+}
+
+#graph_content .container .status {
+    width: 14px;
+    font-size: 0.85em;
+}
+
 #graph_content .container .author {
    width: 105px;
 }
@@ -2551,10 +2563,6 @@
     position: relative;
 }
 
-#graph_content #changesets td.checkbox {
-    width: 20px;
-}
-
 #graph_content .container .changeset_range {
     float: left;
     margin: 6px 3px;
@@ -4291,12 +4299,6 @@
     clear: both
 }
 
-.comment-form .clearfix {
-    background: #EEE;
-    -webkit-border-radius: 4px;
-    border-radius: 4px;
-    padding: 10px;
-}
 
 div.comment-form {
     margin-top: 20px;
@@ -4318,6 +4320,13 @@
     margin-left: 10px;
 }
 
+.comment-inline-form .comment-block-ta,
+.comment-form .comment-block-ta {
+    border: 1px solid #ccc;
+    border-radius: 3px;
+    box-sizing: border-box;
+}
+
 .comment-form-submit {
     margin-top: 5px;
     margin-left: 525px;
@@ -4332,9 +4341,22 @@
 }
 
 .comment-form .comment-help {
-    padding: 0px 0px 5px 0px;
+    padding: 5px 5px 5px 5px;
     color: #666;
 }
+.comment-form .comment-help .preview-btn,
+.comment-form .comment-help .edit-btn {
+    float: right;
+    margin: -6px 0px 0px 0px;
+}
+
+.comment-form .preview-box.unloaded,
+.comment-inline-form .preview-box.unloaded {
+    height: 50px;
+    text-align: center;
+    padding: 20px;
+    background-color: #fafafa;
+}
 
 .comment-form .comment-button {
     padding-top: 5px;
@@ -4348,7 +4370,7 @@
 
 .comment .buttons {
     float: right;
-    padding: 2px 2px 0px 0px;
+    margin: -1px 0px 0px 0px;
 }
 
 
@@ -4358,6 +4380,9 @@
 }
 
 /** comment inline form **/
+.comment-inline-form {
+    margin: 4px;
+}
 .comment-inline-form .overlay {
     display: none;
 }
@@ -4376,11 +4401,13 @@
     margin-top: 5%;
 }
 
-.comment-inline-form .clearfix {
+.comment-inline-form .clearfix,
+.comment-form .clearfix {
     background: #EEE;
     -webkit-border-radius: 4px;
     border-radius: 4px;
     padding: 5px;
+    margin: 0px;
 }
 
 div.comment-inline-form {
@@ -4417,9 +4444,14 @@
 }
 
 .comment-inline-form .comment-help {
-    padding: 0px 0px 2px 0px;
-    color: #666666;
-    font-size: 10px;
+    padding: 5px 5px 5px 5px;
+    color: #666;
+}
+
+.comment-inline-form .comment-help .preview-btn,
+.comment-inline-form .comment-help .edit-btn {
+    float: right;
+    margin: -6px 0px 0px 0px;
 }
 
 .comment-inline-form .comment-button {
--- a/rhodecode/public/js/rhodecode.js	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/public/js/rhodecode.js	Sun Apr 07 18:42:41 2013 +0200
@@ -3,13 +3,13 @@
 **/
 
 if (typeof console == "undefined" || typeof console.log == "undefined"){
-	console = { log: function() {} }
+    console = { log: function() {} }
 }
 
 
 var str_repeat = function(i, m) {
-	for (var o = []; m > 0; o[--m] = i);
-	return o.join('');
+    for (var o = []; m > 0; o[--m] = i);
+    return o.join('');
 };
 
 /**
@@ -19,48 +19,48 @@
  * Inspired by https://gist.github.com/1049426
  */
 String.prototype.format = function() {
-	  
-	  function format() {
-	    var str = this;
-	    var len = arguments.length+1;
-	    var safe = undefined;
-	    var arg = undefined;
-	    
-	    // For each {0} {1} {n...} replace with the argument in that position.  If 
-	    // the argument is an object or an array it will be stringified to JSON.
-	    for (var i=0; i < len; arg = arguments[i++]) {
-	      safe = typeof arg === 'object' ? JSON.stringify(arg) : arg;
-	      str = str.replace(RegExp('\\{'+(i-1)+'\\}', 'g'), safe);
-	    }
-	    return str;
-	  }
+
+      function format() {
+        var str = this;
+        var len = arguments.length+1;
+        var safe = undefined;
+        var arg = undefined;
 
-	  // Save a reference of what may already exist under the property native.  
-	  // Allows for doing something like: if("".format.native) { /* use native */ }
-	  format.native = String.prototype.format;
+        // For each {0} {1} {n...} replace with the argument in that position.  If
+        // the argument is an object or an array it will be stringified to JSON.
+        for (var i=0; i < len; arg = arguments[i++]) {
+          safe = typeof arg === 'object' ? JSON.stringify(arg) : arg;
+          str = str.replace(RegExp('\\{'+(i-1)+'\\}', 'g'), safe);
+        }
+        return str;
+      }
 
-	  // Replace the prototype property
-	  return format;
+      // Save a reference of what may already exist under the property native.
+      // Allows for doing something like: if("".format.native) { /* use native */ }
+      format.native = String.prototype.format;
+
+      // Replace the prototype property
+      return format;
 
 }();
 
 String.prototype.strip = function(char) {
-	if(char === undefined){
-	    char = '\\s';
-	}
-	return this.replace(new RegExp('^'+char+'+|'+char+'+$','g'), '');
+    if(char === undefined){
+        char = '\\s';
+    }
+    return this.replace(new RegExp('^'+char+'+|'+char+'+$','g'), '');
 }
 String.prototype.lstrip = function(char) {
-	if(char === undefined){
-	    char = '\\s';
-	}
-	return this.replace(new RegExp('^'+char+'+'),'');
+    if(char === undefined){
+        char = '\\s';
+    }
+    return this.replace(new RegExp('^'+char+'+'),'');
 }
 String.prototype.rstrip = function(char) {
-	if(char === undefined){
-	    char = '\\s';
-	}
-	return this.replace(new RegExp(''+char+'+$'),'');
+    if(char === undefined){
+        char = '\\s';
+    }
+    return this.replace(new RegExp(''+char+'+$'),'');
 }
 
 
@@ -90,30 +90,30 @@
  * SmartColorGenerator
  *
  *usage::
- *	var CG = new ColorGenerator();
+ *  var CG = new ColorGenerator();
  *  var col = CG.getColor(key); //returns array of RGB
  *  'rgb({0})'.format(col.join(',')
- * 
+ *
  * @returns {ColorGenerator}
  */
 var ColorGenerator = function(){
-	this.GOLDEN_RATIO = 0.618033988749895;
-	this.CURRENT_RATIO = 0.22717784590367374 // this can be random
-	this.HSV_1 = 0.75;//saturation
-	this.HSV_2 = 0.95;
-	this.color;
-	this.cacheColorMap = {};
+    this.GOLDEN_RATIO = 0.618033988749895;
+    this.CURRENT_RATIO = 0.22717784590367374 // this can be random
+    this.HSV_1 = 0.75;//saturation
+    this.HSV_2 = 0.95;
+    this.color;
+    this.cacheColorMap = {};
 };
 
 ColorGenerator.prototype = {
     getColor:function(key){
-    	if(this.cacheColorMap[key] !== undefined){
-    		return this.cacheColorMap[key];
-    	}
-    	else{
-    		this.cacheColorMap[key] = this.generateColor();
-    		return this.cacheColorMap[key];
-    	}
+        if(this.cacheColorMap[key] !== undefined){
+            return this.cacheColorMap[key];
+        }
+        else{
+            this.cacheColorMap[key] = this.generateColor();
+            return this.cacheColorMap[key];
+        }
     },
     _hsvToRgb:function(h,s,v){
         if (s == 0.0)
@@ -124,18 +124,18 @@
         q = v * (1.0 - s * f)
         t = v * (1.0 - s * (1.0 - f))
         i = i % 6
-        if (i == 0) 
+        if (i == 0)
             return [v, t, p]
-        if (i == 1) 
+        if (i == 1)
             return [q, v, p]
-        if (i == 2) 
+        if (i == 2)
             return [p, v, t]
         if (i == 3)
             return [p, q, v]
-        if (i == 4) 
+        if (i == 4)
             return [t, p, v]
         if (i == 5)
-            return [v, p, q]            	
+            return [v, p, q]
     },
     generateColor:function(){
         this.CURRENT_RATIO = this.CURRENT_RATIO+this.GOLDEN_RATIO;
@@ -143,20 +143,20 @@
         HSV_tuple = [this.CURRENT_RATIO, this.HSV_1, this.HSV_2]
         RGB_tuple = this._hsvToRgb(HSV_tuple[0],HSV_tuple[1],HSV_tuple[2]);
         function toRgb(v){
-        	return ""+parseInt(v*256)
+            return ""+parseInt(v*256)
         }
         return [toRgb(RGB_tuple[0]),toRgb(RGB_tuple[1]),toRgb(RGB_tuple[2])];
-        
+
     }
 }
 
 /**
  * PyRoutesJS
- * 
+ *
  * Usage pyroutes.url('mark_error_fixed',{"error_id":error_id}) // /mark_error_fixed/<error_id>
  */
 var pyroutes = (function() {
-	// access global map defined in special file pyroutes
+    // access global map defined in special file pyroutes
     var matchlist = PROUTES_MAP;
     var sprintf = (function() {
         function get_type(variable) {
@@ -285,7 +285,7 @@
         'url': function(route_name, params) {
             var result = route_name;
             if (typeof(params) != 'object'){
-            	params = {};
+                params = {};
             }
             if (matchlist.hasOwnProperty(route_name)) {
                 var route = matchlist[route_name];
@@ -296,40 +296,40 @@
                         throw new Error(route[1][i] + ' missing in "' + route_name + '" route generation');
                 }
                 result = sprintf(route[0], params);
-                
+
                 var ret = [];
                 //extra params => GET
                 for(param in params){
-                	if (route[1].indexOf(param) == -1){
-                		ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));	
-                	}
+                    if (route[1].indexOf(param) == -1){
+                        ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
+                    }
                 }
                 var _parts = ret.join("&");
                 if(_parts){
-                	result = result +'?'+ _parts
+                    result = result +'?'+ _parts
                 }
             }
 
             return result;
         },
-    	'register': function(route_name, route_tmpl, req_params) {
-    		if (typeof(req_params) != 'object') {
-    			req_params = [];
-    		}
-    		//fix escape
-    		route_tmpl = unescape(route_tmpl);
-    		keys = [];
-    		for (o in req_params){
-    			keys.push(req_params[o])
-    		}
-    		matchlist[route_name] = [
-    		    route_tmpl,
-    		    keys
-    		]
-    	},
-    	'_routes': function(){
-    		return matchlist;
-    	}
+        'register': function(route_name, route_tmpl, req_params) {
+            if (typeof(req_params) != 'object') {
+                req_params = [];
+            }
+            //fix escape
+            route_tmpl = unescape(route_tmpl);
+            keys = [];
+            for (o in req_params){
+                keys.push(req_params[o])
+            }
+            matchlist[route_name] = [
+                route_tmpl,
+                keys
+            ]
+        },
+        '_routes': function(){
+            return matchlist;
+        }
     }
 })();
 
@@ -345,31 +345,31 @@
 
 // defines if push state is enabled for this browser ?
 var push_state_enabled = Boolean(
-		window.history && window.history.pushState && window.history.replaceState
-		&& !(   /* disable for versions of iOS before version 4.3 (8F190) */
-				(/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent)
-				/* disable for the mercury iOS browser, or at least older versions of the webkit engine */
-				|| (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent)
-		)
+        window.history && window.history.pushState && window.history.replaceState
+        && !(   /* disable for versions of iOS before version 4.3 (8F190) */
+                (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent)
+                /* disable for the mercury iOS browser, or at least older versions of the webkit engine */
+                || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent)
+        )
 );
 
 var _run_callbacks = function(callbacks){
-	if (callbacks !== undefined){
-		var _l = callbacks.length;
-	    for (var i=0;i<_l;i++){
-	    	var func = callbacks[i];
-	    	if(typeof(func)=='function'){
-	            try{
-	          	    func();
-	            }catch (err){};            		
-	    	}
-	    }
-	}		
+    if (callbacks !== undefined){
+        var _l = callbacks.length;
+        for (var i=0;i<_l;i++){
+            var func = callbacks[i];
+            if(typeof(func)=='function'){
+                try{
+                    func();
+                }catch (err){};
+            }
+        }
+    }
 }
 
 /**
  * Partial Ajax Implementation
- * 
+ *
  * @param url: defines url to make partial request
  * @param container: defines id of container to input partial result
  * @param s_call: success callback function that takes o as arg
@@ -382,44 +382,44 @@
  *  o.responseXML
  *  o.argument
  * @param f_call: failure callback
- * @param args arguments 
+ * @param args arguments
  */
 function ypjax(url,container,s_call,f_call,args){
-	var method='GET';
-	if(args===undefined){
-		args=null;
-	}
-	
-	// Set special header for partial ajax == HTTP_X_PARTIAL_XHR
-	YUC.initHeader('X-PARTIAL-XHR',true);
-	
-	// wrapper of passed callback
-	var s_wrapper = (function(o){
-		return function(o){
-			YUD.get(container).innerHTML=o.responseText;
-			YUD.setStyle(container,'opacity','1.0');
-    		//execute the given original callback
-    		if (s_call !== undefined){
-    			s_call(o);
-    		}
-		}
-	})()	
-	YUD.setStyle(container,'opacity','0.3');
-	YUC.asyncRequest(method,url,{
-		success:s_wrapper,
-		failure:function(o){
-			console.log(o);
-			YUD.get(container).innerHTML='<span class="error_red">ERROR: {0}</span>'.format(o.status);
-			YUD.setStyle(container,'opacity','1.0');
-		},
-		cache:false
-	},args);
-	
+    var method='GET';
+    if(args===undefined){
+        args=null;
+    }
+
+    // Set special header for partial ajax == HTTP_X_PARTIAL_XHR
+    YUC.initHeader('X-PARTIAL-XHR',true);
+
+    // wrapper of passed callback
+    var s_wrapper = (function(o){
+        return function(o){
+            YUD.get(container).innerHTML=o.responseText;
+            YUD.setStyle(container,'opacity','1.0');
+            //execute the given original callback
+            if (s_call !== undefined){
+                s_call(o);
+            }
+        }
+    })()
+    YUD.setStyle(container,'opacity','0.3');
+    YUC.asyncRequest(method,url,{
+        success:s_wrapper,
+        failure:function(o){
+            console.log(o);
+            YUD.get(container).innerHTML='<span class="error_red">ERROR: {0}</span>'.format(o.status);
+            YUD.setStyle(container,'opacity','1.0');
+        },
+        cache:false
+    },args);
+
 };
 
 var ajaxGET = function(url,success) {
-	// Set special header for ajax == HTTP_X_PARTIAL_XHR
-	YUC.initHeader('X-PARTIAL-XHR',true);
+    // Set special header for ajax == HTTP_X_PARTIAL_XHR
+    YUC.initHeader('X-PARTIAL-XHR',true);
 
     var sUrl = url;
     var callback = {
@@ -438,20 +438,20 @@
 
 
 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('&');
-	};
-	
+    // 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,
@@ -469,8 +469,8 @@
  * tooltip activate
  */
 var tooltip_activate = function(){
-	yt = YAHOO.yuitip.main;
-	YUE.onDOMReady(yt.init);
+    yt = YAHOO.yuitip.main;
+    YUE.onDOMReady(yt.init);
 };
 
 /**
@@ -488,26 +488,26 @@
  * show changeset tooltip
  */
 var show_changeset_tooltip = function(){
-	YUE.on(YUD.getElementsByClassName('lazy-cs'), 'mouseover', function(e){
-		var target = e.currentTarget;
-		var rid = YUD.getAttribute(target,'raw_id');
-		var repo_name = YUD.getAttribute(target,'repo_name');
-		var ttid = 'tt-'+rid;
-		var success = function(o){
-			var json = JSON.parse(o.responseText);
-			YUD.addClass(target,'tooltip')
-			YUD.setAttribute(target, 'title',json['message']);
-			YAHOO.yuitip.main.show_yuitip(e, target);
-		}
-		if(rid && !YUD.hasClass(target, 'tooltip')){
-			YUD.setAttribute(target,'id',ttid);
-			YUD.setAttribute(target, 'title',_TM['loading...']);
-			YAHOO.yuitip.main.set_listeners(target);
-			YAHOO.yuitip.main.show_yuitip(e, target);
-			var url = pyroutes.url('changeset_info', {"repo_name":repo_name, "revision": rid});
-			ajaxGET(url, success)
-		}
-	});
+    YUE.on(YUD.getElementsByClassName('lazy-cs'), 'mouseover', function(e){
+        var target = e.currentTarget;
+        var rid = YUD.getAttribute(target,'raw_id');
+        var repo_name = YUD.getAttribute(target,'repo_name');
+        var ttid = 'tt-'+rid;
+        var success = function(o){
+            var json = JSON.parse(o.responseText);
+            YUD.addClass(target,'tooltip')
+            YUD.setAttribute(target, 'title',json['message']);
+            YAHOO.yuitip.main.show_yuitip(e, target);
+        }
+        if(rid && !YUD.hasClass(target, 'tooltip')){
+            YUD.setAttribute(target,'id',ttid);
+            YUD.setAttribute(target, 'title',_TM['loading...']);
+            YAHOO.yuitip.main.set_listeners(target);
+            YAHOO.yuitip.main.show_yuitip(e, target);
+            var url = pyroutes.url('changeset_info', {"repo_name":repo_name, "revision": rid});
+            ajaxGET(url, success)
+        }
+    });
 };
 
 var onSuccessFollow = function(target){
@@ -541,7 +541,7 @@
     }
     YUC.asyncRequest('POST',TOGGLE_FOLLOW_URL,{
         success:function(o){
-        	onSuccessFollow(target);
+            onSuccessFollow(target);
         }
     },args);
     return false;
@@ -556,7 +556,7 @@
     }
     YUC.asyncRequest('POST',TOGGLE_FOLLOW_URL,{
         success:function(o){
-        	onSuccessFollow(target);
+            onSuccessFollow(target);
         }
     },args);
     return false;
@@ -564,18 +564,18 @@
 
 var showRepoSize = function(target, repo_name, token){
     var args= 'auth_token='+token;
-    
+
     if(!YUD.hasClass(target, 'loaded')){
         YUD.get(target).innerHTML = _TM['Loading ...'];
         var url = pyroutes.url('repo_size', {"repo_name":repo_name});
         YUC.asyncRequest('POST',url,{
             success:function(o){
-            	YUD.get(target).innerHTML = JSON.parse(o.responseText);
-            	YUD.addClass(target, 'loaded');
+                YUD.get(target).innerHTML = JSON.parse(o.responseText);
+                YUD.addClass(target, 'loaded');
             }
-        },args);    	
+        },args);
     }
-    return false;	
+    return false;
 }
 
 /**
@@ -584,207 +584,207 @@
 YAHOO.namespace('yuitip');
 YAHOO.yuitip.main = {
 
-	$:			YAHOO.util.Dom.get,
+    $:          YAHOO.util.Dom.get,
 
-	bgColor:	'#000',
-	speed:		0.3,
-	opacity:	0.9,
-	offset:		[15,15],
-	useAnim:	false,
-	maxWidth:	600,
-	add_links:	false,
-	yuitips:    [],
+    bgColor:    '#000',
+    speed:      0.3,
+    opacity:    0.9,
+    offset:     [15,15],
+    useAnim:    false,
+    maxWidth:   600,
+    add_links:  false,
+    yuitips:    [],
 
-	set_listeners: function(tt){
-		YUE.on(tt, 'mouseover', yt.show_yuitip,  tt);
-		YUE.on(tt, 'mousemove', yt.move_yuitip,  tt);
-		YUE.on(tt, 'mouseout',  yt.close_yuitip, tt);		
-	},
+    set_listeners: function(tt){
+        YUE.on(tt, 'mouseover', yt.show_yuitip,  tt);
+        YUE.on(tt, 'mousemove', yt.move_yuitip,  tt);
+        YUE.on(tt, 'mouseout',  yt.close_yuitip, tt);
+    },
 
-	init: function(){
-		yt.tipBox = yt.$('tip-box');
-		if(!yt.tipBox){
-			yt.tipBox = document.createElement('div');
-			document.body.appendChild(yt.tipBox);
-			yt.tipBox.id = 'tip-box';
-		}
+    init: function(){
+        yt.tipBox = yt.$('tip-box');
+        if(!yt.tipBox){
+            yt.tipBox = document.createElement('div');
+            document.body.appendChild(yt.tipBox);
+            yt.tipBox.id = 'tip-box';
+        }
 
-		YUD.setStyle(yt.tipBox, 'display', 'none');
-		YUD.setStyle(yt.tipBox, 'position', 'absolute');
-		if(yt.maxWidth !== null){
-			YUD.setStyle(yt.tipBox, 'max-width', yt.maxWidth+'px');
-		}
+        YUD.setStyle(yt.tipBox, 'display', 'none');
+        YUD.setStyle(yt.tipBox, 'position', 'absolute');
+        if(yt.maxWidth !== null){
+            YUD.setStyle(yt.tipBox, 'max-width', yt.maxWidth+'px');
+        }
 
-		var yuitips = YUD.getElementsByClassName('tooltip');
+        var yuitips = YUD.getElementsByClassName('tooltip');
 
-		if(yt.add_links === true){
-			var links = document.getElementsByTagName('a');
-			var linkLen = links.length;
-			for(i=0;i<linkLen;i++){
-				yuitips.push(links[i]);
-			}
-		}
+        if(yt.add_links === true){
+            var links = document.getElementsByTagName('a');
+            var linkLen = links.length;
+            for(i=0;i<linkLen;i++){
+                yuitips.push(links[i]);
+            }
+        }
 
-		var yuiLen = yuitips.length;
+        var yuiLen = yuitips.length;
 
-		for(i=0;i<yuiLen;i++){
-			yt.set_listeners(yuitips[i]);
-		}
-	},
+        for(i=0;i<yuiLen;i++){
+            yt.set_listeners(yuitips[i]);
+        }
+    },
 
-	show_yuitip: function(e, el){
-		YUE.stopEvent(e);
-		if(el.tagName.toLowerCase() === 'img'){
-			yt.tipText = el.alt ? el.alt : '';
-		} else {
-			yt.tipText = el.title ? el.title : '';
-		}
+    show_yuitip: function(e, el){
+        YUE.stopEvent(e);
+        if(el.tagName.toLowerCase() === 'img'){
+            yt.tipText = el.alt ? el.alt : '';
+        } else {
+            yt.tipText = el.title ? el.title : '';
+        }
 
-		if(yt.tipText !== ''){
-			// save org title
-			YUD.setAttribute(el, 'tt_title', yt.tipText);
-			// reset title to not show org tooltips
-			YUD.setAttribute(el, 'title', '');
+        if(yt.tipText !== ''){
+            // save org title
+            YUD.setAttribute(el, 'tt_title', yt.tipText);
+            // reset title to not show org tooltips
+            YUD.setAttribute(el, 'title', '');
 
-			yt.tipBox.innerHTML = yt.tipText;
-			YUD.setStyle(yt.tipBox, 'display', 'block');
-			if(yt.useAnim === true){
-				YUD.setStyle(yt.tipBox, 'opacity', '0');
-				var newAnim = new YAHOO.util.Anim(yt.tipBox,
-					{
-						opacity: { to: yt.opacity }
-					}, yt.speed, YAHOO.util.Easing.easeOut
-				);
-				newAnim.animate();
-			}
-		}
-	},
+            yt.tipBox.innerHTML = yt.tipText;
+            YUD.setStyle(yt.tipBox, 'display', 'block');
+            if(yt.useAnim === true){
+                YUD.setStyle(yt.tipBox, 'opacity', '0');
+                var newAnim = new YAHOO.util.Anim(yt.tipBox,
+                    {
+                        opacity: { to: yt.opacity }
+                    }, yt.speed, YAHOO.util.Easing.easeOut
+                );
+                newAnim.animate();
+            }
+        }
+    },
 
-	move_yuitip: function(e, el){
-		YUE.stopEvent(e);
-		var movePos = YUE.getXY(e);
-		YUD.setStyle(yt.tipBox, 'top', (movePos[1] + yt.offset[1]) + 'px');
-		YUD.setStyle(yt.tipBox, 'left', (movePos[0] + yt.offset[0]) + 'px');
-	},
+    move_yuitip: function(e, el){
+        YUE.stopEvent(e);
+        var movePos = YUE.getXY(e);
+        YUD.setStyle(yt.tipBox, 'top', (movePos[1] + yt.offset[1]) + 'px');
+        YUD.setStyle(yt.tipBox, 'left', (movePos[0] + yt.offset[0]) + 'px');
+    },
+
+    close_yuitip: function(e, el){
+        YUE.stopEvent(e);
 
-	close_yuitip: function(e, el){
-		YUE.stopEvent(e);
-	
-		if(yt.useAnim === true){
-			var newAnim = new YAHOO.util.Anim(yt.tipBox,
-				{
-					opacity: { to: 0 }
-				}, yt.speed, YAHOO.util.Easing.easeOut
-			);
-			newAnim.animate();
-		} else {
-			YUD.setStyle(yt.tipBox, 'display', 'none');
-		}
-		YUD.setAttribute(el,'title', YUD.getAttribute(el, 'tt_title'));
-	}
+        if(yt.useAnim === true){
+            var newAnim = new YAHOO.util.Anim(yt.tipBox,
+                {
+                    opacity: { to: 0 }
+                }, yt.speed, YAHOO.util.Easing.easeOut
+            );
+            newAnim.animate();
+        } else {
+            YUD.setStyle(yt.tipBox, 'display', 'none');
+        }
+        YUD.setAttribute(el,'title', YUD.getAttribute(el, 'tt_title'));
+    }
 }
 
 /**
  * Quick filter widget
- * 
+ *
  * @param target: filter input target
  * @param nodes: list of nodes in html we want to filter.
  * @param display_element function that takes current node from nodes and
  *    does hide or show based on the node
- * 
+ *
  */
 var q_filter = function(target,nodes,display_element){
-	
-	var nodes = nodes;
-	var q_filter_field = YUD.get(target);
-	var F = YAHOO.namespace(target);
+
+    var nodes = nodes;
+    var q_filter_field = YUD.get(target);
+    var F = YAHOO.namespace(target);
+
+    YUE.on(q_filter_field,'keyup',function(e){
+        clearTimeout(F.filterTimeout);
+        F.filterTimeout = setTimeout(F.updateFilter,600);
+    });
+
+    F.filterTimeout = null;
 
-	YUE.on(q_filter_field,'keyup',function(e){
-	    clearTimeout(F.filterTimeout); 
-	    F.filterTimeout = setTimeout(F.updateFilter,600); 
-	});
+    var show_node = function(node){
+        YUD.setStyle(node,'display','')
+    }
+    var hide_node = function(node){
+        YUD.setStyle(node,'display','none');
+    }
 
-	F.filterTimeout = null;
+    F.updateFilter  = function() {
+       // Reset timeout
+       F.filterTimeout = null;
+
+       var obsolete = [];
 
-	var show_node = function(node){
-		YUD.setStyle(node,'display','')
-	}
-	var hide_node = function(node){
-		YUD.setStyle(node,'display','none');
-	}
-	
-	F.updateFilter  = function() { 
-	   // Reset timeout 
-	   F.filterTimeout = null;
-	   
-	   var obsolete = [];
-	   
-	   var req = q_filter_field.value.toLowerCase();
-	   
-	   var l = nodes.length;
-	   var i;
-	   var showing = 0;
-	   
+       var req = q_filter_field.value.toLowerCase();
+
+       var l = nodes.length;
+       var i;
+       var showing = 0;
+
        for (i=0;i<l;i++ ){
-    	   var n = nodes[i];
-    	   var target_element = display_element(n)
-    	   if(req && n.innerHTML.toLowerCase().indexOf(req) == -1){
-    		   hide_node(target_element);
-    	   }
-    	   else{
-    		   show_node(target_element);
-    		   showing+=1;
-    	   }
-       }	  	   
+           var n = nodes[i];
+           var target_element = display_element(n)
+           if(req && n.innerHTML.toLowerCase().indexOf(req) == -1){
+               hide_node(target_element);
+           }
+           else{
+               show_node(target_element);
+               showing+=1;
+           }
+       }
 
-	   // if repo_count is set update the number
-	   var cnt = YUD.get('repo_count');
-	   if(cnt){
-		   YUD.get('repo_count').innerHTML = showing;
-	   }       
-       
-	}	
+       // if repo_count is set update the number
+       var cnt = YUD.get('repo_count');
+       if(cnt){
+           YUD.get('repo_count').innerHTML = showing;
+       }
+
+    }
 };
 
 var tableTr = function(cls, body){
-	var _el = document.createElement('div');
-	var cont = new YAHOO.util.Element(body);
-	var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
-	var id = 'comment-tr-{0}'.format(comment_id);
-	var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
-	              '<td class="lineno-inline new-inline"></td>'+
-    			  '<td class="lineno-inline old-inline"></td>'+ 
+    var _el = document.createElement('div');
+    var cont = new YAHOO.util.Element(body);
+    var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
+    var id = 'comment-tr-{0}'.format(comment_id);
+    var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
+                  '<td class="lineno-inline new-inline"></td>'+
+                  '<td class="lineno-inline old-inline"></td>'+
                   '<td>{2}</td>'+
                  '</tr></tbody></table>').format(id, cls, body);
-	_el.innerHTML = _html;
-	return _el.children[0].children[0].children[0];
+    _el.innerHTML = _html;
+    return _el.children[0].children[0].children[0];
 };
 
 /** comments **/
 var removeInlineForm = function(form) {
-	form.parentNode.removeChild(form);
+    form.parentNode.removeChild(form);
 };
 
 var createInlineForm = function(parent_tr, f_path, line) {
-	var tmpl = YUD.get('comment-inline-form-template').innerHTML;
-	tmpl = tmpl.format(f_path, line);
-	var form = tableTr('comment-form-inline',tmpl)
+    var tmpl = YUD.get('comment-inline-form-template').innerHTML;
+    tmpl = tmpl.format(f_path, line);
+    var form = tableTr('comment-form-inline',tmpl)
 
-	// create event for hide button
-	form = new YAHOO.util.Element(form);
-	var form_hide_button = new YAHOO.util.Element(YUD.getElementsByClassName('hide-inline-form',null,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');
-		YUD.removeClass(parent_tr, 'hl-comment');
-		
-	});
-	
-	return form
+    // create event for hide button
+    form = new YAHOO.util.Element(form);
+    var form_hide_button = new YAHOO.util.Element(YUD.getElementsByClassName('hide-inline-form',null,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');
+        YUD.removeClass(parent_tr, 'hl-comment');
+
+    });
+
+    return form
 };
 
 /**
@@ -793,192 +793,218 @@
  * block at the very bottom
  */
 var injectInlineForm = function(tr){
-	  if(!YUD.hasClass(tr, 'line')){
-		  return
-	  }
-	  var submit_url = AJAX_COMMENT_URL;
-	  var _td = YUD.getElementsByClassName('code',null,tr)[0];
-	  if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(_td,'no-comment')){
-		  return
-	  }	
-	  YUD.addClass(tr,'form-open');
-	  YUD.addClass(tr,'hl-comment');
-	  var node = YUD.getElementsByClassName('full_f_path',null,tr.parentNode.parentNode.parentNode)[0];
-	  var f_path = YUD.getAttribute(node,'path');
-	  var lineno = getLineNo(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);
-	  var f = YUD.get(form);
-	  var overlay = YUD.getElementsByClassName('overlay',null,f)[0];
-	  var _form = YUD.getElementsByClassName('inline-form',null,f)[0];
-	  
-	  YUE.on(YUD.get(_form), '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(tr, 'line')){
+          return
+      }
+      var submit_url = AJAX_COMMENT_URL;
+      var _td = YUD.getElementsByClassName('code',null,tr)[0];
+      if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(_td,'no-comment')){
+          return
+      }
+      YUD.addClass(tr,'form-open');
+      YUD.addClass(tr,'hl-comment');
+      var node = YUD.getElementsByClassName('full_f_path',null,tr.parentNode.parentNode.parentNode)[0];
+      var f_path = YUD.getAttribute(node,'path');
+      var lineno = getLineNo(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);
+      var f = YUD.get(form);
+      var overlay = YUD.getElementsByClassName('overlay',null,f)[0];
+      var _form = YUD.getElementsByClassName('inline-form',null,f)[0];
+
+      YUE.on(YUD.get(_form), '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 (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);
-	  });
-	  
-	  setTimeout(function(){
-		  // callbacks
-		  tooltip_activate();
-		  MentionsAutoComplete('text_'+lineno, 'mentions_container_'+lineno, 
-	                         _USERS_AC_DATA, _GROUPS_AC_DATA);
-		  var _e = YUD.get('text_'+lineno);
-		  if(_e){
-			  _e.focus();
-		  }
-	  },10)
+          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);
+      });
+
+      YUE.on('preview-btn_'+lineno, 'click', function(e){
+           var _text = YUD.get('text_'+lineno).value;
+           if(!_text){
+               return
+           }
+           var post_data = {'text': _text};
+           YUD.addClass('preview-box_'+lineno, 'unloaded');
+           YUD.get('preview-box_'+lineno).innerHTML = _TM['Loading ...'];
+           YUD.setStyle('edit-container_'+lineno, 'display', 'none');
+           YUD.setStyle('preview-container_'+lineno, 'display', '');
+
+           var url = pyroutes.url('changeset_comment_preview', {'repo_name': REPO_NAME});
+           ajaxPOST(url,post_data,function(o){
+               YUD.get('preview-box_'+lineno).innerHTML = o.responseText;
+               YUD.removeClass('preview-box_'+lineno, 'unloaded');
+           })
+       })
+       YUE.on('edit-btn_'+lineno, 'click', function(e){
+           YUD.setStyle('edit-container_'+lineno, 'display', '');
+           YUD.setStyle('preview-container_'+lineno, 'display', 'none');
+       })
+
+
+      setTimeout(function(){
+          // callbacks
+          tooltip_activate();
+          MentionsAutoComplete('text_'+lineno, 'mentions_container_'+lineno,
+                             _USERS_AC_DATA, _GROUPS_AC_DATA);
+          var _e = YUD.get('text_'+lineno);
+          if(_e){
+              _e.focus();
+          }
+      },10)
 };
 
 var deleteComment = function(comment_id){
-	var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',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 = prevElementSibling(prevElementSibling(n));
         n.parentNode.removeChild(n);
 
-        // scann nodes, and attach add button to last one
-        placeAddButton(root);
+        // scann nodes, and attach add button to last one only for TR
+        // which are the inline comments
+        if(root && root.tagName == 'TR'){
+            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);
-	});
-	return add;
+    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);
+    });
+    return add;
 };
 
 var getLineNo = function(tr) {
-	var line;
-	var o = tr.children[0].id.split('_');
-	var n = tr.children[1].id.split('_');
+    var line;
+    var o = tr.children[0].id.split('_');
+    var n = tr.children[1].id.split('_');
 
-	if (n.length >= 2) {
-		line = n[n.length-1];
-	} else if (o.length >= 2) {
-		line = o[o.length-1];
-	}
+    if (n.length >= 2) {
+        line = n[n.length-1];
+    } else if (o.length >= 2) {
+        line = o[o.length-1];
+    }
 
-	return line
+    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 previous
-			  var comment_add_buttons = YUD.getElementsByClassName('add-comment',null,last_node);
-			  for(var i=0;i<comment_add_buttons.length;i++){
-				  var b = comment_add_buttons[i];
-				  b.parentNode.removeChild(b);
-			  }
-		  }
-		  else{
-			  break;
-		  }
-	  }
-	  
+    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 previous
+              var comment_add_buttons = YUD.getElementsByClassName('add-comment',null,last_node);
+              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 = YUD.getElementsByClassName('comment',null,last_node)[0];
     // attach add button
-    YUD.insertAfter(add,comment_block);	
+    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
+      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;
+      return target_line;
 }
 
 /**
@@ -986,13 +1012,13 @@
  */
 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);
+      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);
+      console.log(e);
     }
 }
 
@@ -1000,135 +1026,135 @@
  * Iterates over all the inlines, and places them inside proper blocks of data
  */
 var renderInlineComments = function(file_comments){
-	for (f in file_comments){
+    for (f in file_comments){
         // holding all comments for a FILE
-		var box = file_comments[f];
+        var box = file_comments[f];
 
-		var target_id = YUD.getAttribute(box,'target_id');
-		// actually comments with line numbers
+        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 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){
-	var current_url_branch = +"?branch=__BRANCH__";
+    var current_url_branch = +"?branch=__BRANCH__";
 
-	YUE.on('stay_at_branch','click',function(e){
-	    if(e.target.checked){
-	        var uri = current_url_branch;
-	        uri = uri.replace('__BRANCH__',e.target.value);
-	        window.location = uri;
-	    }
-	    else{
-	        window.location = current_url;
-	    }
-	})            
+    YUE.on('stay_at_branch','click',function(e){
+        if(e.target.checked){
+            var uri = current_url_branch;
+            uri = uri.replace('__BRANCH__',e.target.value);
+            window.location = uri;
+        }
+        else{
+            window.location = current_url;
+        }
+    })
 
-	var n_filter = YUD.get('node_filter');
-	var F = YAHOO.namespace('node_filter');
-	
-	F.filterTimeout = null;
-	var nodes = null;
+    var n_filter = YUD.get('node_filter');
+    var F = YAHOO.namespace('node_filter');
+
+    F.filterTimeout = null;
+    var nodes = null;
 
-	F.initFilter = function(){
-	  YUD.setStyle('node_filter_box_loading','display','');
-	  YUD.setStyle('search_activate_id','display','none');
-	  YUD.setStyle('add_node_id','display','none');
-	  YUC.initHeader('X-PARTIAL-XHR',true);
-	  YUC.asyncRequest('GET', node_list_url, {
-	      success:function(o){
-	        nodes = JSON.parse(o.responseText).nodes;
-	        YUD.setStyle('node_filter_box_loading','display','none');
-	        YUD.setStyle('node_filter_box','display','');
-	        n_filter.focus();
-			if(YUD.hasClass(n_filter,'init')){
-				n_filter.value = '';
-				YUD.removeClass(n_filter,'init');
-			}   
-	      },
-	      failure:function(o){
-	          console.log('failed to load');
-	      }
-	  },null);            
-	}
+    F.initFilter = function(){
+      YUD.setStyle('node_filter_box_loading','display','');
+      YUD.setStyle('search_activate_id','display','none');
+      YUD.setStyle('add_node_id','display','none');
+      YUC.initHeader('X-PARTIAL-XHR',true);
+      YUC.asyncRequest('GET', node_list_url, {
+          success:function(o){
+            nodes = JSON.parse(o.responseText).nodes;
+            YUD.setStyle('node_filter_box_loading','display','none');
+            YUD.setStyle('node_filter_box','display','');
+            n_filter.focus();
+            if(YUD.hasClass(n_filter,'init')){
+                n_filter.value = '';
+                YUD.removeClass(n_filter,'init');
+            }
+          },
+          failure:function(o){
+              console.log('failed to load');
+          }
+      },null);
+    }
+
+    F.updateFilter  = function(e) {
+
+        return function(){
+            // Reset timeout
+            F.filterTimeout = null;
+            var query = e.target.value.toLowerCase();
+            var match = [];
+            var matches = 0;
+            var matches_max = 20;
+            if (query != ""){
+                for(var i=0;i<nodes.length;i++){
 
-	F.updateFilter  = function(e) {
-	    
-	    return function(){
-	        // Reset timeout 
-	        F.filterTimeout = null;
-	        var query = e.target.value.toLowerCase();
-	        var match = [];
-	        var matches = 0;
-	        var matches_max = 20;
-	        if (query != ""){
-	            for(var i=0;i<nodes.length;i++){
-	            	
-	                var pos = nodes[i].name.toLowerCase().indexOf(query)
-	                if(query && pos != -1){
-	                    
-	                    matches++
-	                    //show only certain amount to not kill browser 
-	                    if (matches > matches_max){
-	                        break;
-	                    }
-	                    
-	                    var n = nodes[i].name;
-	                    var t = nodes[i].type;
-	                    var n_hl = n.substring(0,pos)
-	                      +"<b>{0}</b>".format(n.substring(pos,pos+query.length))
-	                      +n.substring(pos+query.length)
-	                    var new_url = url_base.replace('__FPATH__',n);
-	                    match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,new_url,n_hl));
-	                }
-	                if(match.length >= matches_max){
-	                    match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['Search truncated']));
-	                }
-	            }                       
-	        }
-	        if(query != ""){
-	            YUD.setStyle('tbody','display','none');
-	            YUD.setStyle('tbody_filtered','display','');
-	            
-	            if (match.length==0){
-	              match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['No matching files']));
-	            }                           
-	            
-	            YUD.get('tbody_filtered').innerHTML = match.join("");   
-	        }
-	        else{
-	            YUD.setStyle('tbody','display','');
-	            YUD.setStyle('tbody_filtered','display','none');
-	        }
-	        
-	    }
-	};
+                    var pos = nodes[i].name.toLowerCase().indexOf(query)
+                    if(query && pos != -1){
+
+                        matches++
+                        //show only certain amount to not kill browser
+                        if (matches > matches_max){
+                            break;
+                        }
+
+                        var n = nodes[i].name;
+                        var t = nodes[i].type;
+                        var n_hl = n.substring(0,pos)
+                          +"<b>{0}</b>".format(n.substring(pos,pos+query.length))
+                          +n.substring(pos+query.length)
+                        var new_url = url_base.replace('__FPATH__',n);
+                        match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,new_url,n_hl));
+                    }
+                    if(match.length >= matches_max){
+                        match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['Search truncated']));
+                    }
+                }
+            }
+            if(query != ""){
+                YUD.setStyle('tbody','display','none');
+                YUD.setStyle('tbody_filtered','display','');
 
-	YUE.on(YUD.get('filter_activate'),'click',function(){
-	    F.initFilter();
-	})
-	YUE.on(n_filter,'click',function(){
-		if(YUD.hasClass(n_filter,'init')){
-			n_filter.value = '';
-			YUD.removeClass(n_filter,'init');
-		}
-	 });
-	YUE.on(n_filter,'keyup',function(e){
-	    clearTimeout(F.filterTimeout); 
-	    F.filterTimeout = setTimeout(F.updateFilter(e),600);
-	});
+                if (match.length==0){
+                  match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['No matching files']));
+                }
+
+                YUD.get('tbody_filtered').innerHTML = match.join("");
+            }
+            else{
+                YUD.setStyle('tbody','display','');
+                YUD.setStyle('tbody_filtered','display','none');
+            }
+
+        }
+    };
+
+    YUE.on(YUD.get('filter_activate'),'click',function(){
+        F.initFilter();
+    })
+    YUE.on(n_filter,'click',function(){
+        if(YUD.hasClass(n_filter,'init')){
+            n_filter.value = '';
+            YUD.removeClass(n_filter,'init');
+        }
+     });
+    YUE.on(n_filter,'keyup',function(e){
+        clearTimeout(F.filterTimeout);
+        F.filterTimeout = setTimeout(F.updateFilter(e),600);
+    });
 };
 
 
-var initCodeMirror = function(textAreadId,resetUrl){  
+var initCodeMirror = function(textAreadId,resetUrl){
     var myCodeMirror = CodeMirror.fromTextArea(YUD.get(textAreadId),{
            mode:  "null",
            lineNumbers:true
@@ -1136,129 +1162,129 @@
     YUE.on('reset','click',function(e){
         window.location=resetUrl
     });
-    
+
     YUE.on('file_enable','click',function(){
         YUD.setStyle('editor_container','display','');
         YUD.setStyle('upload_file_container','display','none');
         YUD.setStyle('filename_container','display','');
     });
-    
+
     YUE.on('upload_file_enable','click',function(){
         YUD.setStyle('editor_container','display','none');
         YUD.setStyle('upload_file_container','display','');
         YUD.setStyle('filename_container','display','none');
-    });	
+    });
 };
 
 
 
 var getIdentNode = function(n){
-	//iterate thru nodes untill matched interesting node !
-	
-	if (typeof n == 'undefined'){
-		return -1
-	}
-	
-	if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
-			return n
-		}
-	else{
-		return getIdentNode(n.parentNode);
-	}
+    //iterate thru nodes untill matched interesting node !
+
+    if (typeof n == 'undefined'){
+        return -1
+    }
+
+    if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
+            return n
+        }
+    else{
+        return getIdentNode(n.parentNode);
+    }
 };
 
 var  getSelectionLink = function(e) {
 
-	//get selection from start/to nodes    	
-	if (typeof window.getSelection != "undefined") {
-		s = window.getSelection();
-	
-	   	from = getIdentNode(s.anchorNode);
-	   	till = getIdentNode(s.focusNode);
-	   	
-	    f_int = parseInt(from.id.replace('L',''));
-	    t_int = parseInt(till.id.replace('L',''));
-	    
-	    if (f_int > t_int){
-	    	//highlight from bottom 
-	    	offset = -35;
-	    	ranges = [t_int,f_int];
-	    	
-	    }
-	    else{
-	    	//highligth from top 
-	    	offset = 35;
-	    	ranges = [f_int,t_int];
-	    }
-	    // if we select more than 2 lines
-	    if (ranges[0] != ranges[1]){
-	        if(YUD.get('linktt') == null){
-	            hl_div = document.createElement('div');
-	            hl_div.id = 'linktt';
-	        }
-	        hl_div.innerHTML = '';
+    //get selection from start/to nodes
+    if (typeof window.getSelection != "undefined") {
+        s = window.getSelection();
+
+        from = getIdentNode(s.anchorNode);
+        till = getIdentNode(s.focusNode);
+
+        f_int = parseInt(from.id.replace('L',''));
+        t_int = parseInt(till.id.replace('L',''));
+
+        if (f_int > t_int){
+            //highlight from bottom
+            offset = -35;
+            ranges = [t_int,f_int];
 
-	        anchor = '#L'+ranges[0]+'-'+ranges[1];
-	        var link = document.createElement('a');
-	        link.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
-	        link.innerHTML = _TM['Selection link'];
-	        hl_div.appendChild(link);
-	        YUD.get('body').appendChild(hl_div);
-	        
-	        xy = YUD.getXY(till.id);
+        }
+        else{
+            //highligth from top
+            offset = 35;
+            ranges = [f_int,t_int];
+        }
+        // if we select more than 2 lines
+        if (ranges[0] != ranges[1]){
+            if(YUD.get('linktt') == null){
+                hl_div = document.createElement('div');
+                hl_div.id = 'linktt';
+            }
+            hl_div.innerHTML = '';
 
-	        YUD.addClass('linktt', 'hl-tip-box');
-	        YUD.setStyle('linktt','top',xy[1]+offset+'px');
-	        YUD.setStyle('linktt','left',xy[0]+'px');
-	        YUD.setStyle('linktt','visibility','visible');
+            anchor = '#L'+ranges[0]+'-'+ranges[1];
+            var link = document.createElement('a');
+            link.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
+            link.innerHTML = _TM['Selection link'];
+            hl_div.appendChild(link);
+            YUD.get('body').appendChild(hl_div);
+
+            xy = YUD.getXY(till.id);
 
-	    }
-	    else{
-	    	YUD.setStyle('linktt','visibility','hidden');
-	    }
-	}
+            YUD.addClass('linktt', 'hl-tip-box');
+            YUD.setStyle('linktt','top',xy[1]+offset+'px');
+            YUD.setStyle('linktt','left',xy[0]+'px');
+            YUD.setStyle('linktt','visibility','visible');
+
+        }
+        else{
+            YUD.setStyle('linktt','visibility','hidden');
+        }
+    }
 };
 
 var deleteNotification = function(url, notification_id,callbacks){
-    var callback = { 
-		success:function(o){
-		    var obj = YUD.get(String("notification_"+notification_id));
-		    if(obj.parentNode !== undefined){
-				obj.parentNode.removeChild(obj);
-			}
-			_run_callbacks(callbacks);
-		},
-	    failure:function(o){
-	        alert("error");
-	    },
-	};
+    var callback = {
+        success:function(o){
+            var obj = YUD.get(String("notification_"+notification_id));
+            if(obj.parentNode !== undefined){
+                obj.parentNode.removeChild(obj);
+            }
+            _run_callbacks(callbacks);
+        },
+        failure:function(o){
+            alert("error");
+        },
+    };
     var postData = '_method=delete';
     var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
-    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, 
-    											  callback, postData);
-};	
+    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl,
+                                                  callback, postData);
+};
 
 var readNotification = function(url, notification_id,callbacks){
-    var callback = { 
-		success:function(o){
-		    var obj = YUD.get(String("notification_"+notification_id));
-		    YUD.removeClass(obj, 'unread');
-		    var r_button = YUD.getElementsByClassName('read-notification',null,obj.children[0])[0];
-		    
-		    if(r_button.parentNode !== undefined){
-		    	r_button.parentNode.removeChild(r_button);
-			}		    
-			_run_callbacks(callbacks);
-		},
-	    failure:function(o){
-	        alert("error");
-	    },
-	};
+    var callback = {
+        success:function(o){
+            var obj = YUD.get(String("notification_"+notification_id));
+            YUD.removeClass(obj, 'unread');
+            var r_button = YUD.getElementsByClassName('read-notification',null,obj.children[0])[0];
+
+            if(r_button.parentNode !== undefined){
+                r_button.parentNode.removeChild(r_button);
+            }
+            _run_callbacks(callbacks);
+        },
+        failure:function(o){
+            alert("error");
+        },
+    };
     var postData = '_method=put';
     var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
-    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, 
-    											  callback, postData);
-};	
+    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl,
+                                                  callback, postData);
+};
 
 /** MEMBERS AUTOCOMPLETE WIDGET **/
 
@@ -1277,9 +1303,9 @@
             // Match against each name of each contact
             for (; i < l; i++) {
                 contact = myUsers[i];
-                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) || 
-                   	 ((contact.lname+"").toLowerCase().indexOf(query) > -1) || 
-                   	 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
+                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
+                     ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
+                     ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
                        matches[matches.length] = contact;
                    }
             }
@@ -1328,7 +1354,7 @@
     membersAC.useShadow = false;
     membersAC.resultTypeList = false;
     membersAC.animVert = false;
-    membersAC.animHoriz = false;    
+    membersAC.animHoriz = false;
     membersAC.animSpeed = 0.1;
 
     // Instantiate AutoComplete for owner
@@ -1341,9 +1367,9 @@
 
     // Helper highlight function for the formatter
     var highlightMatch = function (full, snippet, matchindex) {
-            return full.substring(0, matchindex) 
-            + "<span class='match'>" 
-            + full.substr(matchindex, snippet.length) 
+            return full.substring(0, matchindex)
+            + "<span class='match'>"
+            + full.substr(matchindex, snippet.length)
             + "</span>" + full.substring(matchindex + snippet.length);
         };
 
@@ -1351,11 +1377,11 @@
     var custom_formatter = function (oResultData, sQuery, sResultMatch) {
             var query = sQuery.toLowerCase();
             var _gravatar = function(res, em, group){
-            	if (group !== undefined){
-            		em = '/images/icons/group.png'
-            	}
-            	tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
-            	return tmpl.format(em,res)
+                if (group !== undefined){
+                    em = '/images/icons/group.png'
+                }
+                tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
+                return tmpl.format(em,res)
             }
             // group
             if (oResultData.grname != undefined) {
@@ -1369,13 +1395,13 @@
                 if (grnameMatchIndex > -1) {
                     return _gravatar(grprefix + highlightMatch(grname, query, grnameMatchIndex) + grsuffix,null,true);
                 }
-			    return _gravatar(grprefix + oResultData.grname + grsuffix, null,true);
+                return _gravatar(grprefix + oResultData.grname + grsuffix, null,true);
             // Users
             } else if (oResultData.nname != undefined) {
                 var fname = oResultData.fname || "";
                 var lname = oResultData.lname || "";
                 var nname = oResultData.nname;
-                
+
                 // Guard against null value
                 var fnameMatchIndex = fname.toLowerCase().indexOf(query),
                     lnameMatchIndex = lname.toLowerCase().indexOf(query),
@@ -1409,7 +1435,7 @@
     ownerAC.formatResult = custom_formatter;
 
     var myHandler = function (sType, aArgs) {
-    		var nextId = divid.split('perm_new_member_name_')[1];
+            var nextId = divid.split('perm_new_member_name_')[1];
             var myAC = aArgs[0]; // reference back to the AC instance
             var elLI = aArgs[1]; // reference to the selected LI element
             var oData = aArgs[2]; // object literal of selected item's result data
@@ -1427,7 +1453,7 @@
 
     membersAC.itemSelectEvent.subscribe(myHandler);
     if(ownerAC.itemSelectEvent){
-    	ownerAC.itemSelectEvent.subscribe(myHandler);
+        ownerAC.itemSelectEvent.subscribe(myHandler);
     }
 
     return {
@@ -1445,11 +1471,11 @@
 
     // Define a custom search function for the DataSource of users
     var matchUsers = function (sQuery) {
-    	    var org_sQuery = sQuery;
-    	    if(this.mentionQuery == null){
-    	    	return []    	    	
-    	    }
-    	    sQuery = this.mentionQuery;
+            var org_sQuery = sQuery;
+            if(this.mentionQuery == null){
+                return []
+            }
+            sQuery = this.mentionQuery;
             // Case insensitive matching
             var query = sQuery.toLowerCase();
             var i = 0;
@@ -1459,9 +1485,9 @@
             // Match against each name of each contact
             for (; i < l; i++) {
                 contact = myUsers[i];
-                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) || 
-                	 ((contact.lname+"").toLowerCase().indexOf(query) > -1) || 
-                	 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
+                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
+                     ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
+                     ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
                     matches[matches.length] = contact;
                 }
             }
@@ -1487,37 +1513,37 @@
     ownerAC.resultTypeList = false;
     ownerAC.suppressInputUpdate = true;
     ownerAC.animVert = false;
-    ownerAC.animHoriz = false;    
+    ownerAC.animHoriz = false;
     ownerAC.animSpeed = 0.1;
-    
+
     // Helper highlight function for the formatter
     var highlightMatch = function (full, snippet, matchindex) {
-            return full.substring(0, matchindex) 
-            + "<span class='match'>" 
-            + full.substr(matchindex, snippet.length) 
+            return full.substring(0, matchindex)
+            + "<span class='match'>"
+            + full.substr(matchindex, snippet.length)
             + "</span>" + full.substring(matchindex + snippet.length);
         };
 
     // Custom formatter to highlight the matching letters
     ownerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
-		    var org_sQuery = sQuery;
-		    if(this.dataSource.mentionQuery != null){
-		    	sQuery = this.dataSource.mentionQuery;		    	
-		    }
+            var org_sQuery = sQuery;
+            if(this.dataSource.mentionQuery != null){
+                sQuery = this.dataSource.mentionQuery;
+            }
 
             var query = sQuery.toLowerCase();
             var _gravatar = function(res, em, group){
-            	if (group !== undefined){
-            		em = '/images/icons/group.png'
-            	}
-            	tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
-            	return tmpl.format(em,res)
+                if (group !== undefined){
+                    em = '/images/icons/group.png'
+                }
+                tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
+                return tmpl.format(em,res)
             }
             if (oResultData.nname != undefined) {
                 var fname = oResultData.fname || "";
                 var lname = oResultData.lname || "";
                 var nname = oResultData.nname;
-                
+
                 // Guard against null value
                 var fnameMatchIndex = fname.toLowerCase().indexOf(query),
                     lnameMatchIndex = lname.toLowerCase().indexOf(query),
@@ -1549,7 +1575,7 @@
         };
 
     if(ownerAC.itemSelectEvent){
-    	ownerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
+        ownerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
 
             var myAC = aArgs[0]; // reference back to the AC instance
             var elLI = aArgs[1]; // reference to the selected LI element
@@ -1557,13 +1583,13 @@
             //fill the autocomplete with value
             if (oData.nname != undefined) {
                 //users
-            	//Replace the mention name with replaced
-            	var re = new RegExp();
-            	var org = myAC.getInputEl().value;
-            	var chunks = myAC.dataSource.chunks
-            	// replace middle chunk(the search term) with actuall  match
-            	chunks[1] = chunks[1].replace('@'+myAC.dataSource.mentionQuery,
-            								  '@'+oData.nname+' ');
+                //Replace the mention name with replaced
+                var re = new RegExp();
+                var org = myAC.getInputEl().value;
+                var chunks = myAC.dataSource.chunks
+                // replace middle chunk(the search term) with actuall  match
+                chunks[1] = chunks[1].replace('@'+myAC.dataSource.mentionQuery,
+                                              '@'+oData.nname+' ');
                 myAC.getInputEl().value = chunks.join('')
                 YUD.get(myAC.getInputEl()).focus(); // Y U NO WORK !?
             } else {
@@ -1581,48 +1607,48 @@
     ownerAC.dataSource.mentionQuery = null;
 
     ownerAC.get_mention = function(msg, max_pos) {
-    	var org = msg;
-    	var re = new RegExp('(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)$')
-    	var chunks  = [];
+        var org = msg;
+        var re = new RegExp('(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)$')
+        var chunks  = [];
+
 
-		
-    	// cut first chunk until curret pos
-		var to_max = msg.substr(0, max_pos);		
-		var at_pos = Math.max(0,to_max.lastIndexOf('@')-1);
-		var msg2 = to_max.substr(at_pos);
+        // cut first chunk until curret pos
+        var to_max = msg.substr(0, max_pos);
+        var at_pos = Math.max(0,to_max.lastIndexOf('@')-1);
+        var msg2 = to_max.substr(at_pos);
 
-		chunks.push(org.substr(0,at_pos))// prefix chunk
-		chunks.push(msg2)                // search chunk
-		chunks.push(org.substr(max_pos)) // postfix chunk
+        chunks.push(org.substr(0,at_pos))// prefix chunk
+        chunks.push(msg2)                // search chunk
+        chunks.push(org.substr(max_pos)) // postfix chunk
 
-		// clean up msg2 for filtering and regex match
-		var msg2 = msg2.lstrip(' ').lstrip('\n');
+        // clean up msg2 for filtering and regex match
+        var msg2 = msg2.lstrip(' ').lstrip('\n');
 
-		if(re.test(msg2)){
-			var unam = re.exec(msg2)[1];
-			return [unam, chunks];
-		}
-		return [null, null];
+        if(re.test(msg2)){
+            var unam = re.exec(msg2)[1];
+            return [unam, chunks];
+        }
+        return [null, null];
     };
-    
+
     if (ownerAC.textboxKeyUpEvent){
-		ownerAC.textboxKeyUpEvent.subscribe(function(type, args){
-			
-			var ac_obj = args[0];
-			var currentMessage = args[1];
-			var currentCaretPosition = args[0]._elTextbox.selectionStart;
-	
-			var unam = ownerAC.get_mention(currentMessage, currentCaretPosition); 
-			var curr_search = null;
-			if(unam[0]){
-				curr_search = unam[0];
-			}
-			
-			ownerAC.dataSource.chunks = unam[1];
-			ownerAC.dataSource.mentionQuery = curr_search;
-	
-		})
-	}	
+        ownerAC.textboxKeyUpEvent.subscribe(function(type, args){
+
+            var ac_obj = args[0];
+            var currentMessage = args[1];
+            var currentCaretPosition = args[0]._elTextbox.selectionStart;
+
+            var unam = ownerAC.get_mention(currentMessage, currentCaretPosition);
+            var curr_search = null;
+            if(unam[0]){
+                curr_search = unam[0];
+            }
+
+            ownerAC.dataSource.chunks = unam[1];
+            ownerAC.dataSource.mentionQuery = curr_search;
+
+        })
+    }
     return {
         ownerDS: ownerDS,
         ownerAC: ownerAC,
@@ -1630,54 +1656,54 @@
 }
 
 var addReviewMember = function(id,fname,lname,nname,gravatar_link){
-	var members  = YUD.get('review_members');
-	var tmpl = '<li id="reviewer_{2}">'+
+    var members  = YUD.get('review_members');
+    var tmpl = '<li id="reviewer_{2}">'+
     '<div class="reviewers_member">'+
       '<div class="gravatar"><img alt="gravatar" src="{0}"/> </div>'+
       '<div style="float:left">{1}</div>'+
       '<input type="hidden" value="{2}" name="review_members" />'+
       '<span class="delete_icon action_button" onclick="removeReviewMember({2})"></span>'+
     '</div>'+
-    '</li>'	;
+    '</li>' ;
     var displayname = "{0} {1} ({2})".format(fname,lname,nname);
-	var element = tmpl.format(gravatar_link,displayname,id);
-	// check if we don't have this ID already in
-	var ids = [];
-	var _els = YUQ('#review_members li');
-	for (el in _els){
-		ids.push(_els[el].id)
-	}
-	if(ids.indexOf('reviewer_'+id) == -1){
-		//only add if it's not there
-		members.innerHTML += element;
-	}
-	    
+    var element = tmpl.format(gravatar_link,displayname,id);
+    // check if we don't have this ID already in
+    var ids = [];
+    var _els = YUQ('#review_members li');
+    for (el in _els){
+        ids.push(_els[el].id)
+    }
+    if(ids.indexOf('reviewer_'+id) == -1){
+        //only add if it's not there
+        members.innerHTML += element;
+    }
+
 }
 
 var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
-	var el = YUD.get('reviewer_{0}'.format(reviewer_id));
-	if (el.parentNode !== undefined){
-		el.parentNode.removeChild(el);
-	}
+    var el = YUD.get('reviewer_{0}'.format(reviewer_id));
+    if (el.parentNode !== undefined){
+        el.parentNode.removeChild(el);
+    }
 }
 
 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
-	if (reviewers_ids === undefined){
-  	  var reviewers_ids = [];
-	  var ids = YUQ('#review_members input');
-	  for(var i=0; i<ids.length;i++){
-		  var id = ids[i].value
-		  reviewers_ids.push(id);
-	  }		
-	}
-	var url = pyroutes.url('pullrequest_update', {"repo_name":repo_name,
-												  "pull_request_id": pull_request_id});
-	var postData = {'_method':'put',
-			        'reviewers_ids': reviewers_ids};
-	var success = function(o){
-		window.location.reload();
-	}
-	ajaxPOST(url,postData,success);
+    if (reviewers_ids === undefined){
+      var reviewers_ids = [];
+      var ids = YUQ('#review_members input');
+      for(var i=0; i<ids.length;i++){
+          var id = ids[i].value
+          reviewers_ids.push(id);
+      }
+    }
+    var url = pyroutes.url('pullrequest_update', {"repo_name":repo_name,
+                                                  "pull_request_id": pull_request_id});
+    var postData = {'_method':'put',
+                    'reviewers_ids': reviewers_ids};
+    var success = function(o){
+        window.location.reload();
+    }
+    ajaxPOST(url,postData,success);
 }
 
 var PullRequestAutoComplete = function (divid, cont, users_list, groups_list) {
@@ -1695,9 +1721,9 @@
             // Match against each name of each contact
             for (; i < l; i++) {
                 contact = myUsers[i];
-                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) || 
-                   	 ((contact.lname+"").toLowerCase().indexOf(query) > -1) || 
-                   	 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
+                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
+                     ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
+                     ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
                        matches[matches.length] = contact;
                    }
             }
@@ -1741,37 +1767,37 @@
     reviewerAC.resultTypeList = false;
     reviewerAC.suppressInputUpdate = true;
     reviewerAC.animVert = false;
-    reviewerAC.animHoriz = false;    
+    reviewerAC.animHoriz = false;
     reviewerAC.animSpeed = 0.1;
-    
+
     // Helper highlight function for the formatter
     var highlightMatch = function (full, snippet, matchindex) {
-            return full.substring(0, matchindex) 
-            + "<span class='match'>" 
-            + full.substr(matchindex, snippet.length) 
+            return full.substring(0, matchindex)
+            + "<span class='match'>"
+            + full.substr(matchindex, snippet.length)
             + "</span>" + full.substring(matchindex + snippet.length);
         };
 
     // Custom formatter to highlight the matching letters
     reviewerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
-		    var org_sQuery = sQuery;
-		    if(this.dataSource.mentionQuery != null){
-		    	sQuery = this.dataSource.mentionQuery;		    	
-		    }
+            var org_sQuery = sQuery;
+            if(this.dataSource.mentionQuery != null){
+                sQuery = this.dataSource.mentionQuery;
+            }
 
             var query = sQuery.toLowerCase();
             var _gravatar = function(res, em, group){
-            	if (group !== undefined){
-            		em = '/images/icons/group.png'
-            	}
-            	tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
-            	return tmpl.format(em,res)
+                if (group !== undefined){
+                    em = '/images/icons/group.png'
+                }
+                tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
+                return tmpl.format(em,res)
             }
             if (oResultData.nname != undefined) {
                 var fname = oResultData.fname || "";
                 var lname = oResultData.lname || "";
                 var nname = oResultData.nname;
-                
+
                 // Guard against null value
                 var fnameMatchIndex = fname.toLowerCase().indexOf(query),
                     lnameMatchIndex = lname.toLowerCase().indexOf(query),
@@ -1801,26 +1827,26 @@
                 return '';
             }
         };
-        
+
     //members cache to catch duplicates
     reviewerAC.dataSource.cache = [];
     // hack into select event
     if(reviewerAC.itemSelectEvent){
-    	reviewerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
+        reviewerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
 
             var myAC = aArgs[0]; // reference back to the AC instance
             var elLI = aArgs[1]; // reference to the selected LI element
             var oData = aArgs[2]; // object literal of selected item's result data
-            
+
             //fill the autocomplete with value
 
             if (oData.nname != undefined) {
-            	addReviewMember(oData.id, oData.fname, oData.lname, oData.nname,
-            					oData.gravatar_lnk);
-            	myAC.dataSource.cache.push(oData.id);
-            	YUD.get('user').value = '' 
+                addReviewMember(oData.id, oData.fname, oData.lname, oData.nname,
+                                oData.gravatar_lnk);
+                myAC.dataSource.cache.push(oData.id);
+                YUD.get('user').value = ''
             }
-    	});        
+        });
     }
     return {
         ownerDS: ownerDS,
@@ -1855,13 +1881,13 @@
 
 // returns a node from given html;
 var fromHTML = function(html){
-	  var _html = document.createElement('element');
-	  _html.innerHTML = html;
-	  return _html;
+      var _html = document.createElement('element');
+      _html.innerHTML = html;
+      return _html;
 }
 var get_rev = function(node){
     var n = node.firstElementChild.firstElementChild;
-    
+
     if (n===null){
         return -1
     }
@@ -1872,56 +1898,56 @@
 }
 
 var get_name = function(node){
-	 var name = node.firstElementChild.children[2].innerHTML; 
-	 return name
+     var name = node.firstElementChild.children[2].innerHTML;
+     return name
 }
 var get_group_name = function(node){
-	var name = node.firstElementChild.children[1].innerHTML;
-	return name
+    var name = node.firstElementChild.children[1].innerHTML;
+    return name
 }
 var get_date = function(node){
-	var date_ = YUD.getAttribute(node.firstElementChild,'date');
-	return date_
+    var date_ = YUD.getAttribute(node.firstElementChild,'date');
+    return date_
 }
 
 var get_age = function(node){
-	return node
+    return node
 }
 
 var get_link = function(node){
-	return node.firstElementChild.text;
+    return node.firstElementChild.text;
 }
 
 var revisionSort = function(a, b, desc, field) {
-	  
-	  var a_ = fromHTML(a.getData(field));
-	  var b_ = fromHTML(b.getData(field));
-	  
-	  // extract revisions from string nodes 
-	  a_ = get_rev(a_)
-	  b_ = get_rev(b_)
-	      	  
-	  var comp = YAHOO.util.Sort.compare;
-	  var compState = comp(a_, b_, desc);
-	  return compState;
+
+      var a_ = fromHTML(a.getData(field));
+      var b_ = fromHTML(b.getData(field));
+
+      // extract revisions from string nodes
+      a_ = get_rev(a_)
+      b_ = get_rev(b_)
+
+      var comp = YAHOO.util.Sort.compare;
+      var compState = comp(a_, b_, desc);
+      return compState;
 };
 var ageSort = function(a, b, desc, field) {
     var a_ = fromHTML(a.getData(field));
     var b_ = fromHTML(b.getData(field));
-    
+
     // extract name from table
     a_ = get_date(a_)
-    b_ = get_date(b_)          
-    
+    b_ = get_date(b_)
+
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
 };
 
 var lastLoginSort = function(a, b, desc, field) {
-	var a_ = a.getData('last_login_raw') || 0;
+    var a_ = a.getData('last_login_raw') || 0;
     var b_ = b.getData('last_login_raw') || 0;
-    
+
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
@@ -1933,8 +1959,8 @@
 
     // extract name from table
     a_ = get_name(a_)
-    b_ = get_name(b_)          
-    
+    b_ = get_name(b_)
+
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
@@ -1946,8 +1972,8 @@
     // extract name from table
 
     a_ = a_.children[0].innerHTML;
-    b_ = b_.children[0].innerHTML;      
-    
+    b_ = b_.children[0].innerHTML;
+
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
@@ -1956,11 +1982,11 @@
 var groupNameSort = function(a, b, desc, field) {
     var a_ = fromHTML(a.getData(field));
     var b_ = fromHTML(b.getData(field));
-    
+
     // extract name from table
     a_ = get_group_name(a_)
-    b_ = get_group_name(b_)          
-    
+    b_ = get_group_name(b_)
+
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
@@ -1968,26 +1994,26 @@
 var dateSort = function(a, b, desc, field) {
     var a_ = fromHTML(a.getData(field));
     var b_ = fromHTML(b.getData(field));
-    
+
     // extract name from table
     a_ = get_date(a_)
-    b_ = get_date(b_)          
-    
+    b_ = get_date(b_)
+
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
 };
 
 var usernamelinkSort = function(a, b, desc, field) {
-	  var a_ = fromHTML(a.getData(field));
-	  var b_ = fromHTML(b.getData(field));
-	  
-	  // extract url text from string nodes 
-	  a_ = get_link(a_)
-	  b_ = get_link(b_)
-	  var comp = YAHOO.util.Sort.compare;
-	  var compState = comp(a_, b_, desc);
-	  return compState;
+      var a_ = fromHTML(a.getData(field));
+      var b_ = fromHTML(b.getData(field));
+
+      // extract url text from string nodes
+      a_ = get_link(a_)
+      b_ = get_link(b_)
+      var comp = YAHOO.util.Sort.compare;
+      var compState = comp(a_, b_, desc);
+      return compState;
 }
 
 var addPermAction = function(_html, users_list, groups_list){
@@ -1999,15 +2025,15 @@
        last_node.innerHTML = _html;
        YUD.setStyle(last_node, 'display', '');
        YUD.removeClass(last_node, 'last_new_member');
-       MembersAutoComplete("perm_new_member_name_"+next_id, 
-               "perm_container_"+next_id, users_list, groups_list);          
+       MembersAutoComplete("perm_new_member_name_"+next_id,
+               "perm_container_"+next_id, users_list, groups_list);
        //create new last NODE
        var el = document.createElement('tr');
        el.id = 'add_perm_input';
        YUD.addClass(el,'last_new_member');
        YUD.addClass(el,'new_members');
        YUD.insertAfter(el, last_node);
-    }	
+    }
 }
 
 /* Multi selectors */
@@ -2015,156 +2041,155 @@
 var MultiSelectWidget = function(selected_id, available_id, form_id){
 
 
-	//definition of containers ID's
-	var selected_container = selected_id;
-	var available_container = available_id;
-	
-	//temp container for selected storage.
-	var cache = new Array();
-	var av_cache = new Array();
-	var c =  YUD.get(selected_container);
-	var ac = YUD.get(available_container);
-	
-	//get only selected options for further fullfilment
-	for(var i = 0;node =c.options[i];i++){
-	    if(node.selected){
-	        //push selected to my temp storage left overs :)
-	        cache.push(node);
-	    }
-	}
-	
-	//get all available options to cache
-	for(var i = 0;node =ac.options[i];i++){
-	        //push selected to my temp storage left overs :)
-	        av_cache.push(node);
-	}
-	
-	//fill available only with those not in chosen
-	ac.options.length=0;
-	tmp_cache = new Array();
-	
-	for(var i = 0;node = av_cache[i];i++){
-	    var add = true;
-	    for(var i2 = 0;node_2 = cache[i2];i2++){
-	        if(node.value == node_2.value){
-	            add=false;
-	            break;
-	        }
-	    }
-	    if(add){
-	        tmp_cache.push(new Option(node.text, node.value, false, false));
-	    }
-	}
-	
-	for(var i = 0;node = tmp_cache[i];i++){
-	    ac.options[i] = node;
-	}
-	
-	function prompts_action_callback(e){
-	
-	    var chosen = YUD.get(selected_container);
-	    var available = YUD.get(available_container);
-	
-	    //get checked and unchecked options from field
-	    function get_checked(from_field){
-	        //temp container for storage.
-	        var sel_cache = new Array();
-	        var oth_cache = new Array();
-	
-	        for(var i = 0;node = from_field.options[i];i++){
-	            if(node.selected){
-	                //push selected fields :)
-	                sel_cache.push(node);
-	            }
-	            else{
-	                oth_cache.push(node)
-	            }
-	        }
-	
-	        return [sel_cache,oth_cache]
-	    }
-	
-	    //fill the field with given options
-	    function fill_with(field,options){
-	        //clear firtst
-	        field.options.length=0;
-	        for(var i = 0;node = options[i];i++){
-	                field.options[i]=new Option(node.text, node.value,
-	                        false, false);
-	        }
-	
-	    }
-	    //adds to current field
-	    function add_to(field,options){
-	        for(var i = 0;node = options[i];i++){
-	                field.appendChild(new Option(node.text, node.value,
-	                        false, false));
-	        }
-	    }
-	
-	    // add action
-	    if (this.id=='add_element'){
-	        var c = get_checked(available);
-	        add_to(chosen,c[0]);
-	        fill_with(available,c[1]);
-	    }
-	    // remove action
-	    if (this.id=='remove_element'){
-	        var c = get_checked(chosen);
-	        add_to(available,c[0]);
-	        fill_with(chosen,c[1]);
-	    }
-	    // add all elements
-	    if(this.id=='add_all_elements'){
-	        for(var i=0; node = available.options[i];i++){
-	                chosen.appendChild(new Option(node.text,
-	                        node.value, false, false));
-	        }
-	        available.options.length = 0;
-	    }
-	    //remove all elements
-	    if(this.id=='remove_all_elements'){
-	        for(var i=0; node = chosen.options[i];i++){
-	            available.appendChild(new Option(node.text,
-	                    node.value, false, false));
-	        }
-	        chosen.options.length = 0;
-	    }
-	
-	}
-	
-	YUE.addListener(['add_element','remove_element',
-	               'add_all_elements','remove_all_elements'],'click',
-	               prompts_action_callback)
-	if (form_id !== undefined) {
-		YUE.addListener(form_id,'submit',function(){
-		    var chosen = YUD.get(selected_container);
-		    for (var i = 0; i < chosen.options.length; i++) {
-		        chosen.options[i].selected = 'selected';
-		    }
-		});
-	}
+    //definition of containers ID's
+    var selected_container = selected_id;
+    var available_container = available_id;
+
+    //temp container for selected storage.
+    var cache = new Array();
+    var av_cache = new Array();
+    var c =  YUD.get(selected_container);
+    var ac = YUD.get(available_container);
+
+    //get only selected options for further fullfilment
+    for(var i = 0;node =c.options[i];i++){
+        if(node.selected){
+            //push selected to my temp storage left overs :)
+            cache.push(node);
+        }
+    }
+
+    //get all available options to cache
+    for(var i = 0;node =ac.options[i];i++){
+            //push selected to my temp storage left overs :)
+            av_cache.push(node);
+    }
+
+    //fill available only with those not in chosen
+    ac.options.length=0;
+    tmp_cache = new Array();
+
+    for(var i = 0;node = av_cache[i];i++){
+        var add = true;
+        for(var i2 = 0;node_2 = cache[i2];i2++){
+            if(node.value == node_2.value){
+                add=false;
+                break;
+            }
+        }
+        if(add){
+            tmp_cache.push(new Option(node.text, node.value, false, false));
+        }
+    }
+
+    for(var i = 0;node = tmp_cache[i];i++){
+        ac.options[i] = node;
+    }
+
+    function prompts_action_callback(e){
+
+        var chosen = YUD.get(selected_container);
+        var available = YUD.get(available_container);
+
+        //get checked and unchecked options from field
+        function get_checked(from_field){
+            //temp container for storage.
+            var sel_cache = new Array();
+            var oth_cache = new Array();
+
+            for(var i = 0;node = from_field.options[i];i++){
+                if(node.selected){
+                    //push selected fields :)
+                    sel_cache.push(node);
+                }
+                else{
+                    oth_cache.push(node)
+                }
+            }
+
+            return [sel_cache,oth_cache]
+        }
+
+        //fill the field with given options
+        function fill_with(field,options){
+            //clear firtst
+            field.options.length=0;
+            for(var i = 0;node = options[i];i++){
+                    field.options[i]=new Option(node.text, node.value,
+                            false, false);
+            }
+
+        }
+        //adds to current field
+        function add_to(field,options){
+            for(var i = 0;node = options[i];i++){
+                    field.appendChild(new Option(node.text, node.value,
+                            false, false));
+            }
+        }
+
+        // add action
+        if (this.id=='add_element'){
+            var c = get_checked(available);
+            add_to(chosen,c[0]);
+            fill_with(available,c[1]);
+        }
+        // remove action
+        if (this.id=='remove_element'){
+            var c = get_checked(chosen);
+            add_to(available,c[0]);
+            fill_with(chosen,c[1]);
+        }
+        // add all elements
+        if(this.id=='add_all_elements'){
+            for(var i=0; node = available.options[i];i++){
+                    chosen.appendChild(new Option(node.text,
+                            node.value, false, false));
+            }
+            available.options.length = 0;
+        }
+        //remove all elements
+        if(this.id=='remove_all_elements'){
+            for(var i=0; node = chosen.options[i];i++){
+                available.appendChild(new Option(node.text,
+                        node.value, false, false));
+            }
+            chosen.options.length = 0;
+        }
+
+    }
+
+    YUE.addListener(['add_element','remove_element',
+                   'add_all_elements','remove_all_elements'],'click',
+                   prompts_action_callback)
+    if (form_id !== undefined) {
+        YUE.addListener(form_id,'submit',function(){
+            var chosen = YUD.get(selected_container);
+            for (var i = 0; i < chosen.options.length; i++) {
+                chosen.options[i].selected = 'selected';
+            }
+        });
+    }
 }
 
 
 // global hooks after DOM is loaded
 
 YUE.onDOMReady(function(){
-	YUE.on(YUQ('.diff-collapse-button'), 'click', function(e){
-		var button = e.currentTarget;
-		var t = YUD.get(button).getAttribute('target');
-	    console.log(t);
-		if(YUD.hasClass(t, 'hidden')){
-			YUD.removeClass(t, 'hidden');
-			YUD.get(button).innerHTML = "&uarr; {0} &uarr;".format(_TM['Collapse diff']);
-		}
-		else if(!YUD.hasClass(t, 'hidden')){
-			YUD.addClass(t, 'hidden');
-			YUD.get(button).innerHTML = "&darr; {0} &darr;".format(_TM['Expand diff']);
-		}
-	});
-	
-	
-	
+    YUE.on(YUQ('.diff-collapse-button'), 'click', function(e){
+        var button = e.currentTarget;
+        var t = YUD.get(button).getAttribute('target');
+        console.log(t);
+        if(YUD.hasClass(t, 'hidden')){
+            YUD.removeClass(t, 'hidden');
+            YUD.get(button).innerHTML = "&uarr; {0} &uarr;".format(_TM['Collapse diff']);
+        }
+        else if(!YUD.hasClass(t, 'hidden')){
+            YUD.addClass(t, 'hidden');
+            YUD.get(button).innerHTML = "&darr; {0} &darr;".format(_TM['Expand diff']);
+        }
+    });
+
+
+
 });
-
--- a/rhodecode/templates/base/base.html	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/templates/base/base.html	Sun Apr 07 18:42:41 2013 +0200
@@ -43,8 +43,8 @@
            </p>
            <p class="footer-link-right">
                <a href="${h.url('rhodecode_official')}">RhodeCode ${c.rhodecode_version}</a>
+               ${'(%s)' % c.rhodecode_instanceid if c.rhodecode_instanceid else ''}
                &copy; 2010-${h.datetime.today().year} by Marcin Kuzminski and others
-               ${'(%s)' % c.rhodecode_instanceid if c.rhodecode_instanceid else ''}
            </p>
        </div>
    </div>
--- a/rhodecode/templates/base/root.html	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/templates/base/root.html	Sun Apr 07 18:42:41 2013 +0200
@@ -60,6 +60,10 @@
 
             var TOGGLE_FOLLOW_URL  = "${h.url('toggle_following')}";
 
+            var REPO_NAME = "";
+            %if hasattr(c, 'repo_name'):
+                var REPO_NAME = "${c.repo_name}";
+            %endif
             </script>
             <script type="text/javascript" src="${h.url('/js/yui.2.9.js', ver=c.rhodecode_version)}"></script>
             <!--[if lt IE 9]>
@@ -90,6 +94,7 @@
               pyroutes.register('toggle_following', "${h.url('toggle_following')}");
               pyroutes.register('changeset_info', "${h.url('changeset_info', repo_name='%(repo_name)s', revision='%(revision)s')}", ['repo_name', 'revision']);
               pyroutes.register('repo_size', "${h.url('repo_size', repo_name='%(repo_name)s')}", ['repo_name']);
+              pyroutes.register('changeset_comment_preview', "${h.url('changeset_comment_preview', repo_name='%(repo_name)s')}", ['repo_name']);
            })
             </script>
         </%def>
--- a/rhodecode/templates/changelog/changelog.html	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/templates/changelog/changelog.html	Sun Apr 07 18:42:41 2013 +0200
@@ -57,6 +57,18 @@
                     <tr id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
                         <td class="checkbox">
                             ${h.checkbox(cs.raw_id,class_="changeset_range")}
+                        <td class="status">
+                          %if c.statuses.get(cs.raw_id):
+                            <div class="changeset-status-ico">
+                            %if c.statuses.get(cs.raw_id)[2]:
+                              <a class="tooltip" title="${_('Click to open associated pull request #%s' % c.statuses.get(cs.raw_id)[2])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
+                                <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" />
+                              </a>
+                            %else:
+                              <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" />
+                            %endif
+                            </div>
+                          %endif
                         </td>
                         <td class="author">
                             <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),16)}"/>
--- a/rhodecode/templates/changeset/changeset_file_comment.html	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/templates/changeset/changeset_file_comment.html	Sun Apr 07 18:42:41 2013 +0200
@@ -12,7 +12,7 @@
               ${co.author.username}
           </div>
           <div class="date">
-              ${h.age(co.modified_at)} <a class="permalink" href="#comment-${co.comment_id}">&para;</a>
+              ${h.age(co.modified_at)}
           </div>
         %if co.status_change:
            <div  style="float:left" class="changeset-status-container">
@@ -22,7 +22,7 @@
            </div>
         %endif
 
-       <div style="float:left;padding:3px 0px 0px 5px">
+       <div style="float:left;padding:4px 0px 0px 5px">
         <span class="">
          %if co.pull_request:
             <a href="${h.url('pullrequest_show',repo_name=co.pull_request.other_repo.repo_name,pull_request_id=co.pull_request.pull_request_id)}">
@@ -35,11 +35,9 @@
          %endif
         </span>
        </div>
-
+      <a class="permalink" href="#comment-${co.comment_id}">&para;</a>
       %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
-        <div class="buttons">
-          <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
-        </div>
+          <div onClick="deleteComment(${co.comment_id})" class="buttons delete-comment ui-btn small">${_('Delete')}</div>
       %endif
       </div>
       <div class="text">
@@ -56,7 +54,7 @@
   %if c.rhodecode_user.username != 'default':
     <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
       ${h.form('#', class_='inline-form')}
-      <div class="clearfix">
+      <div id="edit-container_{1}" class="clearfix">
           <div class="comment-help">${_('Commenting on line {1}.')}
           ${(_('Comments parsed using %s syntax with %s support.') % (
                  ('<a href="%s">RST</a>' % h.url('rst_help')),
@@ -64,9 +62,17 @@
                )
             )|n
            }
+          <div id="preview-btn_{1}" class="preview-btn ui-btn small">${_('preview')}</div>
           </div>
             <div class="mentions-container" id="mentions_container_{1}"></div>
-            <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
+            <textarea id="text_{1}" name="text" class="comment-block-ta yui-ac-input"></textarea>
+      </div>
+      <div id="preview-container_{1}" class="clearfix" style="display:none">
+         <div class="comment-help">
+              ${_('Comment Preview')}
+            <div id="edit-btn_{1}" class="edit-btn ui-btn small">${_('edit')}</div>
+          </div>
+          <div id="preview-box_{1}" class="preview-box"></div>
       </div>
       <div class="comment-button">
       <input type="hidden" name="f_path" value="{0}">
@@ -134,7 +140,7 @@
     %if c.rhodecode_user.username != 'default':
     <div class="comment-form ac">
         ${h.form(post_url)}
-        <div class="clearfix">
+        <div id="edit-container" class="clearfix">
             <div class="comment-help">
                 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
                   '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
@@ -143,6 +149,7 @@
                 | <a id="show_changeset_link" onClick="change_status_show();"> ${_('Change status')}</a>
                   <input id="show_changeset_status_box" type="checkbox" name="change_changeset_status" style="display: none;" />
               %endif
+              <div id="preview-btn" class="preview-btn ui-btn small">${_('preview')}</div>
             </div>
             %if change_status:
             <div id="status_block_container" class="status-block" style="display:none">
@@ -155,8 +162,17 @@
             </div>
             %endif
             <div class="mentions-container" id="mentions_container"></div>
-             ${h.textarea('text')}
+             ${h.textarea('text', class_="comment-block-ta")}
         </div>
+
+        <div id="preview-container" class="clearfix" style="display:none">
+           <div class="comment-help">
+                ${_('Comment Preview')}
+              <div id="edit-btn" class="edit-btn ui-btn small">${_('edit')}</div>
+            </div>
+            <div id="preview-box" class="preview-box"></div>
+        </div>
+
         <div class="comment-button">
         ${h.submit('save', _('Comment'), class_="ui-btn large")}
         %if close_btn and change_status:
@@ -185,6 +201,27 @@
            YUD.addClass('save_close', 'hidden');
        }
    })
+   YUE.on('preview-btn', 'click', function(e){
+       var _text = YUD.get('text').value;
+       if(!_text){
+           return
+       }
+       var post_data = {'text': _text};
+       YUD.addClass('preview-box', 'unloaded');
+       YUD.get('preview-box').innerHTML = _TM['Loading ...'];
+       YUD.setStyle('edit-container', 'display', 'none');
+       YUD.setStyle('preview-container', 'display', '');
+
+       var url = pyroutes.url('changeset_comment_preview', {'repo_name': '${c.repo_name}'});
+       ajaxPOST(url,post_data,function(o){
+           YUD.get('preview-box').innerHTML = o.responseText;
+           YUD.removeClass('preview-box', 'unloaded');
+       })
+   })
+   YUE.on('edit-btn', 'click', function(e){
+       YUD.setStyle('edit-container', 'display', '');
+       YUD.setStyle('preview-container', 'display', 'none');
+   })
 
 });
 </script>
--- a/rhodecode/tests/functional/test_summary.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/tests/functional/test_summary.py	Sun Apr 07 18:42:41 2013 +0200
@@ -1,9 +1,9 @@
 from rhodecode.tests import *
 from rhodecode.tests.fixture import Fixture
 from rhodecode.model.db import Repository
-from rhodecode.lib.utils import invalidate_cache
 from rhodecode.model.repo import RepoModel
 from rhodecode.model.meta import Session
+from rhodecode.model.scm import ScmModel
 
 fixture = Fixture()
 
@@ -32,7 +32,7 @@
         #codes stats
         self._enable_stats()
 
-        invalidate_cache('get_repo_cached_%s' % HG_REPO)
+        ScmModel().mark_for_invalidation(HG_REPO)
         response = self.app.get(url(controller='summary', action='index',
                                     repo_name=HG_REPO))
         response.mustcontain(
--- a/rhodecode/tests/vcs/test_repository.py	Sun Apr 07 16:44:46 2013 +0200
+++ b/rhodecode/tests/vcs/test_repository.py	Sun Apr 07 18:42:41 2013 +0200
@@ -31,6 +31,19 @@
         self.assertEqual(self.repo.get_user_email(TEST_USER_CONFIG_FILE),
             'foo.bar@example.com')
 
+    def test_repo_equality(self):
+        self.assertTrue(self.repo == self.repo)
+
+    def test_repo_equality_broken_object(self):
+        import copy
+        _repo = copy.copy(self.repo)
+        delattr(_repo, 'path')
+        self.assertTrue(self.repo != _repo)
+
+    def test_repo_equality_other_object(self):
+        class dummy(object):
+            path = self.repo.path
+        self.assertTrue(self.repo != dummy())
 
 
 class RepositoryGetDiffTest(BackendTestMixin):