changeset 2476:19d94d752952 beta

merge branch codereview into beta
author Marcin Kuzminski <marcin@python-works.com>
date Mon, 18 Jun 2012 00:35:13 +0200
parents 930db0673614 (current diff) 6474e138737e (diff)
children 8eab81115660
files
diffstat 71 files changed, 4984 insertions(+), 1131 deletions(-) [+]
line wrap: on
line diff
--- a/requires.txt	Mon Jun 18 00:33:19 2012 +0200
+++ b/requires.txt	Mon Jun 18 00:35:13 2012 +0200
@@ -2,7 +2,7 @@
 Beaker==1.6.3
 WebHelpers==1.3
 formencode==1.2.4
-SQLAlchemy==0.7.6
+SQLAlchemy==0.7.8
 Mako==0.7.0
 pygments>=1.4
 whoosh>=2.4.0,<2.5
--- a/rhodecode/__init__.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/__init__.py	Mon Jun 18 00:35:13 2012 +0200
@@ -38,7 +38,7 @@
 
 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
                '.'.join(VERSION[3:]))
-__dbversion__ = 5  # defines current db version for migrations
+__dbversion__ = 6  # defines current db version for migrations
 __platform__ = platform.system()
 __license__ = 'GPLv3'
 __py_version__ = sys.version_info
@@ -54,7 +54,7 @@
     "Beaker==1.6.3",
     "WebHelpers==1.3",
     "formencode==1.2.4",
-    "SQLAlchemy==0.7.6",
+    "SQLAlchemy==0.7.8",
     "Mako==0.7.0",
     "pygments>=1.4",
     "whoosh>=2.4.0,<2.5",
--- a/rhodecode/config/routing.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/config/routing.py	Mon Jun 18 00:35:13 2012 +0200
@@ -212,6 +212,10 @@
         #EXTRAS USER ROUTES
         m.connect("user_perm", "/users_perm/{id}",
                   action="update_perm", conditions=dict(method=["PUT"]))
+        m.connect("user_emails", "/users_emails/{id}",
+                  action="add_email", conditions=dict(method=["PUT"]))
+        m.connect("user_emails_delete", "/users_emails/{id}",
+                  action="delete_email", conditions=dict(method=["DELETE"]))
 
     #ADMIN USERS GROUPS REST ROUTES
     with rmap.submapper(path_prefix=ADMIN_PREFIX,
@@ -424,6 +428,41 @@
                  controller='changeset', action='raw_changeset',
                  revision='tip', conditions=dict(function=check_repo))
 
+    rmap.connect('compare_url',
+                 '/{repo_name:.*}/compare/{org_ref_type}@{org_ref}...{other_ref_type}@{other_ref}',
+                 controller='compare', action='index',
+                 conditions=dict(function=check_repo),
+                 requirements=dict(org_ref_type='(branch|book|tag)',
+                                   other_ref_type='(branch|book|tag)'))
+
+    rmap.connect('pullrequest_home',
+                 '/{repo_name:.*}/pull-request/new', controller='pullrequests',
+                 action='index', conditions=dict(function=check_repo,
+                                                 method=["GET"]))
+
+    rmap.connect('pullrequest',
+                 '/{repo_name:.*}/pull-request/new', controller='pullrequests',
+                 action='create', conditions=dict(function=check_repo,
+                                                  method=["POST"]))
+
+    rmap.connect('pullrequest_show',
+                 '/{repo_name:.*}/pull-request/{pull_request_id}',
+                 controller='pullrequests',
+                 action='show', conditions=dict(function=check_repo,
+                                                method=["GET"]))
+
+    rmap.connect('pullrequest_show_all',
+                 '/{repo_name:.*}/pull-request',
+                 controller='pullrequests',
+                 action='show_all', conditions=dict(function=check_repo,
+                                                method=["GET"]))
+
+    rmap.connect('pullrequest_comment',
+                 '/{repo_name:.*}/pull-request-comment/{pull_request_id}',
+                 controller='pullrequests',
+                 action='comment', conditions=dict(function=check_repo,
+                                                method=["POST"]))
+
     rmap.connect('summary_home', '/{repo_name:.*}/summary',
                 controller='summary', conditions=dict(function=check_repo))
 
--- a/rhodecode/controllers/admin/notifications.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/admin/notifications.py	Mon Jun 18 00:35:13 2012 +0200
@@ -60,19 +60,23 @@
         """GET /_admin/notifications: All items in the collection"""
         # url('notifications')
         c.user = self.rhodecode_user
-        notif = NotificationModel().get_for_user(self.rhodecode_user.user_id)
+        notif = NotificationModel().get_for_user(self.rhodecode_user.user_id,
+                                                 filter_=request.GET)
         p = int(request.params.get('page', 1))
         c.notifications = Page(notif, page=p, items_per_page=10)
+        c.pull_request_type = Notification.TYPE_PULL_REQUEST
         return render('admin/notifications/notifications.html')
 
     def mark_all_read(self):
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
             nm = NotificationModel()
             # mark all read
-            nm.mark_all_read_for_user(self.rhodecode_user.user_id)
+            nm.mark_all_read_for_user(self.rhodecode_user.user_id,
+                                      filter_=request.GET)
             Session.commit()
             c.user = self.rhodecode_user
-            notif = nm.get_for_user(self.rhodecode_user.user_id)
+            notif = nm.get_for_user(self.rhodecode_user.user_id,
+                                    filter_=request.GET)
             c.notifications = Page(notif, page=1, items_per_page=10)
             return render('admin/notifications/notifications_data.html')
 
--- a/rhodecode/controllers/admin/repos.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/admin/repos.py	Mon Jun 18 00:35:13 2012 +0200
@@ -28,7 +28,7 @@
 import formencode
 from formencode import htmlfill
 
-from paste.httpexceptions import HTTPInternalServerError
+from webob.exc import HTTPInternalServerError
 from pylons import request, session, tmpl_context as c, url
 from pylons.controllers.util import redirect
 from pylons.i18n.translation import _
--- a/rhodecode/controllers/admin/users.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/admin/users.py	Mon Jun 18 00:35:13 2012 +0200
@@ -39,7 +39,7 @@
     AuthUser
 from rhodecode.lib.base import BaseController, render
 
-from rhodecode.model.db import User, Permission
+from rhodecode.model.db import User, Permission, UserEmailMap
 from rhodecode.model.forms import UserForm
 from rhodecode.model.user import UserModel
 from rhodecode.model.meta import Session
@@ -179,7 +179,8 @@
         c.user.permissions = {}
         c.granted_permissions = UserModel().fill_perms(c.user)\
             .permissions['global']
-
+        c.user_email_map = UserEmailMap.query()\
+                        .filter(UserEmailMap.user == c.user).all()
         defaults = c.user.get_dict()
         perm = Permission.get_by_key('hg.create.repository')
         defaults.update({'create_repo_perm': UserModel().has_perm(id, perm)})
@@ -217,3 +218,30 @@
                     category='success')
             Session.commit()
         return redirect(url('edit_user', id=id))
+
+    def add_email(self, id):
+        """POST /user_emails:Add an existing item"""
+        # url('user_emails', id=ID, method='put')
+
+        #TODO: validation and form !!!
+        email = request.POST.get('new_email')
+        user_model = UserModel()
+
+        try:
+            user_model.add_extra_email(id, email)
+            Session.commit()
+            h.flash(_("Added email %s to user" % email), category='success')
+        except Exception:
+            log.error(traceback.format_exc())
+            h.flash(_('An error occurred during email saving'),
+                    category='error')
+        return redirect(url('edit_user', id=id))
+
+    def delete_email(self, id):
+        """DELETE /user_emails_delete/id: Delete an existing item"""
+        # url('user_emails_delete', id=ID, method='delete')
+        user_model = UserModel()
+        user_model.delete_extra_email(id, request.POST.get('del_email'))
+        Session.commit()
+        h.flash(_("Removed email from user"), category='success')
+        return redirect(url('edit_user', id=id))
--- a/rhodecode/controllers/branches.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/branches.py	Mon Jun 18 00:35:13 2012 +0200
@@ -24,14 +24,15 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import logging
+import binascii
 
 from pylons import tmpl_context as c
-import binascii
 
 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.compat import OrderedDict
 from rhodecode.lib.utils2 import safe_unicode
+
 log = logging.getLogger(__name__)
 
 
--- a/rhodecode/controllers/changelog.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/changelog.py	Mon Jun 18 00:35:13 2012 +0200
@@ -82,7 +82,7 @@
             collection = list(c.pagination)
             page_revisions = [x.raw_id for x in collection]
             c.comments = c.rhodecode_db_repo.comments(page_revisions)
-
+            c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
         except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
             log.error(traceback.format_exc())
             h.flash(str(e), category='warning')
--- a/rhodecode/controllers/changeset.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/changeset.py	Mon Jun 18 00:35:13 2012 +0200
@@ -43,8 +43,9 @@
 from rhodecode.lib.utils import EmptyChangeset, action_logger
 from rhodecode.lib.compat import OrderedDict
 from rhodecode.lib import diffs
-from rhodecode.model.db import ChangesetComment
+from rhodecode.model.db import ChangesetComment, ChangesetStatus
 from rhodecode.model.comment import ChangesetCommentsModel
+from rhodecode.model.changeset_status import ChangesetStatusModel
 from rhodecode.model.meta import Session
 from rhodecode.lib.diffs import wrapped_diff
 from rhodecode.model.repo import RepoModel
@@ -205,18 +206,24 @@
 
         cumulative_diff = 0
         c.cut_off = False  # defines if cut off limit is reached
-
+        c.changeset_statuses = ChangesetStatus.STATUSES
         c.comments = []
+        c.statuses = []
         c.inline_comments = []
         c.inline_cnt = 0
         # Iterate over ranges (default changeset view is always one changeset)
         for changeset in c.cs_ranges:
+
+            c.statuses.extend([ChangesetStatusModel()\
+                              .get_status(c.rhodecode_db_repo.repo_id,
+                                          changeset.raw_id)])
+
             c.comments.extend(ChangesetCommentsModel()\
                               .get_comments(c.rhodecode_db_repo.repo_id,
-                                            changeset.raw_id))
+                                            revision=changeset.raw_id))
             inlines = ChangesetCommentsModel()\
                         .get_inline_comments(c.rhodecode_db_repo.repo_id,
-                                             changeset.raw_id)
+                                             revision=changeset.raw_id)
             c.inline_comments.extend(inlines)
             c.changes[changeset.raw_id] = []
             try:
@@ -288,7 +295,7 @@
                 )
 
         # count inline comments
-        for path, lines in c.inline_comments:
+        for __, lines in c.inline_comments:
             for comments in lines.values():
                 c.inline_cnt += len(comments)
 
@@ -365,14 +372,29 @@
 
     @jsonify
     def comment(self, repo_name, revision):
+        status = request.POST.get('changeset_status')
+        change_status = request.POST.get('change_changeset_status')
+
         comm = ChangesetCommentsModel().create(
             text=request.POST.get('text'),
             repo_id=c.rhodecode_db_repo.repo_id,
             user_id=c.rhodecode_user.user_id,
             revision=revision,
             f_path=request.POST.get('f_path'),
-            line_no=request.POST.get('line')
+            line_no=request.POST.get('line'),
+            status_change=(ChangesetStatus.get_status_lbl(status) 
+                           if status and change_status else None)
         )
+
+        # get status if set !
+        if status and change_status:
+            ChangesetStatusModel().set_status(
+                c.rhodecode_db_repo.repo_id,
+                status,
+                c.rhodecode_user.user_id,
+                comm,
+                revision=revision,
+            )
         action_logger(self.rhodecode_user,
                       'user_commented_revision:%s' % revision,
                       c.rhodecode_db_repo, self.ip_addr, self.sa)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/controllers/compare.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.controllers.compare
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    compare controller for pylons showoing differences between two
+    repos, branches, bookmarks or tips
+
+    :created_on: May 6, 2012
+    :author: marcink
+    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import logging
+import traceback
+
+from webob.exc import HTTPNotFound
+from pylons import request, response, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect
+
+from rhodecode.lib import helpers as h
+from rhodecode.lib.base import BaseRepoController, render
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
+from rhodecode.lib import diffs
+
+from rhodecode.model.db import Repository
+from rhodecode.model.pull_request import PullRequestModel
+
+log = logging.getLogger(__name__)
+
+
+class CompareController(BaseRepoController):
+
+    @LoginRequired()
+    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
+                                   'repository.admin')
+    def __before__(self):
+        super(CompareController, self).__before__()
+
+    def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
+
+        org_repo = c.rhodecode_db_repo.repo_name
+        org_ref = (org_ref_type, org_ref)
+        other_ref = (other_ref_type, other_ref)
+        other_repo = request.GET.get('repo', org_repo)
+
+        c.swap_url = h.url('compare_url', repo_name=other_repo,
+              org_ref_type=other_ref[0], org_ref=other_ref[1],
+              other_ref_type=org_ref[0], other_ref=org_ref[1],
+              repo=org_repo)
+
+        c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
+        c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
+
+        if c.org_repo is None or c.other_repo is None:
+            log.error('Could not found repo %s or %s' % (org_repo, other_repo))
+            raise HTTPNotFound
+
+        if c.org_repo.scm_instance.alias != 'hg':
+            log.error('Review not available for GIT REPOS')
+            raise HTTPNotFound
+
+        c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
+                                       org_repo, org_ref, other_repo, other_ref
+                                      )
+
+        c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
+                                                   c.cs_ranges])
+        # defines that we need hidden inputs with changesets
+        c.as_form = request.GET.get('as_form', False)
+        if request.environ.get('HTTP_X_PARTIAL_XHR'):
+            return render('compare/compare_cs.html')
+
+        c.org_ref = org_ref[1]
+        c.other_ref = other_ref[1]
+        # diff needs to have swapped org with other to generate proper diff
+        _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
+                             discovery_data)
+        diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
+        _parsed = diff_processor.prepare()
+
+        c.files = []
+        c.changes = {}
+
+        for f in _parsed:
+            fid = h.FID('', f['filename'])
+            c.files.append([fid, f['operation'], f['filename'], f['stats']])
+            diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
+            c.changes[fid] = [f['operation'], f['filename'], diff]
+
+        return render('compare/compare_diff.html')
--- a/rhodecode/controllers/home.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/home.py	Mon Jun 18 00:35:13 2012 +0200
@@ -26,7 +26,7 @@
 import logging
 
 from pylons import tmpl_context as c, request
-from paste.httpexceptions import HTTPBadRequest
+from webob.exc import HTTPBadRequest
 
 from rhodecode.lib.auth import LoginRequired
 from rhodecode.lib.base import BaseController, render
--- a/rhodecode/controllers/journal.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/controllers/journal.py	Mon Jun 18 00:35:13 2012 +0200
@@ -30,7 +30,7 @@
 from webhelpers.paginate import Page
 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
 
-from paste.httpexceptions import HTTPBadRequest
+from webob.exc import HTTPBadRequest
 from pylons import request, tmpl_context as c, response, url
 from pylons.i18n.translation import _
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/controllers/pullrequests.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.controllers.pullrequests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    pull requests controller for rhodecode for initializing pull requests
+
+    :created_on: May 7, 2012
+    :author: marcink
+    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import logging
+import traceback
+
+from webob.exc import HTTPNotFound
+
+from pylons import request, response, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect
+from pylons.i18n.translation import _
+from pylons.decorators import jsonify
+
+from rhodecode.lib.base import BaseRepoController, render
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
+from rhodecode.lib import helpers as h
+from rhodecode.lib import diffs
+from rhodecode.lib.utils import action_logger
+from rhodecode.model.db import User, PullRequest, ChangesetStatus
+from rhodecode.model.pull_request import PullRequestModel
+from rhodecode.model.meta import Session
+from rhodecode.model.repo import RepoModel
+from rhodecode.model.comment import ChangesetCommentsModel
+from rhodecode.model.changeset_status import ChangesetStatusModel
+
+log = logging.getLogger(__name__)
+
+
+class PullrequestsController(BaseRepoController):
+
+    @LoginRequired()
+    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
+                                   'repository.admin')
+    def __before__(self):
+        super(PullrequestsController, self).__before__()
+
+    def _get_repo_refs(self, repo):
+        hist_l = []
+
+        branches_group = ([('branch:%s:%s' % (k, v), k) for
+                         k, v in repo.branches.iteritems()], _("Branches"))
+        bookmarks_group = ([('book:%s:%s' % (k, v), k) for
+                         k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
+        tags_group = ([('tag:%s:%s' % (k, v), k) for 
+                         k, v in repo.tags.iteritems()], _("Tags"))
+
+        hist_l.append(bookmarks_group)
+        hist_l.append(branches_group)
+        hist_l.append(tags_group)
+
+        return hist_l
+
+    def show_all(self, repo_name):
+        c.pull_requests = PullRequestModel().get_all(repo_name)
+        c.repo_name = repo_name
+        return render('/pullrequests/pullrequest_show_all.html')
+
+    def index(self):
+        org_repo = c.rhodecode_db_repo
+
+        if org_repo.scm_instance.alias != 'hg':
+            log.error('Review not available for GIT REPOS')
+            raise HTTPNotFound
+
+        c.org_refs = self._get_repo_refs(c.rhodecode_repo)
+        c.org_repos = []
+        c.other_repos = []
+        c.org_repos.append((org_repo.repo_name, '%s/%s' % (
+                                org_repo.user.username, c.repo_name))
+                           )
+
+        c.other_refs = c.org_refs
+        c.other_repos.extend(c.org_repos)
+        c.default_pull_request = org_repo.repo_name
+        #gather forks and add to this list
+        for fork in org_repo.forks:
+            c.other_repos.append((fork.repo_name, '%s/%s' % (
+                                    fork.user.username, fork.repo_name))
+                                 )
+        #add parents of this fork also
+        if org_repo.parent:
+            c.default_pull_request = org_repo.parent.repo_name
+            c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
+                                        org_repo.parent.user.username,
+                                        org_repo.parent.repo_name))
+                                     )
+
+        c.review_members = []
+        c.available_members = []
+        for u in User.query().filter(User.username != 'default').all():
+            uname = u.username
+            if org_repo.user == u:
+                uname = _('%s (owner)' % u.username)
+                # auto add owner to pull-request recipients
+                c.review_members.append([u.user_id, uname])
+            c.available_members.append([u.user_id, uname])
+        return render('/pullrequests/pullrequest.html')
+
+    def create(self, repo_name):
+        req_p = request.POST
+        org_repo = req_p['org_repo']
+        org_ref = req_p['org_ref']
+        other_repo = req_p['other_repo']
+        other_ref = req_p['other_ref']
+        revisions = req_p.getall('revisions')
+        reviewers = req_p.getall('review_members')
+        #TODO: wrap this into a FORM !!!
+
+        title = req_p['pullrequest_title']
+        description = req_p['pullrequest_desc']
+
+        try:
+            model = PullRequestModel()
+            model.create(self.rhodecode_user.user_id, org_repo,
+                         org_ref, other_repo, other_ref, revisions,
+                         reviewers, title, description)
+            Session.commit()
+            h.flash(_('Pull request send'), category='success')
+        except Exception:
+            raise
+            h.flash(_('Error occured during sending pull request'),
+                    category='error')
+            log.error(traceback.format_exc())
+
+        return redirect(url('changelog_home', repo_name=repo_name))
+
+    def _load_compare_data(self, pull_request):
+        """
+        Load context data needed for generating compare diff
+
+        :param pull_request:
+        :type pull_request:
+        """
+
+        org_repo = pull_request.org_repo
+        org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
+        other_repo = pull_request.other_repo
+        other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
+
+        org_ref = (org_ref_type, org_ref)
+        other_ref = (other_ref_type, other_ref)
+
+        c.org_repo = org_repo
+        c.other_repo = other_repo
+
+        c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
+                                       org_repo, org_ref, other_repo, other_ref
+                                      )
+
+        c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
+                                                   c.cs_ranges])
+        # defines that we need hidden inputs with changesets
+        c.as_form = request.GET.get('as_form', False)
+
+        c.org_ref = org_ref[1]
+        c.other_ref = other_ref[1]
+        # diff needs to have swapped org with other to generate proper diff
+        _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
+                             discovery_data)
+        diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
+        _parsed = diff_processor.prepare()
+
+        c.files = []
+        c.changes = {}
+
+        for f in _parsed:
+            fid = h.FID('', f['filename'])
+            c.files.append([fid, f['operation'], f['filename'], f['stats']])
+            diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
+            c.changes[fid] = [f['operation'], f['filename'], diff]
+
+    def show(self, repo_name, pull_request_id):
+        repo_model = RepoModel()
+        c.users_array = repo_model.get_users_js()
+        c.users_groups_array = repo_model.get_users_groups_js()
+        c.pull_request = PullRequest.get(pull_request_id)
+
+        # valid ID
+        if not c.pull_request:
+            raise HTTPNotFound
+
+        # pull_requests repo_name we opened it against
+        # ie. other_repo must match
+        if repo_name != c.pull_request.other_repo.repo_name:
+            raise HTTPNotFound
+
+        # load compare data into template context
+        self._load_compare_data(c.pull_request)
+
+        # inline comments
+        c.inline_cnt = 0
+        c.inline_comments = ChangesetCommentsModel()\
+                            .get_inline_comments(c.rhodecode_db_repo.repo_id,
+                                                 pull_request=pull_request_id)
+        # count inline comments
+        for __, lines in c.inline_comments:
+            for comments in lines.values():
+                c.inline_cnt += len(comments)
+        # comments
+        c.comments = ChangesetCommentsModel()\
+                          .get_comments(c.rhodecode_db_repo.repo_id,
+                                        pull_request=pull_request_id)
+
+        # changeset(pull-request) status
+        c.current_changeset_status = ChangesetStatusModel()\
+                              .get_status(c.pull_request.org_repo,
+                                          pull_request=c.pull_request)
+        c.changeset_statuses = ChangesetStatus.STATUSES
+        return render('/pullrequests/pullrequest_show.html')
+
+    @jsonify
+    def comment(self, repo_name, pull_request_id):
+
+        status = request.POST.get('changeset_status')
+        change_status = request.POST.get('change_changeset_status')
+
+        comm = ChangesetCommentsModel().create(
+            text=request.POST.get('text'),
+            repo_id=c.rhodecode_db_repo.repo_id,
+            user_id=c.rhodecode_user.user_id,
+            pull_request=pull_request_id,
+            f_path=request.POST.get('f_path'),
+            line_no=request.POST.get('line'),
+            status_change=(ChangesetStatus.get_status_lbl(status) 
+                           if status and change_status else None)
+        )
+
+        # get status if set !
+        if status and change_status:
+            ChangesetStatusModel().set_status(
+                c.rhodecode_db_repo.repo_id,
+                status,
+                c.rhodecode_user.user_id,
+                comm,
+                pull_request=pull_request_id
+            )
+        action_logger(self.rhodecode_user,
+                      'user_commented_pull_request:%s' % pull_request_id,
+                      c.rhodecode_db_repo, self.ip_addr, self.sa)
+
+        Session.commit()
+
+        if not request.environ.get('HTTP_X_PARTIAL_XHR'):
+            return redirect(h.url('pullrequest_show', repo_name=repo_name,
+                                  pull_request_id=pull_request_id))
+
+        data = {
+           'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
+        }
+        if comm:
+            c.co = comm
+            data.update(comm.get_dict())
+            data.update({'rendered_text':
+                         render('changeset/changeset_comment_block.html')})
+
+        return data
--- a/rhodecode/lib/base.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/base.py	Mon Jun 18 00:35:13 2012 +0200
@@ -204,7 +204,7 @@
         super(BaseRepoController, self).__before__()
         if c.repo_name:
 
-            c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
+            dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
             c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
 
             if c.rhodecode_repo is None:
@@ -213,5 +213,7 @@
 
                 redirect(url('home'))
 
-            c.repository_followers = self.scm_model.get_followers(c.repo_name)
-            c.repository_forks = self.scm_model.get_forks(c.repo_name)
+            # some globals counter for menu
+            c.repository_followers = self.scm_model.get_followers(dbr)
+            c.repository_forks = self.scm_model.get_forks(dbr)
+            c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
\ No newline at end of file
--- a/rhodecode/lib/db_manage.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/db_manage.py	Mon Jun 18 00:35:13 2012 +0200
@@ -178,6 +178,8 @@
             def step_5(self):
                 pass
 
+            def step_6(self):
+                pass
         upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
 
         # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
--- a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py	Mon Jun 18 00:35:13 2012 +0200
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-    rhodecode.model.db
-    ~~~~~~~~~~~~~~~~~~
+    rhodecode.model.db_1_2_0
+    ~~~~~~~~~~~~~~~~~~~~~~~~
 
-    Database Models for RhodeCode
+    Database Models for RhodeCode <=1.2.X
 
     :created_on: Apr 08, 2010
     :author: marcink
--- a/rhodecode/lib/dbmigrate/schema/db_1_3_0.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py	Mon Jun 18 00:35:13 2012 +0200
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-    rhodecode.model.db
-    ~~~~~~~~~~~~~~~~~~
+    rhodecode.model.db_1_3_0
+    ~~~~~~~~~~~~~~~~~~~~~~~~
 
-    Database Models for RhodeCode
+    Database Models for RhodeCode <=1.3.X
 
     :created_on: Apr 08, 2010
     :author: marcink
@@ -23,6 +23,1270 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-#TODO: when branch 1.3 is finished replacem with db.py content
+import os
+import logging
+import datetime
+import traceback
+from collections import defaultdict
+
+from sqlalchemy import *
+from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
+from beaker.cache import cache_region, region_invalidate
+
+from rhodecode.lib.vcs import get_backend
+from rhodecode.lib.vcs.utils.helpers import get_scm
+from rhodecode.lib.vcs.exceptions import VCSError
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
+
+from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
+    safe_unicode
+from rhodecode.lib.compat import json
+from rhodecode.lib.caching_query import FromCache
+
+from rhodecode.model.meta import Base, Session
+import hashlib
+
+
+log = logging.getLogger(__name__)
+
+#==============================================================================
+# BASE CLASSES
+#==============================================================================
+
+_hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
+
+
+class ModelSerializer(json.JSONEncoder):
+    """
+    Simple Serializer for JSON,
+
+    usage::
+
+        to make object customized for serialization implement a __json__
+        method that will return a dict for serialization into json
+
+    example::
+
+        class Task(object):
+
+            def __init__(self, name, value):
+                self.name = name
+                self.value = value
+
+            def __json__(self):
+                return dict(name=self.name,
+                            value=self.value)
+
+    """
+
+    def default(self, obj):
+
+        if hasattr(obj, '__json__'):
+            return obj.__json__()
+        else:
+            return json.JSONEncoder.default(self, obj)
+
+
+class BaseModel(object):
+    """
+    Base Model for all classess
+    """
+
+    @classmethod
+    def _get_keys(cls):
+        """return column names for this model """
+        return class_mapper(cls).c.keys()
+
+    def get_dict(self):
+        """
+        return dict with keys and values corresponding
+        to this model data """
+
+        d = {}
+        for k in self._get_keys():
+            d[k] = getattr(self, k)
+
+        # also use __json__() if present to get additional fields
+        for k, val in getattr(self, '__json__', lambda: {})().iteritems():
+            d[k] = val
+        return d
+
+    def get_appstruct(self):
+        """return list with keys and values tupples corresponding
+        to this model data """
+
+        l = []
+        for k in self._get_keys():
+            l.append((k, getattr(self, k),))
+        return l
+
+    def populate_obj(self, populate_dict):
+        """populate model with data from given populate_dict"""
+
+        for k in self._get_keys():
+            if k in populate_dict:
+                setattr(self, k, populate_dict[k])
+
+    @classmethod
+    def query(cls):
+        return Session.query(cls)
+
+    @classmethod
+    def get(cls, id_):
+        if id_:
+            return cls.query().get(id_)
+
+    @classmethod
+    def getAll(cls):
+        return cls.query().all()
+
+    @classmethod
+    def delete(cls, id_):
+        obj = cls.query().get(id_)
+        Session.delete(obj)
+
+    def __repr__(self):
+        if hasattr(self, '__unicode__'):
+            # python repr needs to return str
+            return safe_str(self.__unicode__())
+        return '<DB:%s>' % (self.__class__.__name__)
+
+class RhodeCodeSetting(Base, BaseModel):
+    __tablename__ = 'rhodecode_settings'
+    __table_args__ = (
+        UniqueConstraint('app_settings_name'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+
+    def __init__(self, k='', v=''):
+        self.app_settings_name = k
+        self.app_settings_value = v
+
+    @validates('_app_settings_value')
+    def validate_settings_value(self, key, val):
+        assert type(val) == unicode
+        return val
+
+    @hybrid_property
+    def app_settings_value(self):
+        v = self._app_settings_value
+        if self.app_settings_name == 'ldap_active':
+            v = str2bool(v)
+        return v
+
+    @app_settings_value.setter
+    def app_settings_value(self, val):
+        """
+        Setter that will always make sure we use unicode in app_settings_value
+
+        :param val:
+        """
+        self._app_settings_value = safe_unicode(val)
+
+    def __unicode__(self):
+        return u"<%s('%s:%s')>" % (
+            self.__class__.__name__,
+            self.app_settings_name, self.app_settings_value
+        )
+
+    @classmethod
+    def get_by_name(cls, ldap_key):
+        return cls.query()\
+            .filter(cls.app_settings_name == ldap_key).scalar()
+
+    @classmethod
+    def get_app_settings(cls, cache=False):
+
+        ret = cls.query()
+
+        if cache:
+            ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
+
+        if not ret:
+            raise Exception('Could not get application settings !')
+        settings = {}
+        for each in ret:
+            settings['rhodecode_' + each.app_settings_name] = \
+                each.app_settings_value
+
+        return settings
+
+    @classmethod
+    def get_ldap_settings(cls, cache=False):
+        ret = cls.query()\
+                .filter(cls.app_settings_name.startswith('ldap_')).all()
+        fd = {}
+        for row in ret:
+            fd.update({row.app_settings_name:row.app_settings_value})
+
+        return fd
+
+
+class RhodeCodeUi(Base, BaseModel):
+    __tablename__ = 'rhodecode_ui'
+    __table_args__ = (
+        UniqueConstraint('ui_key'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+
+    HOOK_UPDATE = 'changegroup.update'
+    HOOK_REPO_SIZE = 'changegroup.repo_size'
+    HOOK_PUSH = 'pretxnchangegroup.push_logger'
+    HOOK_PULL = 'preoutgoing.pull_logger'
+
+    ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
+
+    @classmethod
+    def get_by_key(cls, key):
+        return cls.query().filter(cls.ui_key == key)
+
+    @classmethod
+    def get_builtin_hooks(cls):
+        q = cls.query()
+        q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
+                                    cls.HOOK_REPO_SIZE,
+                                    cls.HOOK_PUSH, cls.HOOK_PULL]))
+        return q.all()
+
+    @classmethod
+    def get_custom_hooks(cls):
+        q = cls.query()
+        q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
+                                    cls.HOOK_REPO_SIZE,
+                                    cls.HOOK_PUSH, cls.HOOK_PULL]))
+        q = q.filter(cls.ui_section == 'hooks')
+        return q.all()
+
+    @classmethod
+    def create_or_update_hook(cls, key, val):
+        new_ui = cls.get_by_key(key).scalar() or cls()
+        new_ui.ui_section = 'hooks'
+        new_ui.ui_active = True
+        new_ui.ui_key = key
+        new_ui.ui_value = val
+
+        Session.add(new_ui)
+
+
+class User(Base, BaseModel):
+    __tablename__ = 'users'
+    __table_args__ = (
+        UniqueConstraint('username'), UniqueConstraint('email'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    active = Column("active", Boolean(), nullable=True, unique=None, default=None)
+    admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
+    name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
+    ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+
+    user_log = relationship('UserLog', cascade='all')
+    user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
+
+    repositories = relationship('Repository')
+    user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
+    repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
+    repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
+
+    group_member = relationship('UsersGroupMember', cascade='all')
+
+    notifications = relationship('UserNotification', cascade='all')
+    # notifications assigned to this user
+    user_created_notifications = relationship('Notification', cascade='all')
+    # comments created by this user
+    user_comments = relationship('ChangesetComment', cascade='all')
+
+    @hybrid_property
+    def email(self):
+        return self._email
+
+    @email.setter
+    def email(self, val):
+        self._email = val.lower() if val else None
+
+    @property
+    def full_name(self):
+        return '%s %s' % (self.name, self.lastname)
+
+    @property
+    def full_name_or_username(self):
+        return ('%s %s' % (self.name, self.lastname)
+                if (self.name and self.lastname) else self.username)
+
+    @property
+    def full_contact(self):
+        return '%s %s <%s>' % (self.name, self.lastname, self.email)
+
+    @property
+    def short_contact(self):
+        return '%s %s' % (self.name, self.lastname)
+
+    @property
+    def is_admin(self):
+        return self.admin
+
+    def __unicode__(self):
+        return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
+                                     self.user_id, self.username)
+
+    @classmethod
+    def get_by_username(cls, username, case_insensitive=False, cache=False):
+        if case_insensitive:
+            q = cls.query().filter(cls.username.ilike(username))
+        else:
+            q = cls.query().filter(cls.username == username)
+
+        if cache:
+            q = q.options(FromCache(
+                            "sql_cache_short",
+                            "get_user_%s" % _hash_key(username)
+                          )
+            )
+        return q.scalar()
+
+    @classmethod
+    def get_by_api_key(cls, api_key, cache=False):
+        q = cls.query().filter(cls.api_key == api_key)
+
+        if cache:
+            q = q.options(FromCache("sql_cache_short",
+                                    "get_api_key_%s" % api_key))
+        return q.scalar()
+
+    @classmethod
+    def get_by_email(cls, email, case_insensitive=False, cache=False):
+        if case_insensitive:
+            q = cls.query().filter(cls.email.ilike(email))
+        else:
+            q = cls.query().filter(cls.email == email)
+
+        if cache:
+            q = q.options(FromCache("sql_cache_short",
+                                    "get_api_key_%s" % email))
+        return q.scalar()
+
+    def update_lastlogin(self):
+        """Update user lastlogin"""
+        self.last_login = datetime.datetime.now()
+        Session.add(self)
+        log.debug('updated user %s lastlogin' % self.username)
+
+    def __json__(self):
+        return dict(
+            user_id=self.user_id,
+            first_name=self.name,
+            last_name=self.lastname,
+            email=self.email,
+            full_name=self.full_name,
+            full_name_or_username=self.full_name_or_username,
+            short_contact=self.short_contact,
+            full_contact=self.full_contact
+        )
+
+
+class UserLog(Base, BaseModel):
+    __tablename__ = 'user_logs'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+    user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+    repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
+    repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
+
+    @property
+    def action_as_day(self):
+        return datetime.date(*self.action_date.timetuple()[:3])
+
+    user = relationship('User')
+    repository = relationship('Repository', cascade='')
+
+
+class UsersGroup(Base, BaseModel):
+    __tablename__ = 'users_groups'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+
+    users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
+    users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
+
+    members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
+    users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
+    users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
+
+    def __unicode__(self):
+        return u'<userGroup(%s)>' % (self.users_group_name)
+
+    @classmethod
+    def get_by_group_name(cls, group_name, cache=False,
+                          case_insensitive=False):
+        if case_insensitive:
+            q = cls.query().filter(cls.users_group_name.ilike(group_name))
+        else:
+            q = cls.query().filter(cls.users_group_name == group_name)
+        if cache:
+            q = q.options(FromCache(
+                            "sql_cache_short",
+                            "get_user_%s" % _hash_key(group_name)
+                          )
+            )
+        return q.scalar()
+
+    @classmethod
+    def get(cls, users_group_id, cache=False):
+        users_group = cls.query()
+        if cache:
+            users_group = users_group.options(FromCache("sql_cache_short",
+                                    "get_users_group_%s" % users_group_id))
+        return users_group.get(users_group_id)
+
+
+class UsersGroupMember(Base, BaseModel):
+    __tablename__ = 'users_groups_members'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+
+    users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+
+    user = relationship('User', lazy='joined')
+    users_group = relationship('UsersGroup')
+
+    def __init__(self, gr_id='', u_id=''):
+        self.users_group_id = gr_id
+        self.user_id = u_id
+
+
+class Repository(Base, BaseModel):
+    __tablename__ = 'repositories'
+    __table_args__ = (
+        UniqueConstraint('repo_name'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+
+    repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
+    clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
+    repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
+    private = Column("private", Boolean(), nullable=True, unique=None, default=None)
+    enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
+    enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
+    description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
+
+    fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
+    group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
+
+    user = relationship('User')
+    fork = relationship('Repository', remote_side=repo_id)
+    group = relationship('RepoGroup')
+    repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
+    users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
+    stats = relationship('Statistics', cascade='all', uselist=False)
+
+    followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
+
+    logs = relationship('UserLog')
+
+    def __unicode__(self):
+        return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
+                                   self.repo_name)
+
+    @classmethod
+    def url_sep(cls):
+        return '/'
+
+    @classmethod
+    def get_by_repo_name(cls, repo_name):
+        q = Session.query(cls).filter(cls.repo_name == repo_name)
+        q = q.options(joinedload(Repository.fork))\
+                .options(joinedload(Repository.user))\
+                .options(joinedload(Repository.group))
+        return q.scalar()
+
+    @classmethod
+    def get_repo_forks(cls, repo_id):
+        return cls.query().filter(Repository.fork_id == repo_id)
+
+    @classmethod
+    def base_path(cls):
+        """
+        Returns base path when all repos are stored
+
+        :param cls:
+        """
+        q = Session.query(RhodeCodeUi)\
+            .filter(RhodeCodeUi.ui_key == cls.url_sep())
+        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
+        return q.one().ui_value
+
+    @property
+    def just_name(self):
+        return self.repo_name.split(Repository.url_sep())[-1]
+
+    @property
+    def groups_with_parents(self):
+        groups = []
+        if self.group is None:
+            return groups
+
+        cur_gr = self.group
+        groups.insert(0, cur_gr)
+        while 1:
+            gr = getattr(cur_gr, 'parent_group', None)
+            cur_gr = cur_gr.parent_group
+            if gr is None:
+                break
+            groups.insert(0, gr)
+
+        return groups
+
+    @property
+    def groups_and_repo(self):
+        return self.groups_with_parents, self.just_name
+
+    @LazyProperty
+    def repo_path(self):
+        """
+        Returns base full path for that repository means where it actually
+        exists on a filesystem
+        """
+        q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
+                                              Repository.url_sep())
+        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
+        return q.one().ui_value
+
+    @property
+    def repo_full_path(self):
+        p = [self.repo_path]
+        # we need to split the name by / since this is how we store the
+        # names in the database, but that eventually needs to be converted
+        # into a valid system path
+        p += self.repo_name.split(Repository.url_sep())
+        return os.path.join(*p)
+
+    def get_new_name(self, repo_name):
+        """
+        returns new full repository name based on assigned group and new new
+
+        :param group_name:
+        """
+        path_prefix = self.group.full_path_splitted if self.group else []
+        return Repository.url_sep().join(path_prefix + [repo_name])
+
+    @property
+    def _ui(self):
+        """
+        Creates an db based ui object for this repository
+        """
+        from mercurial import ui
+        from mercurial import config
+        baseui = ui.ui()
+
+        #clean the baseui object
+        baseui._ocfg = config.config()
+        baseui._ucfg = config.config()
+        baseui._tcfg = config.config()
+
+        ret = RhodeCodeUi.query()\
+            .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
+
+        hg_ui = ret
+        for ui_ in hg_ui:
+            if ui_.ui_active:
+                log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
+                          ui_.ui_key, ui_.ui_value)
+                baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
+
+        return baseui
+
+    @classmethod
+    def is_valid(cls, repo_name):
+        """
+        returns True if given repo name is a valid filesystem repository
+
+        :param cls:
+        :param repo_name:
+        """
+        from rhodecode.lib.utils import is_valid_repo
+
+        return is_valid_repo(repo_name, cls.base_path())
+
+    #==========================================================================
+    # SCM PROPERTIES
+    #==========================================================================
+
+    def get_changeset(self, rev):
+        return get_changeset_safe(self.scm_instance, rev)
+
+    @property
+    def tip(self):
+        return self.get_changeset('tip')
+
+    @property
+    def author(self):
+        return self.tip.author
 
-from rhodecode.model.db import *
+    @property
+    def last_change(self):
+        return self.scm_instance.last_change
+
+    def comments(self, revisions=None):
+        """
+        Returns comments for this repository grouped by revisions
+
+        :param revisions: filter query by revisions only
+        """
+        cmts = ChangesetComment.query()\
+            .filter(ChangesetComment.repo == self)
+        if revisions:
+            cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
+        grouped = defaultdict(list)
+        for cmt in cmts.all():
+            grouped[cmt.revision].append(cmt)
+        return grouped
+
+    #==========================================================================
+    # SCM CACHE INSTANCE
+    #==========================================================================
+
+    @property
+    def invalidate(self):
+        return CacheInvalidation.invalidate(self.repo_name)
+
+    def set_invalidate(self):
+        """
+        set a cache for invalidation for this instance
+        """
+        CacheInvalidation.set_invalidate(self.repo_name)
+
+    @LazyProperty
+    def scm_instance(self):
+        return self.__get_instance()
+
+    @property
+    def scm_instance_cached(self):
+        @cache_region('long_term')
+        def _c(repo_name):
+            return self.__get_instance()
+        rn = self.repo_name
+        log.debug('Getting cached instance of repo')
+        inv = self.invalidate
+        if inv is not None:
+            region_invalidate(_c, None, rn)
+            # update our cache
+            CacheInvalidation.set_valid(inv.cache_key)
+        return _c(rn)
+
+    def __get_instance(self):
+        repo_full_path = self.repo_full_path
+        try:
+            alias = get_scm(repo_full_path)[0]
+            log.debug('Creating instance of %s repository' % alias)
+            backend = get_backend(alias)
+        except VCSError:
+            log.error(traceback.format_exc())
+            log.error('Perhaps this repository is in db and not in '
+                      'filesystem run rescan repositories with '
+                      '"destroy old data " option from admin panel')
+            return
+
+        if alias == 'hg':
+
+            repo = backend(safe_str(repo_full_path), create=False,
+                           baseui=self._ui)
+            # skip hidden web repository
+            if repo._get_hidden():
+                return
+        else:
+            repo = backend(repo_full_path, create=False)
+
+        return repo
+
+
+class RepoGroup(Base, BaseModel):
+    __tablename__ = 'groups'
+    __table_args__ = (
+        UniqueConstraint('group_name', 'group_parent_id'),
+        CheckConstraint('group_id != group_parent_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+    __mapper_args__ = {'order_by': 'group_name'}
+
+    group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
+    group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
+    group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+
+    repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
+    users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
+
+    parent_group = relationship('RepoGroup', remote_side=group_id)
+
+    def __init__(self, group_name='', parent_group=None):
+        self.group_name = group_name
+        self.parent_group = parent_group
+
+    def __unicode__(self):
+        return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
+                                  self.group_name)
+
+    @classmethod
+    def groups_choices(cls):
+        from webhelpers.html import literal as _literal
+        repo_groups = [('', '')]
+        sep = ' &raquo; '
+        _name = lambda k: _literal(sep.join(k))
+
+        repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
+                              for x in cls.query().all()])
+
+        repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
+        return repo_groups
+
+    @classmethod
+    def url_sep(cls):
+        return '/'
+
+    @classmethod
+    def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
+        if case_insensitive:
+            gr = cls.query()\
+                .filter(cls.group_name.ilike(group_name))
+        else:
+            gr = cls.query()\
+                .filter(cls.group_name == group_name)
+        if cache:
+            gr = gr.options(FromCache(
+                            "sql_cache_short",
+                            "get_group_%s" % _hash_key(group_name)
+                            )
+            )
+        return gr.scalar()
+
+    @property
+    def parents(self):
+        parents_recursion_limit = 5
+        groups = []
+        if self.parent_group is None:
+            return groups
+        cur_gr = self.parent_group
+        groups.insert(0, cur_gr)
+        cnt = 0
+        while 1:
+            cnt += 1
+            gr = getattr(cur_gr, 'parent_group', None)
+            cur_gr = cur_gr.parent_group
+            if gr is None:
+                break
+            if cnt == parents_recursion_limit:
+                # this will prevent accidental infinit loops
+                log.error('group nested more than %s' %
+                          parents_recursion_limit)
+                break
+
+            groups.insert(0, gr)
+        return groups
+
+    @property
+    def children(self):
+        return RepoGroup.query().filter(RepoGroup.parent_group == self)
+
+    @property
+    def name(self):
+        return self.group_name.split(RepoGroup.url_sep())[-1]
+
+    @property
+    def full_path(self):
+        return self.group_name
+
+    @property
+    def full_path_splitted(self):
+        return self.group_name.split(RepoGroup.url_sep())
+
+    @property
+    def repositories(self):
+        return Repository.query()\
+                .filter(Repository.group == self)\
+                .order_by(Repository.repo_name)
+
+    @property
+    def repositories_recursive_count(self):
+        cnt = self.repositories.count()
+
+        def children_count(group):
+            cnt = 0
+            for child in group.children:
+                cnt += child.repositories.count()
+                cnt += children_count(child)
+            return cnt
+
+        return cnt + children_count(self)
+
+    def get_new_name(self, group_name):
+        """
+        returns new full group name based on parent and new name
+
+        :param group_name:
+        """
+        path_prefix = (self.parent_group.full_path_splitted if
+                       self.parent_group else [])
+        return RepoGroup.url_sep().join(path_prefix + [group_name])
+
+
+class Permission(Base, BaseModel):
+    __tablename__ = 'permissions'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+    permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+
+    def __unicode__(self):
+        return u"<%s('%s:%s')>" % (
+            self.__class__.__name__, self.permission_id, self.permission_name
+        )
+
+    @classmethod
+    def get_by_key(cls, key):
+        return cls.query().filter(cls.permission_name == key).scalar()
+
+    @classmethod
+    def get_default_perms(cls, default_user_id):
+        q = Session.query(UserRepoToPerm, Repository, cls)\
+         .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
+         .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
+         .filter(UserRepoToPerm.user_id == default_user_id)
+
+        return q.all()
+
+    @classmethod
+    def get_default_group_perms(cls, default_user_id):
+        q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
+         .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
+         .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
+         .filter(UserRepoGroupToPerm.user_id == default_user_id)
+
+        return q.all()
+
+
+class UserRepoToPerm(Base, BaseModel):
+    __tablename__ = 'repo_to_perm'
+    __table_args__ = (
+        UniqueConstraint('user_id', 'repository_id', 'permission_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+    permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+    repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
+
+    user = relationship('User')
+    repository = relationship('Repository')
+    permission = relationship('Permission')
+
+    @classmethod
+    def create(cls, user, repository, permission):
+        n = cls()
+        n.user = user
+        n.repository = repository
+        n.permission = permission
+        Session.add(n)
+        return n
+
+    def __unicode__(self):
+        return u'<user:%s => %s >' % (self.user, self.repository)
+
+
+class UserToPerm(Base, BaseModel):
+    __tablename__ = 'user_to_perm'
+    __table_args__ = (
+        UniqueConstraint('user_id', 'permission_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+    permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+    user = relationship('User')
+    permission = relationship('Permission', lazy='joined')
+
+
+class UsersGroupRepoToPerm(Base, BaseModel):
+    __tablename__ = 'users_group_repo_to_perm'
+    __table_args__ = (
+        UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
+    permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+    repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
+
+    users_group = relationship('UsersGroup')
+    permission = relationship('Permission')
+    repository = relationship('Repository')
+
+    @classmethod
+    def create(cls, users_group, repository, permission):
+        n = cls()
+        n.users_group = users_group
+        n.repository = repository
+        n.permission = permission
+        Session.add(n)
+        return n
+
+    def __unicode__(self):
+        return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
+
+
+class UsersGroupToPerm(Base, BaseModel):
+    __tablename__ = 'users_group_to_perm'
+    __table_args__ = (
+        UniqueConstraint('users_group_id', 'permission_id',),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
+    permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+    users_group = relationship('UsersGroup')
+    permission = relationship('Permission')
+
+
+class UserRepoGroupToPerm(Base, BaseModel):
+    __tablename__ = 'user_repo_group_to_perm'
+    __table_args__ = (
+        UniqueConstraint('user_id', 'group_id', 'permission_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+
+    group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+    group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
+    permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+    user = relationship('User')
+    group = relationship('RepoGroup')
+    permission = relationship('Permission')
+
+
+class UsersGroupRepoGroupToPerm(Base, BaseModel):
+    __tablename__ = 'users_group_repo_group_to_perm'
+    __table_args__ = (
+        UniqueConstraint('users_group_id', 'group_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+
+    users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
+    group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
+    permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
+
+    users_group = relationship('UsersGroup')
+    permission = relationship('Permission')
+    group = relationship('RepoGroup')
+
+
+class Statistics(Base, BaseModel):
+    __tablename__ = 'statistics'
+    __table_args__ = (
+         UniqueConstraint('repository_id'),
+         {'extend_existing': True, 'mysql_engine':'InnoDB',
+          'mysql_charset': 'utf8'}
+    )
+    stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
+    stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
+    commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
+    commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
+    languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
+
+    repository = relationship('Repository', single_parent=True)
+
+
+class UserFollowing(Base, BaseModel):
+    __tablename__ = 'user_followings'
+    __table_args__ = (
+        UniqueConstraint('user_id', 'follows_repository_id'),
+        UniqueConstraint('user_id', 'follows_user_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+
+    user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+    follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
+    follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
+    follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
+
+    user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
+
+    follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
+    follows_repository = relationship('Repository', order_by='Repository.repo_name')
+
+    @classmethod
+    def get_repo_followers(cls, repo_id):
+        return cls.query().filter(cls.follows_repo_id == repo_id)
+
+
+class CacheInvalidation(Base, BaseModel):
+    __tablename__ = 'cache_invalidation'
+    __table_args__ = (
+        UniqueConstraint('cache_key'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+    cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
+
+    def __init__(self, cache_key, cache_args=''):
+        self.cache_key = cache_key
+        self.cache_args = cache_args
+        self.cache_active = False
+
+    def __unicode__(self):
+        return u"<%s('%s:%s')>" % (self.__class__.__name__,
+                                  self.cache_id, self.cache_key)
+    @classmethod
+    def clear_cache(cls):
+        cls.query().delete()
+
+    @classmethod
+    def _get_key(cls, key):
+        """
+        Wrapper for generating a key, together with a prefix
+
+        :param key:
+        """
+        import rhodecode
+        prefix = ''
+        iid = rhodecode.CONFIG.get('instance_id')
+        if iid:
+            prefix = iid
+        return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
+
+    @classmethod
+    def get_by_key(cls, key):
+        return cls.query().filter(cls.cache_key == key).scalar()
+
+    @classmethod
+    def _get_or_create_key(cls, key, prefix, org_key):
+        inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
+        if not inv_obj:
+            try:
+                inv_obj = CacheInvalidation(key, org_key)
+                Session.add(inv_obj)
+                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:
+        """
+
+        key, _prefix, _org_key = cls._get_key(key)
+        inv = cls._get_or_create_key(key, _prefix, _org_key)
+
+        if inv and inv.cache_active is False:
+            return inv
+
+    @classmethod
+    def set_invalidate(cls, key):
+        """
+        Mark this Cache key for invalidation
+
+        :param key:
+        """
+
+        key, _prefix, _org_key = cls._get_key(key)
+        inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
+        log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
+                                                             _org_key))
+        try:
+            for inv_obj in inv_objs:
+                if inv_obj:
+                    inv_obj.cache_active = False
+
+                Session.add(inv_obj)
+            Session.commit()
+        except Exception:
+            log.error(traceback.format_exc())
+            Session.rollback()
+
+    @classmethod
+    def set_valid(cls, key):
+        """
+        Mark this cache key as active and currently cached
+
+        :param key:
+        """
+        inv_obj = cls.get_by_key(key)
+        inv_obj.cache_active = True
+        Session.add(inv_obj)
+        Session.commit()
+
+
+class ChangesetComment(Base, BaseModel):
+    __tablename__ = 'changeset_comments'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+    comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
+    repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+    revision = Column('revision', String(40), nullable=False)
+    line_no = Column('line_no', Unicode(10), nullable=True)
+    f_path = Column('f_path', Unicode(1000), nullable=True)
+    user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
+    text = Column('text', Unicode(25000), nullable=False)
+    modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
+
+    author = relationship('User', lazy='joined')
+    repo = relationship('Repository')
+
+    @classmethod
+    def get_users(cls, revision):
+        """
+        Returns user associated with this changesetComment. ie those
+        who actually commented
+
+        :param cls:
+        :param revision:
+        """
+        return Session.query(User)\
+                .filter(cls.revision == revision)\
+                .join(ChangesetComment.author).all()
+
+
+class Notification(Base, BaseModel):
+    __tablename__ = 'notifications'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+
+    TYPE_CHANGESET_COMMENT = u'cs_comment'
+    TYPE_MESSAGE = u'message'
+    TYPE_MENTION = u'mention'
+    TYPE_REGISTRATION = u'registration'
+
+    notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
+    subject = Column('subject', Unicode(512), nullable=True)
+    body = Column('body', Unicode(50000), nullable=True)
+    created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
+    created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+    type_ = Column('type', Unicode(256))
+
+    created_by_user = relationship('User')
+    notifications_to_users = relationship('UserNotification', lazy='joined',
+                                          cascade="all, delete, delete-orphan")
+
+    @property
+    def recipients(self):
+        return [x.user for x in UserNotification.query()\
+                .filter(UserNotification.notification == self).all()]
+
+    @classmethod
+    def create(cls, created_by, subject, body, recipients, type_=None):
+        if type_ is None:
+            type_ = Notification.TYPE_MESSAGE
+
+        notification = cls()
+        notification.created_by_user = created_by
+        notification.subject = subject
+        notification.body = body
+        notification.type_ = type_
+        notification.created_on = datetime.datetime.now()
+
+        for u in recipients:
+            assoc = UserNotification()
+            assoc.notification = notification
+            u.notifications.append(assoc)
+        Session.add(notification)
+        return notification
+
+    @property
+    def description(self):
+        from rhodecode.model.notification import NotificationModel
+        return NotificationModel().make_description(self)
+
+
+class UserNotification(Base, BaseModel):
+    __tablename__ = 'user_to_notification'
+    __table_args__ = (
+        UniqueConstraint('user_id', 'notification_id'),
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
+    notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
+    read = Column('read', Boolean, default=False)
+    sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
+
+    user = relationship('User', lazy="joined")
+    notification = relationship('Notification', lazy="joined",
+                                order_by=lambda: Notification.created_on.desc(),)
+
+    def mark_as_read(self):
+        self.read = True
+        Session.add(self)
+
+
+class DbMigrateVersion(Base, BaseModel):
+    __tablename__ = 'db_migrate_version'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine':'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+    repository_id = Column('repository_id', String(250), primary_key=True)
+    repository_path = Column('repository_path', Text)
+    version = Column('version', Integer)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/lib/dbmigrate/schema/db_1_4_0.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.model.db_1_4_0
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Database Models for RhodeCode <=1.4.X
+
+    :created_on: Apr 08, 2010
+    :author: marcink
+    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#TODO: replace that will db.py content after 1.5 Release
+
+from rhodecode.model.db import *
\ No newline at end of file
--- a/rhodecode/lib/diffs.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/diffs.py	Mon Jun 18 00:35:13 2012 +0200
@@ -26,16 +26,23 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import re
+import io
 import difflib
 import markupsafe
+
 from itertools import tee, imap
 
+from mercurial import patch
+from mercurial.mdiff import diffopts
+from mercurial.bundlerepo import bundlerepository
+from mercurial import localrepo
+
 from pylons.i18n.translation import _
 
 from rhodecode.lib.vcs.exceptions import VCSError
 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
 from rhodecode.lib.helpers import escape
-from rhodecode.lib.utils import EmptyChangeset
+from rhodecode.lib.utils import EmptyChangeset, make_ui
 
 
 def wrap_to_table(str_):
@@ -379,6 +386,7 @@
                             })
 
                         line = lineiter.next()
+
         except StopIteration:
             pass
 
@@ -445,7 +453,7 @@
                 new_lineno_class='lineno old', old_lineno_class='lineno new',
                 code_class='code', enable_comments=False, diff_lines=None):
         """
-        Return udiff as html table with customized css classes
+        Return given diff as html table with customized css classes
         """
         def _link_to_if(condition, label, url):
             """
@@ -542,3 +550,78 @@
         Returns tuple of added, and removed lines for this instance
         """
         return self.adds, self.removes
+
+
+class InMemoryBundleRepo(bundlerepository):
+    def __init__(self, ui, path, bundlestream):
+        self._tempparent = None
+        localrepo.localrepository.__init__(self, ui, path)
+        self.ui.setconfig('phases', 'publish', False)
+
+        self.bundle = bundlestream
+
+        # dict with the mapping 'filename' -> position in the bundle
+        self.bundlefilespos = {}
+
+
+def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
+    """
+    General differ between branches, bookmarks or separate but releated 
+    repositories
+
+    :param org_repo:
+    :type org_repo:
+    :param org_ref:
+    :type org_ref:
+    :param other_repo:
+    :type other_repo:
+    :param other_ref:
+    :type other_ref:
+    """
+
+    bundlerepo = None
+    ignore_whitespace = False
+    context = 3
+    org_repo = org_repo.scm_instance._repo
+    other_repo = other_repo.scm_instance._repo
+    opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
+    org_ref = org_ref[1]
+    other_ref = other_ref[1]
+
+    if org_repo != other_repo:
+
+        common, incoming, rheads = discovery_data
+
+        # create a bundle (uncompressed if other repo is not local)
+        if other_repo.capable('getbundle') and incoming:
+            # disable repo hooks here since it's just bundle !
+            # patch and reset hooks section of UI config to not run any
+            # hooks on fetching archives with subrepos
+            for k, _ in other_repo.ui.configitems('hooks'):
+                other_repo.ui.setconfig('hooks', k, None)
+
+            unbundle = other_repo.getbundle('incoming', common=common,
+                                            heads=rheads)
+
+            buf = io.BytesIO()
+            while True:
+                chunk = unbundle._stream.read(1024 * 4)
+                if not chunk:
+                    break
+                buf.write(chunk)
+
+            buf.seek(0)
+            # replace chunked _stream with data that can do tell() and seek()
+            unbundle._stream = buf
+
+            ui = make_ui('db')
+            bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
+                                            bundlestream=unbundle)
+
+        return ''.join(patch.diff(bundlerepo or org_repo,
+                                  node1=org_repo[org_ref].node(),
+                                  node2=other_repo[other_ref].node(),
+                                  opts=opts))
+    else:
+        return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
+                                  opts=opts))
--- a/rhodecode/lib/helpers.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/helpers.py	Mon Jun 18 00:35:13 2012 +0200
@@ -45,11 +45,26 @@
 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
 from rhodecode.lib.vcs.backends.base import BaseChangeset
 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
+from rhodecode.model.changeset_status import ChangesetStatusModel
 from rhodecode.model.db import URL_SEP
 
 log = logging.getLogger(__name__)
 
 
+html_escape_table = {
+    "&": "&amp;",
+    '"': "&quot;",
+    "'": "&apos;",
+    ">": "&gt;",
+    "<": "&lt;",
+}
+
+
+def html_escape(text):
+    """Produce entities within text."""
+    return "".join(html_escape_table.get(c,c) for c in text)
+
+
 def shorter(text, size=20):
     postfix = '...'
     if len(text) > size:
@@ -340,7 +355,7 @@
 #==============================================================================
 from rhodecode.lib.vcs.utils import author_name, author_email
 from rhodecode.lib.utils2 import credentials_filter, age as _age
-from rhodecode.model.db import User
+from rhodecode.model.db import User, ChangesetStatus
 
 age = lambda  x: _age(x)
 capitalize = lambda x: x.capitalize()
@@ -982,3 +997,11 @@
     """
     return literal('<div class="rst-block">%s</div>' %
                    MarkupRenderer.rst_with_mentions(source))
+
+
+def changeset_status(repo, revision):
+    return ChangesetStatusModel().get_status(repo, revision)
+
+
+def changeset_status_lbl(changeset_status):
+    return dict(ChangesetStatus.STATUSES).get(changeset_status)
--- a/rhodecode/lib/profiler.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/profiler.py	Mon Jun 18 00:35:13 2012 +0200
@@ -1,5 +1,7 @@
 from __future__ import with_statement
 
+import gc
+import objgraph
 import cProfile
 import pstats
 import cgi
@@ -26,7 +28,7 @@
             profiler.snapshot_stats()
 
             stats = pstats.Stats(profiler)
-            stats.sort_stats('cumulative')
+            stats.sort_stats('calls') #cummulative
 
             # Redirect output
             out = StringIO()
@@ -44,6 +46,11 @@
                          'border-top: 4px dashed red; padding: 1em;">')
                 resp += cgi.escape(out.getvalue(), True)
 
+                ct = objgraph.show_most_common_types()
+                print ct
+
+                resp += ct if ct else '---'
+
                 output = StringIO()
                 pprint.pprint(environ, output, depth=3)
 
--- a/rhodecode/lib/vcs/nodes.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/vcs/nodes.py	Mon Jun 18 00:35:13 2012 +0200
@@ -431,8 +431,10 @@
     name, kind or state (or methods/attributes checking those two) would raise
     RemovedFileNodeError.
     """
-    ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
-        'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
+    ALLOWED_ATTRIBUTES = [
+        'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind', 
+        'added', 'changed', 'not_changed', 'removed'
+    ]
 
     def __init__(self, path):
         """
--- a/rhodecode/lib/vcs/utils/hgcompat.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/lib/vcs/utils/hgcompat.py	Mon Jun 18 00:35:13 2012 +0200
@@ -12,3 +12,4 @@
 from mercurial.mdiff import diffopts
 from mercurial.node import hex
 from mercurial.encoding import tolocal
+from mercurial import discovery
\ No newline at end of file
--- a/rhodecode/model/__init__.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/__init__.py	Mon Jun 18 00:35:13 2012 +0200
@@ -42,7 +42,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import logging
-
+from rhodecode.model.db import User, Repository, Permission
 from rhodecode.model import meta
 
 log = logging.getLogger(__name__)
@@ -96,3 +96,33 @@
                     )
                 else:
                     return callback(instance)
+
+    def _get_user(self, user):
+        """
+        Helper method to get user by ID, or username fallback
+
+        :param user:
+        :type user: UserID, username, or User instance
+        """
+        return self._get_instance(User, user,
+                                  callback=User.get_by_username)
+
+    def _get_repo(self, repository):
+        """
+        Helper method to get repository by ID, or repository name
+
+        :param repository:
+        :type repository: RepoID, repository name or Repository Instance
+        """
+        return self._get_instance(Repository, repository,
+                                  callback=Repository.get_by_repo_name)
+
+    def _get_perm(self, permission):
+        """
+        Helper method to get permission by ID, or permission name
+
+        :param permission:
+        :type permission: PermissionID, permission_name or Permission instance
+        """
+        return self._get_instance(Permission, permission,
+                                  callback=Permission.get_by_key)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/model/changeset_status.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.model.changeset_status
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+    :created_on: Apr 30, 2012
+    :author: marcink
+    :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import logging
+
+from rhodecode.model import BaseModel
+from rhodecode.model.db import ChangesetStatus, PullRequest
+
+log = logging.getLogger(__name__)
+
+
+class ChangesetStatusModel(BaseModel):
+
+    def __get_changeset_status(self, changeset_status):
+        return self._get_instance(ChangesetStatus, changeset_status)
+
+    def __get_pull_request(self, pull_request):
+        return self._get_instance(PullRequest, pull_request)
+
+    def get_status(self, repo, revision=None, pull_request=None):
+        """
+        Returns latest status of changeset for given revision or for given
+        pull request. Statuses are versioned inside a table itself and
+        version == 0 is always the current one
+
+        :param repo:
+        :type repo:
+        :param revision: 40char hash or None
+        :type revision: str
+        :param pull_request: pull_request reference
+        :type:
+        """
+        repo = self._get_repo(repo)
+
+        q = ChangesetStatus.query()\
+            .filter(ChangesetStatus.repo == repo)\
+            .filter(ChangesetStatus.version == 0)
+
+        if revision:
+            q = q.filter(ChangesetStatus.revision == revision)
+        elif pull_request:
+            pull_request = self.__get_pull_request(pull_request)
+            q = q.filter(ChangesetStatus.pull_request == pull_request)
+        else:
+            raise Exception('Please specify revision or pull_request')
+
+        # need to use first here since there can be multiple statuses
+        # returned from pull_request
+        status = q.first()
+        status = status.status if status else status
+        st = status or ChangesetStatus.DEFAULT
+        return str(st)
+
+    def set_status(self, repo, status, user, comment, revision=None,
+                   pull_request=None):
+        """
+        Creates new status for changeset or updates the old ones bumping their
+        version, leaving the current status at
+
+        :param repo:
+        :type repo:
+        :param revision:
+        :type revision:
+        :param status:
+        :type status:
+        :param user:
+        :type user:
+        :param comment:
+        :type comment:
+        """
+        repo = self._get_repo(repo)
+
+        q = ChangesetStatus.query()
+
+        if revision:
+            q = q.filter(ChangesetStatus.repo == repo)
+            q = q.filter(ChangesetStatus.revision == revision)
+        elif pull_request:
+            pull_request = self.__get_pull_request(pull_request)
+            q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
+            q = q.filter(ChangesetStatus.pull_request == pull_request)
+        cur_statuses = q.all()
+
+        if cur_statuses:
+            for st in cur_statuses:
+                st.version += 1
+                self.sa.add(st)
+
+        def _create_status(user, repo, status, comment, revision, pull_request):
+            new_status = ChangesetStatus()
+            new_status.author = self._get_user(user)
+            new_status.repo = self._get_repo(repo)
+            new_status.status = status
+            new_status.comment = comment
+            new_status.revision = revision
+            new_status.pull_request = pull_request
+            return new_status
+
+        if revision:
+            new_status = _create_status(user=user, repo=repo, status=status,
+                           comment=comment, revision=revision, 
+                           pull_request=None)
+            self.sa.add(new_status)
+            return new_status
+        elif pull_request:
+            #pull request can have more than one revision associated to it
+            #we need to create new version for each one
+            new_statuses = []
+            repo = pull_request.org_repo
+            for rev in pull_request.revisions:
+                new_status = _create_status(user=user, repo=repo,
+                                            status=status, comment=comment,
+                                            revision=rev,
+                                            pull_request=pull_request)
+                new_statuses.append(new_status)
+                self.sa.add(new_status)
+            return new_statuses
--- a/rhodecode/model/comment.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/comment.py	Mon Jun 18 00:35:13 2012 +0200
@@ -32,7 +32,8 @@
 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
 from rhodecode.lib import helpers as h
 from rhodecode.model import BaseModel
-from rhodecode.model.db import ChangesetComment, User, Repository, Notification
+from rhodecode.model.db import ChangesetComment, User, Repository, \
+    Notification, PullRequest
 from rhodecode.model.notification import NotificationModel
 
 log = logging.getLogger(__name__)
@@ -43,6 +44,9 @@
     def __get_changeset_comment(self, changeset_comment):
         return self._get_instance(ChangesetComment, changeset_comment)
 
+    def __get_pull_request(self, pull_request):
+        return self._get_instance(PullRequest, pull_request)
+
     def _extract_mentions(self, s):
         user_objects = []
         for username in extract_mentioned_users(s):
@@ -51,36 +55,54 @@
                 user_objects.append(user_obj)
         return user_objects
 
-    def create(self, text, repo_id, user_id, revision, f_path=None,
-               line_no=None):
+    def create(self, text, repo_id, user_id, revision=None, pull_request=None,
+               f_path=None, line_no=None, status_change=None):
         """
-        Creates new comment for changeset
+        Creates new comment for changeset or pull request.
+        IF status_change is not none this comment is associated with a 
+        status change of changeset or changesets associated with pull request
 
         :param text:
         :param repo_id:
         :param user_id:
         :param revision:
+        :param pull_request:
         :param f_path:
         :param line_no:
+        :param status_change:
         """
+        if not text:
+            return
 
-        if text:
-            repo = Repository.get(repo_id)
+        repo = Repository.get(repo_id)
+        comment = ChangesetComment()
+        comment.repo = repo
+        comment.user_id = user_id
+        comment.text = text
+        comment.f_path = f_path
+        comment.line_no = line_no
+
+        if revision:
             cs = repo.scm_instance.get_changeset(revision)
             desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
             author_email = cs.author_email
-            comment = ChangesetComment()
-            comment.repo = repo
-            comment.user_id = user_id
             comment.revision = revision
-            comment.text = text
-            comment.f_path = f_path
-            comment.line_no = line_no
+        elif pull_request:
+            pull_request = self.__get_pull_request(pull_request)
+            comment.pull_request = pull_request
+            desc = ''
+        else:
+            raise Exception('Please specify revision or pull_request_id')
 
-            self.sa.add(comment)
-            self.sa.flush()
-            # make notification
-            line = ''
+        self.sa.add(comment)
+        self.sa.flush()
+
+        # make notification
+        line = ''
+        body = text
+
+        #changeset
+        if revision:
             if line_no:
                 line = _('on line %s') % line_no
             subj = safe_unicode(
@@ -93,32 +115,41 @@
                           )
                 )
             )
-
-            body = text
-
+            notification_type = Notification.TYPE_CHANGESET_COMMENT
             # get the current participants of this changeset
             recipients = ChangesetComment.get_users(revision=revision)
-
             # add changeset author if it's in rhodecode system
             recipients += [User.get_by_email(author_email)]
+        #pull request
+        elif pull_request:
+            #TODO: make this something usefull
+            subj = 'commented on pull request something...'
+            notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
+            # get the current participants of this pull request
+            recipients = ChangesetComment.get_users(pull_request_id=
+                                                pull_request.pull_request_id)
+            # add pull request author
+            recipients += [pull_request.author]
 
-            # create notification objects, and emails
+        # create notification objects, and emails
+        NotificationModel().create(
+          created_by=user_id, subject=subj, body=body,
+          recipients=recipients, type_=notification_type,
+          email_kwargs={'status_change': status_change}
+        )
+
+        mention_recipients = set(self._extract_mentions(body))\
+                                .difference(recipients)
+        if mention_recipients:
+            subj = _('[Mention]') + ' ' + subj
             NotificationModel().create(
-              created_by=user_id, subject=subj, body=body,
-              recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT
+                created_by=user_id, subject=subj, body=body,
+                recipients=mention_recipients,
+                type_=notification_type,
+                email_kwargs={'status_change': status_change}
             )
 
-            mention_recipients = set(self._extract_mentions(body))\
-                                    .difference(recipients)
-            if mention_recipients:
-                subj = _('[Mention]') + ' ' + subj
-                NotificationModel().create(
-                    created_by=user_id, subject=subj, body=body,
-                    recipients=mention_recipients,
-                    type_=Notification.TYPE_CHANGESET_COMMENT
-                )
-
-            return comment
+        return comment
 
     def delete(self, comment):
         """
@@ -131,21 +162,47 @@
 
         return comment
 
-    def get_comments(self, repo_id, revision):
-        return ChangesetComment.query()\
+    def get_comments(self, repo_id, revision=None, pull_request=None):
+        """
+        Get's main comments based on revision or pull_request_id
+
+        :param repo_id:
+        :type repo_id:
+        :param revision:
+        :type revision:
+        :param pull_request:
+        :type pull_request:
+        """
+
+        q = ChangesetComment.query()\
                 .filter(ChangesetComment.repo_id == repo_id)\
-                .filter(ChangesetComment.revision == revision)\
                 .filter(ChangesetComment.line_no == None)\
-                .filter(ChangesetComment.f_path == None).all()
+                .filter(ChangesetComment.f_path == None)
+        if revision:
+            q = q.filter(ChangesetComment.revision == revision)
+        elif pull_request:
+            pull_request = self.__get_pull_request(pull_request)
+            q = q.filter(ChangesetComment.pull_request == pull_request)
+        else:
+            raise Exception('Please specify revision or pull_request')
+        return q.all()
 
-    def get_inline_comments(self, repo_id, revision):
-        comments = self.sa.query(ChangesetComment)\
+    def get_inline_comments(self, repo_id, revision=None, pull_request=None):
+        q = self.sa.query(ChangesetComment)\
             .filter(ChangesetComment.repo_id == repo_id)\
-            .filter(ChangesetComment.revision == revision)\
             .filter(ChangesetComment.line_no != None)\
             .filter(ChangesetComment.f_path != None)\
             .order_by(ChangesetComment.comment_id.asc())\
-            .all()
+
+        if revision:
+            q = q.filter(ChangesetComment.revision == revision)
+        elif pull_request:
+            pull_request = self.__get_pull_request(pull_request)
+            q = q.filter(ChangesetComment.pull_request == pull_request)
+        else:
+            raise Exception('Please specify revision or pull_request_id')
+
+        comments = q.all()
 
         paths = defaultdict(lambda: defaultdict(list))
 
--- a/rhodecode/model/db.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/db.py	Mon Jun 18 00:35:13 2012 +0200
@@ -33,8 +33,11 @@
 from sqlalchemy import *
 from sqlalchemy.ext.hybrid import hybrid_property
 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
+from sqlalchemy.exc import DatabaseError
 from beaker.cache import cache_region, region_invalidate
 
+from pylons.i18n.translation import lazy_ugettext as _
+
 from rhodecode.lib.vcs import get_backend
 from rhodecode.lib.vcs.utils.helpers import get_scm
 from rhodecode.lib.vcs.exceptions import VCSError
@@ -44,6 +47,7 @@
     safe_unicode
 from rhodecode.lib.compat import json
 from rhodecode.lib.caching_query import FromCache
+
 from rhodecode.model.meta import Base, Session
 
 
@@ -384,8 +388,23 @@
 
         if cache:
             q = q.options(FromCache("sql_cache_short",
-                                    "get_api_key_%s" % email))
-        return q.scalar()
+                                    "get_email_key_%s" % email))
+
+        ret = q.scalar()
+        if ret is None:
+            q = UserEmailMap.query()
+            # try fetching in alternate email map
+            if case_insensitive:
+                q = q.filter(UserEmailMap.email.ilike(email))
+            else:
+                q = q.filter(UserEmailMap.email == email)
+            q = q.options(joinedload(UserEmailMap.user))
+            if cache:
+                q = q.options(FromCache("sql_cache_short",
+                                        "get_email_map_key_%s" % email))
+            ret = getattr(q.scalar(), 'user', None)
+
+        return ret
 
     def update_lastlogin(self):
         """Update user lastlogin"""
@@ -406,6 +425,39 @@
         )
 
 
+class UserEmailMap(Base, BaseModel):
+    __tablename__ = 'user_email_map'
+    __table_args__ = (
+        Index('uem_email_idx', 'email'),
+        UniqueConstraint('email'),
+        {'extend_existing': True, 'mysql_engine': 'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+    __mapper_args__ = {}
+
+    email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
+    _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
+
+    user = relationship('User', lazy='joined')
+
+    @validates('_email')
+    def validate_email(self, key, email):
+        # check if this email is not main one
+        main_email = Session.query(User).filter(User.email == email).scalar()
+        if main_email is not None:
+            raise AttributeError('email %s is present is user table' % email)
+        return email
+
+    @hybrid_property
+    def email(self):
+        return self._email
+
+    @email.setter
+    def email(self, val):
+        self._email = val.lower() if val else None
+
+
 class UserLog(Base, BaseModel):
     __tablename__ = 'user_logs'
     __table_args__ = (
@@ -522,6 +574,7 @@
     followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
 
     logs = relationship('UserLog')
+    comments = relationship('ChangesetComment')
 
     def __unicode__(self):
         return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
@@ -561,6 +614,20 @@
         return q.one().ui_value
 
     @property
+    def forks(self):
+        """
+        Return forks of this repo
+        """
+        return Repository.get_repo_forks(self.repo_id)
+
+    @property
+    def parent(self):
+        """
+        Returns fork parent
+        """
+        return self.fork
+
+    @property
     def just_name(self):
         return self.repo_name.split(Repository.url_sep())[-1]
 
@@ -686,6 +753,29 @@
             grouped[cmt.revision].append(cmt)
         return grouped
 
+    def statuses(self, revisions=None):
+        """
+        Returns statuses for this repository
+
+        :param revisions: list of revisions to get statuses for
+        :type revisions: list
+        """
+
+        statuses = ChangesetStatus.query()\
+            .filter(ChangesetStatus.repo == self)\
+            .filter(ChangesetStatus.version == 0)
+        if revisions:
+            statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
+        grouped = {}
+        for stat in statuses.all():
+            pr_id = pr_repo = None
+            if stat.pull_request:
+                pr_id = stat.pull_request.pull_request_id
+                pr_repo = stat.pull_request.other_repo.repo_name
+            grouped[stat.revision] = [str(stat.status), stat.status_lbl,
+                                      pr_id, pr_repo]
+        return grouped
+
     #==========================================================================
     # SCM CACHE INSTANCE
     #==========================================================================
@@ -887,6 +977,7 @@
 class Permission(Base, BaseModel):
     __tablename__ = 'permissions'
     __table_args__ = (
+        Index('p_perm_name_idx', 'permission_name'),
         {'extend_existing': True, 'mysql_engine': 'InnoDB',
          'mysql_charset': 'utf8'},
     )
@@ -1234,12 +1325,14 @@
 class ChangesetComment(Base, BaseModel):
     __tablename__ = 'changeset_comments'
     __table_args__ = (
+        Index('cc_revision_idx', 'revision'),
         {'extend_existing': True, 'mysql_engine': 'InnoDB',
          'mysql_charset': 'utf8'},
     )
     comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
     repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
-    revision = Column('revision', String(40), nullable=False)
+    revision = Column('revision', String(40), nullable=True)
+    pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
     line_no = Column('line_no', Unicode(10), nullable=True)
     f_path = Column('f_path', Unicode(1000), nullable=True)
     user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
@@ -1248,24 +1341,129 @@
 
     author = relationship('User', lazy='joined')
     repo = relationship('Repository')
+    status_change = relationship('ChangesetStatus', uselist=False)
+    pull_request = relationship('PullRequest', lazy='joined')
 
     @classmethod
-    def get_users(cls, revision):
+    def get_users(cls, revision=None, pull_request_id=None):
         """
-        Returns user associated with this changesetComment. ie those
+        Returns user associated with this ChangesetComment. ie those
         who actually commented
 
         :param cls:
         :param revision:
         """
-        return Session.query(User)\
-                .filter(cls.revision == revision)\
-                .join(ChangesetComment.author).all()
+        q = Session.query(User)\
+                .join(ChangesetComment.author)
+        if revision:
+            q = q.filter(cls.revision == revision)
+        elif pull_request_id:
+            q = q.filter(cls.pull_request_id == pull_request_id)
+        return q.all()
+
+
+class ChangesetStatus(Base, BaseModel):
+    __tablename__ = 'changeset_statuses'
+    __table_args__ = (
+        Index('cs_revision_idx', 'revision'),
+        Index('cs_version_idx', 'version'),
+        UniqueConstraint('repo_id', 'revision', 'version'),
+        {'extend_existing': True, 'mysql_engine': 'InnoDB',
+         'mysql_charset': 'utf8'}
+    )
+
+    STATUSES = [
+        ('not_reviewed', _("Not Reviewed")),  # (no icon) and default
+        ('approved', _("Approved")),
+        ('rejected', _("Rejected")),
+        ('under_review', _("Under Review")),
+    ]
+    DEFAULT = STATUSES[0][0]
+
+    changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
+    repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
+    revision = Column('revision', String(40), nullable=False)
+    status = Column('status', String(128), nullable=False, default=DEFAULT)
+    changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
+    modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
+    version = Column('version', Integer(), nullable=False, default=0)
+    pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
+
+    author = relationship('User', lazy='joined')
+    repo = relationship('Repository')
+    comment = relationship('ChangesetComment', lazy='joined')
+    pull_request = relationship('PullRequest', lazy='joined')
+
+    @classmethod
+    def get_status_lbl(cls, value):
+        return dict(cls.STATUSES).get(value)
+
+    @property
+    def status_lbl(self):
+        return ChangesetStatus.get_status_lbl(self.status)
+
+
+class PullRequest(Base, BaseModel):
+    __tablename__ = 'pull_requests'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine': 'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+
+    pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
+    title = Column('title', Unicode(256), nullable=True)
+    description = Column('description', Unicode(10240), nullable=True)
+    created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
+    _revisions = Column('revisions', UnicodeText(20500))  # 500 revisions max
+    org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+    org_ref = Column('org_ref', Unicode(256), nullable=False)
+    other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
+    other_ref = Column('other_ref', Unicode(256), nullable=False)
+
+    @hybrid_property
+    def revisions(self):
+        return self._revisions.split(':')
+
+    @revisions.setter
+    def revisions(self, val):
+        self._revisions = ':'.join(val)
+
+    author = relationship('User', lazy='joined')
+    reviewers = relationship('PullRequestReviewers')
+    org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
+    other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
+
+    def __json__(self):
+        return dict(
+          revisions=self.revisions
+        )
+
+
+class PullRequestReviewers(Base, BaseModel):
+    __tablename__ = 'pull_request_reviewers'
+    __table_args__ = (
+        {'extend_existing': True, 'mysql_engine': 'InnoDB',
+         'mysql_charset': 'utf8'},
+    )
+
+    def __init__(self, user=None, pull_request=None):
+        self.user = user
+        self.pull_request = pull_request
+
+    pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
+    pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
+
+    user = relationship('User')
+    pull_request = relationship('PullRequest')
 
 
 class Notification(Base, BaseModel):
     __tablename__ = 'notifications'
     __table_args__ = (
+        Index('notification_type_idx', 'type'),
         {'extend_existing': True, 'mysql_engine': 'InnoDB',
          'mysql_charset': 'utf8'},
     )
@@ -1274,6 +1472,8 @@
     TYPE_MESSAGE = u'message'
     TYPE_MENTION = u'mention'
     TYPE_REGISTRATION = u'registration'
+    TYPE_PULL_REQUEST = u'pull_request'
+    TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
 
     notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
     subject = Column('subject', Unicode(512), nullable=True)
--- a/rhodecode/model/forms.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/forms.py	Mon Jun 18 00:35:13 2012 +0200
@@ -19,573 +19,76 @@
 for SELECT use formencode.All(OneOf(list), Int())
 
 """
-import os
-import re
 import logging
-import traceback
 
 import formencode
 from formencode import All
-from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
-    Email, Bool, StringBoolean, Set
 
 from pylons.i18n.translation import _
-from webhelpers.pylonslib.secure_form import authentication_token
 
-from rhodecode.config.routing import ADMIN_PREFIX
-from rhodecode.lib.utils import repo_name_slug
-from rhodecode.lib.auth import authenticate, get_crypt_password
-from rhodecode.lib.exceptions import LdapImportError
-from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
+from rhodecode.model import validators as v
 from rhodecode import BACKENDS
 
 log = logging.getLogger(__name__)
 
 
-#this is needed to translate the messages using _() in validators
-class State_obj(object):
-    _ = staticmethod(_)
-
-
-#==============================================================================
-# VALIDATORS
-#==============================================================================
-class ValidAuthToken(formencode.validators.FancyValidator):
-    messages = {'invalid_token': _('Token mismatch')}
-
-    def validate_python(self, value, state):
-
-        if value != authentication_token():
-            raise formencode.Invalid(
-                self.message('invalid_token',
-                             state, search_number=value),
-                value,
-                state
-            )
-
-
-def ValidUsername(edit, old_data):
-    class _ValidUsername(formencode.validators.FancyValidator):
-
-        def validate_python(self, value, state):
-            if value in ['default', 'new_user']:
-                raise formencode.Invalid(_('Invalid username'), value, state)
-            #check if user is unique
-            old_un = None
-            if edit:
-                old_un = User.get(old_data.get('user_id')).username
-
-            if old_un != value or not edit:
-                if User.get_by_username(value, case_insensitive=True):
-                    raise formencode.Invalid(_('This username already '
-                                               'exists') , value, state)
-
-            if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
-                raise formencode.Invalid(
-                    _('Username may only contain alphanumeric characters '
-                      'underscores, periods or dashes and must begin with '
-                      'alphanumeric character'),
-                    value,
-                    state
-                )
-
-    return _ValidUsername
-
-
-def ValidUsersGroup(edit, old_data):
-
-    class _ValidUsersGroup(formencode.validators.FancyValidator):
-
-        def validate_python(self, value, state):
-            if value in ['default']:
-                raise formencode.Invalid(_('Invalid group name'), value, state)
-            #check if group is unique
-            old_ugname = None
-            if edit:
-                old_ugname = UsersGroup.get(
-                            old_data.get('users_group_id')).users_group_name
-
-            if old_ugname != value or not edit:
-                if UsersGroup.get_by_group_name(value, cache=False,
-                                               case_insensitive=True):
-                    raise formencode.Invalid(_('This users group '
-                                               'already exists'), value,
-                                             state)
-
-            if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
-                raise formencode.Invalid(
-                    _('RepoGroup name may only contain  alphanumeric characters '
-                      'underscores, periods or dashes and must begin with '
-                      'alphanumeric character'),
-                    value,
-                    state
-                )
-
-    return _ValidUsersGroup
-
-
-def ValidReposGroup(edit, old_data):
-    class _ValidReposGroup(formencode.validators.FancyValidator):
-
-        def validate_python(self, value, state):
-            # TODO WRITE VALIDATIONS
-            group_name = value.get('group_name')
-            group_parent_id = value.get('group_parent_id')
-
-            # slugify repo group just in case :)
-            slug = repo_name_slug(group_name)
-
-            # check for parent of self
-            parent_of_self = lambda: (
-                old_data['group_id'] == int(group_parent_id)
-                if group_parent_id else False
-            )
-            if edit and parent_of_self():
-                    e_dict = {
-                        'group_parent_id': _('Cannot assign this group as parent')
-                    }
-                    raise formencode.Invalid('', value, state,
-                                             error_dict=e_dict)
-
-            old_gname = None
-            if edit:
-                old_gname = RepoGroup.get(old_data.get('group_id')).group_name
-
-            if old_gname != group_name or not edit:
-
-                # check group
-                gr = RepoGroup.query()\
-                      .filter(RepoGroup.group_name == slug)\
-                      .filter(RepoGroup.group_parent_id == group_parent_id)\
-                      .scalar()
-
-                if gr:
-                    e_dict = {
-                        'group_name': _('This group already exists')
-                    }
-                    raise formencode.Invalid('', value, state,
-                                             error_dict=e_dict)
-
-                # check for same repo
-                repo = Repository.query()\
-                      .filter(Repository.repo_name == slug)\
-                      .scalar()
-
-                if repo:
-                    e_dict = {
-                        'group_name': _('Repository with this name already exists')
-                    }
-                    raise formencode.Invalid('', value, state,
-                                             error_dict=e_dict)
-
-    return _ValidReposGroup
-
-
-class ValidPassword(formencode.validators.FancyValidator):
-
-    def to_python(self, value, state):
-
-        if not value:
-            return
-
-        if value.get('password'):
-            try:
-                value['password'] = get_crypt_password(value['password'])
-            except UnicodeEncodeError:
-                e_dict = {'password': _('Invalid characters in password')}
-                raise formencode.Invalid('', value, state, error_dict=e_dict)
-
-        if value.get('password_confirmation'):
-            try:
-                value['password_confirmation'] = \
-                    get_crypt_password(value['password_confirmation'])
-            except UnicodeEncodeError:
-                e_dict = {
-                    'password_confirmation': _('Invalid characters in password')
-                }
-                raise formencode.Invalid('', value, state, error_dict=e_dict)
-
-        if value.get('new_password'):
-            try:
-                value['new_password'] = \
-                    get_crypt_password(value['new_password'])
-            except UnicodeEncodeError:
-                e_dict = {'new_password': _('Invalid characters in password')}
-                raise formencode.Invalid('', value, state, error_dict=e_dict)
-
-        return value
-
-
-class ValidPasswordsMatch(formencode.validators.FancyValidator):
-
-    def validate_python(self, value, state):
-
-        pass_val = value.get('password') or value.get('new_password')
-        if pass_val != value['password_confirmation']:
-            e_dict = {'password_confirmation':
-                   _('Passwords do not match')}
-            raise formencode.Invalid('', value, state, error_dict=e_dict)
-
-
-class ValidAuth(formencode.validators.FancyValidator):
-    messages = {
-        'invalid_password':_('invalid password'),
-        'invalid_login':_('invalid user name'),
-        'disabled_account':_('Your account is disabled')
-    }
-
-    # error mapping
-    e_dict = {'username': messages['invalid_login'],
-              'password': messages['invalid_password']}
-    e_dict_disable = {'username': messages['disabled_account']}
-
-    def validate_python(self, value, state):
-        password = value['password']
-        username = value['username']
-        user = User.get_by_username(username)
-
-        if authenticate(username, password):
-            return value
-        else:
-            if user and user.active is False:
-                log.warning('user %s is disabled' % username)
-                raise formencode.Invalid(
-                    self.message('disabled_account',
-                    state=State_obj),
-                    value, state,
-                    error_dict=self.e_dict_disable
-                )
-            else:
-                log.warning('user %s failed to authenticate' % username)
-                raise formencode.Invalid(
-                    self.message('invalid_password',
-                    state=State_obj), value, state,
-                    error_dict=self.e_dict
-                )
-
-
-class ValidRepoUser(formencode.validators.FancyValidator):
-
-    def to_python(self, value, state):
-        try:
-            User.query().filter(User.active == True)\
-                .filter(User.username == value).one()
-        except Exception:
-            raise formencode.Invalid(_('This username is not valid'),
-                                     value, state)
-        return value
-
-
-def ValidRepoName(edit, old_data):
-    class _ValidRepoName(formencode.validators.FancyValidator):
-        def to_python(self, value, state):
-
-            repo_name = value.get('repo_name')
-
-            slug = repo_name_slug(repo_name)
-            if slug in [ADMIN_PREFIX, '']:
-                e_dict = {'repo_name': _('This repository name is disallowed')}
-                raise formencode.Invalid('', value, state, error_dict=e_dict)
-
-            if value.get('repo_group'):
-                gr = RepoGroup.get(value.get('repo_group'))
-                group_path = gr.full_path
-                # value needs to be aware of group name in order to check
-                # db key This is an actual just the name to store in the
-                # database
-                repo_name_full = group_path + RepoGroup.url_sep() + repo_name
-
-            else:
-                group_path = ''
-                repo_name_full = repo_name
-
-            value['repo_name_full'] = repo_name_full
-            rename = old_data.get('repo_name') != repo_name_full
-            create = not edit
-            if  rename or create:
-
-                if group_path != '':
-                    if Repository.get_by_repo_name(repo_name_full):
-                        e_dict = {
-                            'repo_name': _('This repository already exists in '
-                                           'a group "%s"') % gr.group_name
-                        }
-                        raise formencode.Invalid('', value, state,
-                                                 error_dict=e_dict)
-                elif RepoGroup.get_by_group_name(repo_name_full):
-                        e_dict = {
-                            'repo_name': _('There is a group with this name '
-                                           'already "%s"') % repo_name_full
-                        }
-                        raise formencode.Invalid('', value, state,
-                                                 error_dict=e_dict)
-
-                elif Repository.get_by_repo_name(repo_name_full):
-                        e_dict = {'repo_name': _('This repository '
-                                                'already exists')}
-                        raise formencode.Invalid('', value, state,
-                                                 error_dict=e_dict)
-
-            return value
-
-    return _ValidRepoName
-
-
-def ValidForkName(*args, **kwargs):
-    return ValidRepoName(*args, **kwargs)
-
-
-def SlugifyName():
-    class _SlugifyName(formencode.validators.FancyValidator):
-
-        def to_python(self, value, state):
-            return repo_name_slug(value)
-
-    return _SlugifyName
-
-
-def ValidCloneUri():
-    from rhodecode.lib.utils import make_ui
-
-    def url_handler(repo_type, url, proto, ui=None):
-        if repo_type == 'hg':
-            from mercurial.httprepo import httprepository, httpsrepository
-            if proto == 'https':
-                httpsrepository(make_ui('db'), url).capabilities
-            elif proto == 'http':
-                httprepository(make_ui('db'), url).capabilities
-        elif repo_type == 'git':
-            #TODO: write a git url validator
-            pass
-
-    class _ValidCloneUri(formencode.validators.FancyValidator):
-
-        def to_python(self, value, state):
-
-            repo_type = value.get('repo_type')
-            url = value.get('clone_uri')
-            e_dict = {'clone_uri': _('invalid clone url')}
-
-            if not url:
-                pass
-            elif url.startswith('https'):
-                try:
-                    url_handler(repo_type, url, 'https', make_ui('db'))
-                except Exception:
-                    log.error(traceback.format_exc())
-                    raise formencode.Invalid('', value, state, error_dict=e_dict)
-            elif url.startswith('http'):
-                try:
-                    url_handler(repo_type, url, 'http', make_ui('db'))
-                except Exception:
-                    log.error(traceback.format_exc())
-                    raise formencode.Invalid('', value, state, error_dict=e_dict)
-            else:
-                e_dict = {'clone_uri': _('Invalid clone url, provide a '
-                                         'valid clone http\s url')}
-                raise formencode.Invalid('', value, state, error_dict=e_dict)
-
-            return value
-
-    return _ValidCloneUri
-
-
-def ValidForkType(old_data):
-    class _ValidForkType(formencode.validators.FancyValidator):
-
-        def to_python(self, value, state):
-            if old_data['repo_type'] != value:
-                raise formencode.Invalid(_('Fork have to be the same '
-                                           'type as original'), value, state)
-
-            return value
-    return _ValidForkType
-
-
-def ValidPerms(type_='repo'):
-    if type_ == 'group':
-        EMPTY_PERM = 'group.none'
-    elif type_ == 'repo':
-        EMPTY_PERM = 'repository.none'
-
-    class _ValidPerms(formencode.validators.FancyValidator):
-        messages = {
-            'perm_new_member_name':
-                _('This username or users group name is not valid')
-        }
-
-        def to_python(self, value, state):
-            perms_update = []
-            perms_new = []
-            # build a list of permission to update and new permission to create
-            for k, v in value.items():
-                # means new added member to permissions
-                if k.startswith('perm_new_member'):
-                    new_perm = value.get('perm_new_member', False)
-                    new_member = value.get('perm_new_member_name', False)
-                    new_type = value.get('perm_new_member_type')
-
-                    if new_member and new_perm:
-                        if (new_member, new_perm, new_type) not in perms_new:
-                            perms_new.append((new_member, new_perm, new_type))
-                elif k.startswith('u_perm_') or k.startswith('g_perm_'):
-                    member = k[7:]
-                    t = {'u': 'user',
-                         'g': 'users_group'
-                    }[k[0]]
-                    if member == 'default':
-                        if value.get('private'):
-                            # set none for default when updating to private repo
-                            v = EMPTY_PERM
-                    perms_update.append((member, v, t))
-
-            value['perms_updates'] = perms_update
-            value['perms_new'] = perms_new
-
-            # update permissions
-            for k, v, t in perms_new:
-                try:
-                    if t is 'user':
-                        self.user_db = User.query()\
-                            .filter(User.active == True)\
-                            .filter(User.username == k).one()
-                    if t is 'users_group':
-                        self.user_db = UsersGroup.query()\
-                            .filter(UsersGroup.users_group_active == True)\
-                            .filter(UsersGroup.users_group_name == k).one()
-
-                except Exception:
-                    msg = self.message('perm_new_member_name',
-                                         state=State_obj)
-                    raise formencode.Invalid(
-                        msg, value, state, error_dict={'perm_new_member_name': msg}
-                    )
-            return value
-    return _ValidPerms
-
-
-class ValidSettings(formencode.validators.FancyValidator):
-
-    def to_python(self, value, state):
-        # settings  form can't edit user
-        if 'user' in value:
-            del['value']['user']
-        return value
-
-
-class ValidPath(formencode.validators.FancyValidator):
-    def to_python(self, value, state):
-
-        if not os.path.isdir(value):
-            msg = _('This is not a valid path')
-            raise formencode.Invalid(msg, value, state,
-                                     error_dict={'paths_root_path': msg})
-        return value
-
-
-def UniqSystemEmail(old_data):
-    class _UniqSystemEmail(formencode.validators.FancyValidator):
-        def to_python(self, value, state):
-            value = value.lower()
-            if (old_data.get('email') or '').lower() != value:
-                user = User.get_by_email(value, case_insensitive=True)
-                if user:
-                    raise formencode.Invalid(
-                        _("This e-mail address is already taken"), value, state
-                    )
-            return value
-
-    return _UniqSystemEmail
-
-
-class ValidSystemEmail(formencode.validators.FancyValidator):
-    def to_python(self, value, state):
-        value = value.lower()
-        user = User.get_by_email(value, case_insensitive=True)
-        if  user is None:
-            raise formencode.Invalid(
-                _("This e-mail address doesn't exist."), value, state
-            )
-
-        return value
-
-
-class LdapLibValidator(formencode.validators.FancyValidator):
-
-    def to_python(self, value, state):
-
-        try:
-            import ldap
-        except ImportError:
-            raise LdapImportError
-        return value
-
-
-class AttrLoginValidator(formencode.validators.FancyValidator):
-
-    def to_python(self, value, state):
-
-        if not value or not isinstance(value, (str, unicode)):
-            raise formencode.Invalid(
-                _("The LDAP Login attribute of the CN must be specified - "
-                  "this is the name of the attribute that is equivalent "
-                  "to 'username'"), value, state
-            )
-
-        return value
-
-
-#==============================================================================
-# FORMS
-#==============================================================================
 class LoginForm(formencode.Schema):
     allow_extra_fields = True
     filter_extra_fields = True
-    username = UnicodeString(
+    username = v.UnicodeString(
         strip=True,
         min=1,
         not_empty=True,
         messages={
-           'empty': _('Please enter a login'),
-           'tooShort': _('Enter a value %(min)i characters long or more')}
+           'empty': _(u'Please enter a login'),
+           'tooShort': _(u'Enter a value %(min)i characters long or more')}
     )
 
-    password = UnicodeString(
+    password = v.UnicodeString(
         strip=False,
         min=3,
         not_empty=True,
         messages={
-            'empty': _('Please enter a password'),
-            'tooShort': _('Enter %(min)i characters or more')}
+            'empty': _(u'Please enter a password'),
+            'tooShort': _(u'Enter %(min)i characters or more')}
     )
 
-    remember = StringBoolean(if_missing=False)
+    remember = v.StringBoolean(if_missing=False)
 
-    chained_validators = [ValidAuth]
+    chained_validators = [v.ValidAuth()]
 
 
 def UserForm(edit=False, old_data={}):
     class _UserForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        username = All(UnicodeString(strip=True, min=1, not_empty=True),
-                       ValidUsername(edit, old_data))
+        username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
+                       v.ValidUsername(edit, old_data))
         if edit:
-            new_password = All(UnicodeString(strip=False, min=6, not_empty=False))
-            password_confirmation = All(UnicodeString(strip=False, min=6,
-                                                      not_empty=False))
-            admin = StringBoolean(if_missing=False)
+            new_password = All(
+                v.UnicodeString(strip=False, min=6, not_empty=False)
+            )
+            password_confirmation = All(
+                v.ValidPassword(),
+                v.UnicodeString(strip=False, min=6, not_empty=False),
+            )
+            admin = v.StringBoolean(if_missing=False)
         else:
-            password = All(UnicodeString(strip=False, min=6, not_empty=True))
-            password_confirmation = All(UnicodeString(strip=False, min=6,
-                                                      not_empty=False))
+            password = All(
+                v.ValidPassword(),
+                v.UnicodeString(strip=False, min=6, not_empty=True)
+            )
+            password_confirmation = All(
+                v.ValidPassword(),
+                v.UnicodeString(strip=False, min=6, not_empty=False)
+            )
 
-        active = StringBoolean(if_missing=False)
-        name = UnicodeString(strip=True, min=1, not_empty=False)
-        lastname = UnicodeString(strip=True, min=1, not_empty=False)
-        email = All(Email(not_empty=True), UniqSystemEmail(old_data))
+        active = v.StringBoolean(if_missing=False)
+        name = v.UnicodeString(strip=True, min=1, not_empty=False)
+        lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
+        email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
 
-        chained_validators = [ValidPasswordsMatch, ValidPassword]
+        chained_validators = [v.ValidPasswordsMatch()]
 
     return _UserForm
 
@@ -595,15 +98,18 @@
         allow_extra_fields = True
         filter_extra_fields = True
 
-        users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
-                       ValidUsersGroup(edit, old_data))
+        users_group_name = All(
+            v.UnicodeString(strip=True, min=1, not_empty=True),
+            v.ValidUsersGroup(edit, old_data)
+        )
 
-        users_group_active = StringBoolean(if_missing=False)
+        users_group_active = v.StringBoolean(if_missing=False)
 
         if edit:
-            users_group_members = OneOf(available_members, hideList=False,
-                                        testValueList=True,
-                                        if_missing=None, not_empty=False)
+            users_group_members = v.OneOf(
+                available_members, hideList=False, testValueList=True,
+                if_missing=None, not_empty=False
+            )
 
     return _UsersGroupForm
 
@@ -613,15 +119,16 @@
         allow_extra_fields = True
         filter_extra_fields = False
 
-        group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
-                               SlugifyName())
-        group_description = UnicodeString(strip=True, min=1,
+        group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
+                               v.SlugifyName())
+        group_description = v.UnicodeString(strip=True, min=1,
                                                 not_empty=True)
-        group_parent_id = OneOf(available_groups, hideList=False,
+        group_parent_id = v.OneOf(available_groups, hideList=False,
                                         testValueList=True,
                                         if_missing=None, not_empty=False)
 
-        chained_validators = [ValidReposGroup(edit, old_data), ValidPerms('group')]
+        chained_validators = [v.ValidReposGroup(edit, old_data),
+                              v.ValidPerms('group')]
 
     return _ReposGroupForm
 
@@ -630,16 +137,24 @@
     class _RegisterForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        username = All(ValidUsername(edit, old_data),
-                       UnicodeString(strip=True, min=1, not_empty=True))
-        password = All(UnicodeString(strip=False, min=6, not_empty=True))
-        password_confirmation = All(UnicodeString(strip=False, min=6, not_empty=True))
-        active = StringBoolean(if_missing=False)
-        name = UnicodeString(strip=True, min=1, not_empty=False)
-        lastname = UnicodeString(strip=True, min=1, not_empty=False)
-        email = All(Email(not_empty=True), UniqSystemEmail(old_data))
+        username = All(
+            v.ValidUsername(edit, old_data),
+            v.UnicodeString(strip=True, min=1, not_empty=True)
+        )
+        password = All(
+            v.ValidPassword(),
+            v.UnicodeString(strip=False, min=6, not_empty=True)
+        )
+        password_confirmation = All(
+            v.ValidPassword(),
+            v.UnicodeString(strip=False, min=6, not_empty=True)
+        )
+        active = v.StringBoolean(if_missing=False)
+        name = v.UnicodeString(strip=True, min=1, not_empty=False)
+        lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
+        email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
 
-        chained_validators = [ValidPasswordsMatch, ValidPassword]
+        chained_validators = [v.ValidPasswordsMatch()]
 
     return _RegisterForm
 
@@ -648,7 +163,7 @@
     class _PasswordResetForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        email = All(ValidSystemEmail(), Email(not_empty=True))
+        email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
     return _PasswordResetForm
 
 
@@ -657,24 +172,24 @@
     class _RepoForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
-        repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
-                        SlugifyName())
-        clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False))
-        repo_group = OneOf(repo_groups, hideList=True)
-        repo_type = OneOf(supported_backends)
-        description = UnicodeString(strip=True, min=1, not_empty=False)
-        private = StringBoolean(if_missing=False)
-        enable_statistics = StringBoolean(if_missing=False)
-        enable_downloads = StringBoolean(if_missing=False)
-        landing_rev = OneOf(landing_revs, hideList=True)
+        repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
+                        v.SlugifyName())
+        clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
+        repo_group = v.OneOf(repo_groups, hideList=True)
+        repo_type = v.OneOf(supported_backends)
+        description = v.UnicodeString(strip=True, min=1, not_empty=False)
+        private = v.StringBoolean(if_missing=False)
+        enable_statistics = v.StringBoolean(if_missing=False)
+        enable_downloads = v.StringBoolean(if_missing=False)
+        landing_rev = v.OneOf(landing_revs, hideList=True)
 
         if edit:
             #this is repo owner
-            user = All(UnicodeString(not_empty=True), ValidRepoUser)
+            user = All(v.UnicodeString(not_empty=True), v.ValidRepoUser())
 
-        chained_validators = [ValidCloneUri()(),
-                              ValidRepoName(edit, old_data),
-                              ValidPerms()]
+        chained_validators = [v.ValidCloneUri(),
+                              v.ValidRepoName(edit, old_data),
+                              v.ValidPerms()]
     return _RepoForm
 
 
@@ -683,33 +198,34 @@
     class _RepoForkForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
-        repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
-                        SlugifyName())
-        repo_group = OneOf(repo_groups, hideList=True)
-        repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
-        description = UnicodeString(strip=True, min=1, not_empty=True)
-        private = StringBoolean(if_missing=False)
-        copy_permissions = StringBoolean(if_missing=False)
-        update_after_clone = StringBoolean(if_missing=False)
-        fork_parent_id = UnicodeString()
-        chained_validators = [ValidForkName(edit, old_data)]
+        repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
+                        v.SlugifyName())
+        repo_group = v.OneOf(repo_groups, hideList=True)
+        repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
+        description = v.UnicodeString(strip=True, min=1, not_empty=True)
+        private = v.StringBoolean(if_missing=False)
+        copy_permissions = v.StringBoolean(if_missing=False)
+        update_after_clone = v.StringBoolean(if_missing=False)
+        fork_parent_id = v.UnicodeString()
+        chained_validators = [v.ValidForkName(edit, old_data)]
 
     return _RepoForkForm
 
 
-def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
-                     repo_groups=[], landing_revs=[]):
+def RepoSettingsForm(edit=False, old_data={},
+                     supported_backends=BACKENDS.keys(), repo_groups=[],
+                     landing_revs=[]):
     class _RepoForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
-        repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
-                        SlugifyName())
-        description = UnicodeString(strip=True, min=1, not_empty=True)
-        repo_group = OneOf(repo_groups, hideList=True)
-        private = StringBoolean(if_missing=False)
-        landing_rev = OneOf(landing_revs, hideList=True)
-        chained_validators = [ValidRepoName(edit, old_data), ValidPerms(),
-                              ValidSettings]
+        repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
+                        v.SlugifyName())
+        description = v.UnicodeString(strip=True, min=1, not_empty=True)
+        repo_group = v.OneOf(repo_groups, hideList=True)
+        private = v.StringBoolean(if_missing=False)
+        landing_rev = v.OneOf(landing_revs, hideList=True)
+        chained_validators = [v.ValidRepoName(edit, old_data), v.ValidPerms(),
+                              v.ValidSettings()]
     return _RepoForm
 
 
@@ -717,9 +233,9 @@
     class _ApplicationSettingsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
-        rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
-        rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
-        rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
+        rhodecode_title = v.UnicodeString(strip=True, min=1, not_empty=True)
+        rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
+        rhodecode_ga_code = v.UnicodeString(strip=True, min=1, not_empty=False)
 
     return _ApplicationSettingsForm
 
@@ -728,12 +244,19 @@
     class _ApplicationUiSettingsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
-        web_push_ssl = OneOf(['true', 'false'], if_missing='false')
-        paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
-        hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
-        hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
-        hooks_changegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
-        hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
+        web_push_ssl = v.OneOf(['true', 'false'], if_missing='false')
+        paths_root_path = All(
+            v.ValidPath(),
+            v.UnicodeString(strip=True, min=1, not_empty=True)
+        )
+        hooks_changegroup_update = v.OneOf(['True', 'False'],
+                                           if_missing=False)
+        hooks_changegroup_repo_size = v.OneOf(['True', 'False'],
+                                              if_missing=False)
+        hooks_changegroup_push_logger = v.OneOf(['True', 'False'],
+                                                if_missing=False)
+        hooks_preoutgoing_pull_logger = v.OneOf(['True', 'False'],
+                                                if_missing=False)
 
     return _ApplicationUiSettingsForm
 
@@ -742,33 +265,37 @@
     class _DefaultPermissionsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        overwrite_default = StringBoolean(if_missing=False)
-        anonymous = OneOf(['True', 'False'], if_missing=False)
-        default_perm = OneOf(perms_choices)
-        default_register = OneOf(register_choices)
-        default_create = OneOf(create_choices)
+        overwrite_default = v.StringBoolean(if_missing=False)
+        anonymous = v.OneOf(['True', 'False'], if_missing=False)
+        default_perm = v.OneOf(perms_choices)
+        default_register = v.OneOf(register_choices)
+        default_create = v.OneOf(create_choices)
 
     return _DefaultPermissionsForm
 
 
-def LdapSettingsForm(tls_reqcert_choices, search_scope_choices, tls_kind_choices):
+def LdapSettingsForm(tls_reqcert_choices, search_scope_choices,
+                     tls_kind_choices):
     class _LdapSettingsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
         #pre_validators = [LdapLibValidator]
-        ldap_active = StringBoolean(if_missing=False)
-        ldap_host = UnicodeString(strip=True,)
-        ldap_port = Number(strip=True,)
-        ldap_tls_kind = OneOf(tls_kind_choices)
-        ldap_tls_reqcert = OneOf(tls_reqcert_choices)
-        ldap_dn_user = UnicodeString(strip=True,)
-        ldap_dn_pass = UnicodeString(strip=True,)
-        ldap_base_dn = UnicodeString(strip=True,)
-        ldap_filter = UnicodeString(strip=True,)
-        ldap_search_scope = OneOf(search_scope_choices)
-        ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
-        ldap_attr_firstname = UnicodeString(strip=True,)
-        ldap_attr_lastname = UnicodeString(strip=True,)
-        ldap_attr_email = UnicodeString(strip=True,)
+        ldap_active = v.StringBoolean(if_missing=False)
+        ldap_host = v.UnicodeString(strip=True,)
+        ldap_port = v.Number(strip=True,)
+        ldap_tls_kind = v.OneOf(tls_kind_choices)
+        ldap_tls_reqcert = v.OneOf(tls_reqcert_choices)
+        ldap_dn_user = v.UnicodeString(strip=True,)
+        ldap_dn_pass = v.UnicodeString(strip=True,)
+        ldap_base_dn = v.UnicodeString(strip=True,)
+        ldap_filter = v.UnicodeString(strip=True,)
+        ldap_search_scope = v.OneOf(search_scope_choices)
+        ldap_attr_login = All(
+            v.AttrLoginValidator(),
+            v.UnicodeString(strip=True,)
+        )
+        ldap_attr_firstname = v.UnicodeString(strip=True,)
+        ldap_attr_lastname = v.UnicodeString(strip=True,)
+        ldap_attr_email = v.UnicodeString(strip=True,)
 
     return _LdapSettingsForm
--- a/rhodecode/model/notification.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/notification.py	Mon Jun 18 00:35:13 2012 +0200
@@ -35,15 +35,13 @@
 from rhodecode.lib import helpers as h
 from rhodecode.model import BaseModel
 from rhodecode.model.db import Notification, User, UserNotification
+from sqlalchemy.orm import joinedload
 
 log = logging.getLogger(__name__)
 
 
 class NotificationModel(BaseModel):
 
-    def __get_user(self, user):
-        return self._get_instance(User, user, callback=User.get_by_username)
-
     def __get_notification(self, notification):
         if isinstance(notification, Notification):
             return notification
@@ -76,12 +74,12 @@
         if recipients and not getattr(recipients, '__iter__', False):
             raise Exception('recipients must be a list of iterable')
 
-        created_by_obj = self.__get_user(created_by)
+        created_by_obj = self._get_user(created_by)
 
         if recipients:
             recipients_objs = []
             for u in recipients:
-                obj = self.__get_user(u)
+                obj = self._get_user(u)
                 if obj:
                     recipients_objs.append(obj)
             recipients_objs = set(recipients_objs)
@@ -110,6 +108,7 @@
             email_subject = NotificationModel().make_description(notif, False)
             type_ = type_
             email_body = body
+            ## this is passed into template
             kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
             kwargs.update(email_kwargs)
             email_body_html = EmailNotificationModel()\
@@ -124,7 +123,7 @@
         # we don't want to remove actual notification just the assignment
         try:
             notification = self.__get_notification(notification)
-            user = self.__get_user(user)
+            user = self._get_user(user)
             if notification and user:
                 obj = UserNotification.query()\
                         .filter(UserNotification.user == user)\
@@ -137,30 +136,56 @@
             log.error(traceback.format_exc())
             raise
 
-    def get_for_user(self, user):
-        user = self.__get_user(user)
-        return user.notifications
+    def get_for_user(self, user, filter_=None):
+        """
+        Get mentions for given user, filter them if filter dict is given
+
+        :param user:
+        :type user:
+        :param filter:
+        """
+        user = self._get_user(user)
+
+        q = UserNotification.query()\
+            .filter(UserNotification.user == user)\
+            .join((Notification, UserNotification.notification_id ==
+                                 Notification.notification_id))
+
+        if filter_:
+            q = q.filter(Notification.type_ == filter_.get('type'))
 
-    def mark_all_read_for_user(self, user):
-        user = self.__get_user(user)
-        UserNotification.query()\
-            .filter(UserNotification.read==False)\
-            .update({'read': True})
+        return q.all()
+
+    def mark_all_read_for_user(self, user, filter_=None):
+        user = self._get_user(user)
+        q = UserNotification.query()\
+            .filter(UserNotification.user == user)\
+            .filter(UserNotification.read == False)\
+            .join((Notification, UserNotification.notification_id ==
+                                 Notification.notification_id))
+        if filter_:
+            q = q.filter(Notification.type_ == filter_.get('type'))
+
+        # this is a little inefficient but sqlalchemy doesn't support
+        # update on joined tables :(
+        for obj in q.all():
+            obj.read = True
+            self.sa.add(obj)
 
     def get_unread_cnt_for_user(self, user):
-        user = self.__get_user(user)
+        user = self._get_user(user)
         return UserNotification.query()\
                 .filter(UserNotification.read == False)\
                 .filter(UserNotification.user == user).count()
 
     def get_unread_for_user(self, user):
-        user = self.__get_user(user)
+        user = self._get_user(user)
         return [x.notification for x in UserNotification.query()\
                 .filter(UserNotification.read == False)\
                 .filter(UserNotification.user == user).all()]
 
     def get_user_notification(self, user, notification):
-        user = self.__get_user(user)
+        user = self._get_user(user)
         notification = self.__get_notification(notification)
 
         return UserNotification.query()\
@@ -172,12 +197,15 @@
         Creates a human readable description based on properties
         of notification object
         """
-
+        #alias
+        _n = notification
         _map = {
-            notification.TYPE_CHANGESET_COMMENT: _('commented on commit'),
-            notification.TYPE_MESSAGE: _('sent message'),
-            notification.TYPE_MENTION: _('mentioned you'),
-            notification.TYPE_REGISTRATION: _('registered in RhodeCode')
+            _n.TYPE_CHANGESET_COMMENT: _('commented on commit'),
+            _n.TYPE_MESSAGE: _('sent message'),
+            _n.TYPE_MENTION: _('mentioned you'),
+            _n.TYPE_REGISTRATION: _('registered in RhodeCode'),
+            _n.TYPE_PULL_REQUEST: _('opened new pull request'),
+            _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request')
         }
 
         # action == _map string
@@ -199,6 +227,7 @@
     TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
     TYPE_PASSWORD_RESET = 'passoword_link'
     TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
+    TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
     TYPE_DEFAULT = 'default'
 
     def __init__(self):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/model/pull_request.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.model.pull_reuquest
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    pull request model for RhodeCode
+
+    :created_on: Jun 6, 2012
+    :author: marcink
+    :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import binascii
+from pylons.i18n.translation import _
+
+from rhodecode.lib import helpers as h
+from rhodecode.model import BaseModel
+from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
+from rhodecode.model.notification import NotificationModel
+from rhodecode.lib.utils2 import safe_unicode
+
+from rhodecode.lib.vcs.utils.hgcompat import discovery
+
+log = logging.getLogger(__name__)
+
+
+class PullRequestModel(BaseModel):
+
+    def get_all(self, repo):
+        repo = self._get_repo(repo)
+        return PullRequest.query().filter(PullRequest.other_repo == repo).all()
+
+    def create(self, created_by, org_repo, org_ref, other_repo,
+               other_ref, revisions, reviewers, title, description=None):
+        created_by_user = self._get_user(created_by)
+
+        new = PullRequest()
+        new.org_repo = self._get_repo(org_repo)
+        new.org_ref = org_ref
+        new.other_repo = self._get_repo(other_repo)
+        new.other_ref = other_ref
+        new.revisions = revisions
+        new.title = title
+        new.description = description
+        new.author = created_by_user
+        self.sa.add(new)
+
+        #members
+        for member in reviewers:
+            _usr = self._get_user(member)
+            reviewer = PullRequestReviewers(_usr, new)
+            self.sa.add(reviewer)
+
+        #notification to reviewers
+        notif = NotificationModel()
+
+        subject = safe_unicode(
+            h.link_to(
+              _('%(user)s wants you to review pull request #%(pr_id)s') % \
+                {'user': created_by_user.username,
+                 'pr_id': new.pull_request_id},
+              h.url('pullrequest_show', repo_name=other_repo,
+                    pull_request_id=new.pull_request_id,
+                    qualified=True,
+              )
+            )
+        )
+        body = description
+        notif.create(created_by=created_by, subject=subject, body=body,
+                     recipients=reviewers,
+                     type_=Notification.TYPE_PULL_REQUEST,)
+
+        return new
+
+    def _get_changesets(self, org_repo, org_ref, other_repo, other_ref,
+                        discovery_data):
+        """
+        Returns a list of changesets that are incoming from org_repo@org_ref
+        to other_repo@other_ref
+
+        :param org_repo:
+        :type org_repo:
+        :param org_ref:
+        :type org_ref:
+        :param other_repo:
+        :type other_repo:
+        :param other_ref:
+        :type other_ref:
+        :param tmp:
+        :type tmp:
+        """
+        changesets = []
+        #case two independent repos
+        if org_repo != other_repo:
+            common, incoming, rheads = discovery_data
+
+            if not incoming:
+                revs = []
+            else:
+                revs = org_repo._repo.changelog.findmissing(common, rheads)
+
+            for cs in reversed(map(binascii.hexlify, revs)):
+                changesets.append(org_repo.get_changeset(cs))
+        else:
+            revs = ['ancestors(%s) and not ancestors(%s)' % (org_ref[1],
+                                                             other_ref[1])]
+            from mercurial import scmutil
+            out = scmutil.revrange(org_repo._repo, revs)
+            for cs in reversed(out):
+                changesets.append(org_repo.get_changeset(cs))
+
+        return changesets
+
+    def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
+        """
+        Get's mercurial discovery data used to calculate difference between
+        repos and refs
+
+        :param org_repo:
+        :type org_repo:
+        :param org_ref:
+        :type org_ref:
+        :param other_repo:
+        :type other_repo:
+        :param other_ref:
+        :type other_ref:
+        """
+
+        other = org_repo._repo
+        repo = other_repo._repo
+        tip = other[org_ref[1]]
+        log.debug('Doing discovery for %s@%s vs %s@%s' % (
+                        org_repo, org_ref, other_repo, other_ref)
+        )
+        log.debug('Filter heads are %s[%s]' % (tip, org_ref[1]))
+        tmp = discovery.findcommonincoming(
+                  repo=repo,  # other_repo we check for incoming
+                  remote=other,  # org_repo source for incoming
+                  heads=[tip.node()],
+                  force=False
+        )
+        return tmp
+
+    def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
+        """
+        Returns a tuple of incomming changesets, and discoverydata cache
+
+        :param org_repo:
+        :type org_repo:
+        :param org_ref:
+        :type org_ref:
+        :param other_repo:
+        :type other_repo:
+        :param other_ref:
+        :type other_ref:
+        """
+
+        if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
+            raise Exception('org_ref must be a two element list/tuple')
+
+        if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
+            raise Exception('other_ref must be a two element list/tuple')
+
+        discovery_data = self._get_discovery(org_repo.scm_instance,
+                                           org_ref,
+                                           other_repo.scm_instance,
+                                           other_ref)
+        cs_ranges = self._get_changesets(org_repo.scm_instance,
+                                           org_ref,
+                                           other_repo.scm_instance,
+                                           other_ref,
+                                           discovery_data)
+        return cs_ranges, discovery_data
--- a/rhodecode/model/repo.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/repo.py	Mon Jun 18 00:35:13 2012 +0200
@@ -48,9 +48,6 @@
 
 class RepoModel(BaseModel):
 
-    def __get_user(self, user):
-        return self._get_instance(User, user, callback=User.get_by_username)
-
     def __get_users_group(self, users_group):
         return self._get_instance(UsersGroup, users_group,
                                   callback=UsersGroup.get_by_group_name)
@@ -59,14 +56,6 @@
         return self._get_instance(RepoGroup, repos_group,
                                   callback=RepoGroup.get_by_group_name)
 
-    def __get_repo(self, repository):
-        return self._get_instance(Repository, repository,
-                                  callback=Repository.get_by_repo_name)
-
-    def __get_perm(self, permission):
-        return self._get_instance(Permission, permission,
-                                  callback=Permission.get_by_key)
-
     @LazyProperty
     def repos_path(self):
         """
@@ -86,7 +75,7 @@
         return repo.scalar()
 
     def get_repo(self, repository):
-        return self.__get_repo(repository)
+        return self._get_repo(repository)
 
     def get_by_repo_name(self, repo_name, cache=False):
         repo = self.sa.query(Repository)\
@@ -311,7 +300,7 @@
         run_task(tasks.create_repo_fork, form_data, cur_user)
 
     def delete(self, repo):
-        repo = self.__get_repo(repo)
+        repo = self._get_repo(repo)
         try:
             self.sa.delete(repo)
             self.__delete_repo(repo)
@@ -328,9 +317,9 @@
         :param user: Instance of User, user_id or username
         :param perm: Instance of Permission, or permission_name
         """
-        user = self.__get_user(user)
-        repo = self.__get_repo(repo)
-        permission = self.__get_perm(perm)
+        user = self._get_user(user)
+        repo = self._get_repo(repo)
+        permission = self._get_perm(perm)
 
         # check if we have that permission already
         obj = self.sa.query(UserRepoToPerm)\
@@ -353,8 +342,8 @@
         :param user: Instance of User, user_id or username
         """
 
-        user = self.__get_user(user)
-        repo = self.__get_repo(repo)
+        user = self._get_user(user)
+        repo = self._get_repo(repo)
 
         obj = self.sa.query(UserRepoToPerm)\
             .filter(UserRepoToPerm.repository == repo)\
@@ -372,9 +361,9 @@
             or users group name
         :param perm: Instance of Permission, or permission_name
         """
-        repo = self.__get_repo(repo)
+        repo = self._get_repo(repo)
         group_name = self.__get_users_group(group_name)
-        permission = self.__get_perm(perm)
+        permission = self._get_perm(perm)
 
         # check if we have that permission already
         obj = self.sa.query(UsersGroupRepoToPerm)\
@@ -399,7 +388,7 @@
         :param group_name: Instance of UserGroup, users_group_id,
             or users group name
         """
-        repo = self.__get_repo(repo)
+        repo = self._get_repo(repo)
         group_name = self.__get_users_group(group_name)
 
         obj = self.sa.query(UsersGroupRepoToPerm)\
--- a/rhodecode/model/repo_permission.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/repo_permission.py	Mon Jun 18 00:35:13 2012 +0200
@@ -26,28 +26,17 @@
 
 import logging
 from rhodecode.model import BaseModel
-from rhodecode.model.db import UserRepoToPerm, UsersGroupRepoToPerm, Permission,\
-    User, Repository
+from rhodecode.model.db import UserRepoToPerm, UsersGroupRepoToPerm, \
+    Permission
 
 log = logging.getLogger(__name__)
 
 
 class RepositoryPermissionModel(BaseModel):
 
-    def __get_user(self, user):
-        return self._get_instance(User, user, callback=User.get_by_username)
-
-    def __get_repo(self, repository):
-        return self._get_instance(Repository, repository,
-                                  callback=Repository.get_by_repo_name)
-
-    def __get_perm(self, permission):
-        return self._get_instance(Permission, permission,
-                                  callback=Permission.get_by_key)
-
     def get_user_permission(self, repository, user):
-        repository = self.__get_repo(repository)
-        user = self.__get_user(user)
+        repository = self._get_repo(repository)
+        user = self._get_user(user)
 
         return UserRepoToPerm.query() \
                 .filter(UserRepoToPerm.user == user) \
--- a/rhodecode/model/repos_group.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/repos_group.py	Mon Jun 18 00:35:13 2012 +0200
@@ -39,9 +39,6 @@
 
 class ReposGroupModel(BaseModel):
 
-    def __get_user(self, user):
-        return self._get_instance(User, user, callback=User.get_by_username)
-
     def __get_users_group(self, users_group):
         return self._get_instance(UsersGroup, users_group,
                                   callback=UsersGroup.get_by_group_name)
@@ -50,10 +47,6 @@
         return self._get_instance(RepoGroup, repos_group,
                                   callback=RepoGroup.get_by_group_name)
 
-    def __get_perm(self, permission):
-        return self._get_instance(Permission, permission,
-                                  callback=Permission.get_by_key)
-
     @LazyProperty
     def repos_path(self):
         """
@@ -206,13 +199,13 @@
             log.error(traceback.format_exc())
             raise
 
-    def delete(self, users_group_id):
+    def delete(self, repos_group):
+        repos_group = self.__get_repos_group(repos_group)
         try:
-            users_group = RepoGroup.get(users_group_id)
-            self.sa.delete(users_group)
-            self.__delete_group(users_group)
+            self.sa.delete(repos_group)
+            self.__delete_group(repos_group)
         except:
-            log.error(traceback.format_exc())
+            log.exception('Error removing repos_group %s' % repos_group)
             raise
 
     def grant_user_permission(self, repos_group, user, perm):
@@ -227,8 +220,8 @@
         """
 
         repos_group = self.__get_repos_group(repos_group)
-        user = self.__get_user(user)
-        permission = self.__get_perm(perm)
+        user = self._get_user(user)
+        permission = self._get_perm(perm)
 
         # check if we have that permission already
         obj = self.sa.query(UserRepoGroupToPerm)\
@@ -253,7 +246,7 @@
         """
 
         repos_group = self.__get_repos_group(repos_group)
-        user = self.__get_user(user)
+        user = self._get_user(user)
 
         obj = self.sa.query(UserRepoGroupToPerm)\
             .filter(UserRepoGroupToPerm.user == user)\
@@ -274,7 +267,7 @@
         """
         repos_group = self.__get_repos_group(repos_group)
         group_name = self.__get_users_group(group_name)
-        permission = self.__get_perm(perm)
+        permission = self._get_perm(perm)
 
         # check if we have that permission already
         obj = self.sa.query(UsersGroupRepoGroupToPerm)\
--- a/rhodecode/model/scm.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/scm.py	Mon Jun 18 00:35:13 2012 +0200
@@ -44,7 +44,7 @@
     action_logger, EmptyChangeset, REMOVED_REPO_PAT
 from rhodecode.model import BaseModel
 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
-    UserFollowing, UserLog, User, RepoGroup
+    UserFollowing, UserLog, User, RepoGroup, PullRequest
 
 log = logging.getLogger(__name__)
 
@@ -321,19 +321,21 @@
 
         return f is not None
 
-    def get_followers(self, repo_id):
-        if not isinstance(repo_id, int):
-            repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
+    def get_followers(self, repo):
+        repo = self._get_repo(repo)
 
         return self.sa.query(UserFollowing)\
-                .filter(UserFollowing.follows_repo_id == repo_id).count()
+                .filter(UserFollowing.follows_repository == repo).count()
 
-    def get_forks(self, repo_id):
-        if not isinstance(repo_id, int):
-            repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
+    def get_forks(self, repo):
+        repo = self._get_repo(repo)
+        return self.sa.query(Repository)\
+                .filter(Repository.fork == repo).count()
 
-        return self.sa.query(Repository)\
-                .filter(Repository.fork_id == repo_id).count()
+    def get_pull_requests(self, repo):
+        repo = self._get_repo(repo)
+        return self.sa.query(PullRequest)\
+                .filter(PullRequest.other_repo == repo).count()
 
     def mark_as_fork(self, repo, fork, user):
         repo = self.__get_repo(repo)
--- a/rhodecode/model/user.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/user.py	Mon Jun 18 00:35:13 2012 +0200
@@ -35,8 +35,8 @@
 from rhodecode.model import BaseModel
 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
     UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
-    Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\
-    UsersGroupRepoGroupToPerm
+    Notification, RepoGroup, UserRepoGroupToPerm, UsersGroupRepoGroupToPerm, \
+    UserEmailMap
 from rhodecode.lib.exceptions import DefaultUserException, \
     UserOwnsReposException
 
@@ -61,13 +61,6 @@
 
 class UserModel(BaseModel):
 
-    def __get_user(self, user):
-        return self._get_instance(User, user, callback=User.get_by_username)
-
-    def __get_perm(self, permission):
-        return self._get_instance(Permission, permission,
-                                  callback=Permission.get_by_key)
-
     def get(self, user_id, cache=False):
         user = self.sa.query(User)
         if cache:
@@ -76,7 +69,7 @@
         return user.get(user_id)
 
     def get_user(self, user):
-        return self.__get_user(user)
+        return self._get_user(user)
 
     def get_by_username(self, username, cache=False, case_insensitive=False):
 
@@ -94,9 +87,12 @@
         return User.get_by_api_key(api_key, cache)
 
     def create(self, form_data):
+        from rhodecode.lib.auth import get_crypt_password
         try:
             new_user = User()
             for k, v in form_data.items():
+                if k == 'password':
+                    v = get_crypt_password(v)
                 setattr(new_user, k, v)
 
             new_user.api_key = generate_api_key(form_data['username'])
@@ -272,15 +268,17 @@
             raise
 
     def update_my_account(self, user_id, form_data):
+        from rhodecode.lib.auth import get_crypt_password
         try:
             user = self.get(user_id, cache=False)
             if user.username == 'default':
                 raise DefaultUserException(
-                                _("You can't Edit this user since it's"
-                                  " crucial for entire application"))
+                    _("You can't Edit this user since it's"
+                      " crucial for entire application")
+                )
             for k, v in form_data.items():
                 if k == 'new_password' and v != '':
-                    user.password = v
+                    user.password = get_crypt_password(v)
                     user.api_key = generate_api_key(user.username)
                 else:
                     if k not in ['admin', 'active']:
@@ -292,7 +290,7 @@
             raise
 
     def delete(self, user):
-        user = self.__get_user(user)
+        user = self._get_user(user)
 
         try:
             if user.username == 'default':
@@ -545,7 +543,7 @@
             raise Exception('perm needs to be an instance of Permission class '
                             'got %s instead' % type(perm))
 
-        user = self.__get_user(user)
+        user = self._get_user(user)
 
         return UserToPerm.query().filter(UserToPerm.user == user)\
             .filter(UserToPerm.permission == perm).scalar() is not None
@@ -557,8 +555,8 @@
         :param user:
         :param perm:
         """
-        user = self.__get_user(user)
-        perm = self.__get_perm(perm)
+        user = self._get_user(user)
+        perm = self._get_perm(perm)
         # if this permission is already granted skip it
         _perm = UserToPerm.query()\
             .filter(UserToPerm.user == user)\
@@ -578,8 +576,8 @@
         :param user:
         :param perm:
         """
-        user = self.__get_user(user)
-        perm = self.__get_perm(perm)
+        user = self._get_user(user)
+        perm = self._get_perm(perm)
 
         obj = UserToPerm.query()\
                 .filter(UserToPerm.user == user)\
@@ -587,3 +585,29 @@
                 .scalar()
         if obj:
             self.sa.delete(obj)
+
+    def add_extra_email(self, user, email):
+        """
+        Adds email address to UserEmailMap
+
+        :param user:
+        :param email:
+        """
+        user = self._get_user(user)
+        obj = UserEmailMap()
+        obj.user = user
+        obj.email = email
+        self.sa.add(obj)
+        return obj
+
+    def delete_extra_email(self, user, email_id):
+        """
+        Removes email address from UserEmailMap
+
+        :param user:
+        :param email_id:
+        """
+        user = self._get_user(user)
+        obj = UserEmailMap.query().get(email_id)
+        if obj:
+            self.sa.delete(obj)
\ No newline at end of file
--- a/rhodecode/model/users_group.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/model/users_group.py	Mon Jun 18 00:35:13 2012 +0200
@@ -37,17 +37,10 @@
 
 class UsersGroupModel(BaseModel):
 
-    def __get_user(self, user):
-        return self._get_instance(User, user, callback=User.get_by_username)
-
     def __get_users_group(self, users_group):
         return self._get_instance(UsersGroup, users_group,
                                   callback=UsersGroup.get_by_group_name)
 
-    def __get_perm(self, permission):
-        return self._get_instance(Permission, permission,
-                                  callback=Permission.get_by_key)
-
     def get(self, users_group_id, cache=False):
         return UsersGroup.get(users_group_id)
 
@@ -115,7 +108,7 @@
 
     def add_user_to_group(self, users_group, user):
         users_group = self.__get_users_group(users_group)
-        user = self.__get_user(user)
+        user = self._get_user(user)
 
         for m in users_group.members:
             u = m.user
@@ -138,7 +131,7 @@
 
     def remove_user_from_group(self, users_group, user):
         users_group = self.__get_users_group(users_group)
-        user = self.__get_user(user)
+        user = self._get_user(user)
 
         users_group_member = None
         for m in users_group.members:
@@ -160,7 +153,7 @@
 
     def has_perm(self, users_group, perm):
         users_group = self.__get_users_group(users_group)
-        perm = self.__get_perm(perm)
+        perm = self._get_perm(perm)
 
         return UsersGroupToPerm.query()\
             .filter(UsersGroupToPerm.users_group == users_group)\
@@ -187,7 +180,7 @@
 
     def revoke_perm(self, users_group, perm):
         users_group = self.__get_users_group(users_group)
-        perm = self.__get_perm(perm)
+        perm = self._get_perm(perm)
 
         obj = UsersGroupToPerm.query()\
             .filter(UsersGroupToPerm.users_group == users_group)\
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/model/validators.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,592 @@
+"""
+Set of generic validators
+"""
+import os
+import re
+import formencode
+import logging
+from pylons.i18n.translation import _
+from webhelpers.pylonslib.secure_form import authentication_token
+
+from formencode.validators import (
+    UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set
+)
+
+from rhodecode.lib.utils import repo_name_slug
+from rhodecode.model.db import RepoGroup, Repository, UsersGroup, User
+from rhodecode.lib.auth import authenticate
+from rhodecode.lib.exceptions import LdapImportError
+from rhodecode.config.routing import ADMIN_PREFIX
+# silence warnings and pylint
+UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set
+
+log = logging.getLogger(__name__)
+
+
+class StateObj(object):
+    """
+    this is needed to translate the messages using _() in validators
+    """
+    _ = staticmethod(_)
+
+
+def M(self, key, state=None, **kwargs):
+    """
+    returns string from self.message based on given key,
+    passed kw params are used to substitute %(named)s params inside
+    translated strings
+
+    :param msg:
+    :param state:
+    """
+    if state is None:
+        state = StateObj()
+    else:
+        state._ = staticmethod(_)
+    #inject validator into state object
+    return self.message(key, state, **kwargs)
+
+
+def ValidUsername(edit=False, old_data={}):
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'username_exists': _(u'Username "%(username)s" already exists'),
+            'system_invalid_username':
+                _(u'Username "%(username)s" is forbidden'),
+            'invalid_username':
+                _(u'Username may only contain alphanumeric characters '
+                  'underscores, periods or dashes and must begin with '
+                  'alphanumeric character')
+        }
+
+        def validate_python(self, value, state):
+            if value in ['default', 'new_user']:
+                msg = M(self, 'system_invalid_username', state, username=value)
+                raise formencode.Invalid(msg, value, state)
+            #check if user is unique
+            old_un = None
+            if edit:
+                old_un = User.get(old_data.get('user_id')).username
+
+            if old_un != value or not edit:
+                if User.get_by_username(value, case_insensitive=True):
+                    msg = M(self, 'username_exists', state, username=value)
+                    raise formencode.Invalid(msg, value, state)
+
+            if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
+                msg = M(self, 'invalid_username', state)
+                raise formencode.Invalid(msg, value, state)
+    return _validator
+
+
+def ValidRepoUser():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_username': _(u'Username %(username)s is not valid')
+        }
+
+        def validate_python(self, value, state):
+            try:
+                User.query().filter(User.active == True)\
+                    .filter(User.username == value).one()
+            except Exception:
+                msg = M(self, 'invalid_username', state, username=value)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(username=msg)
+                )
+
+    return _validator
+
+
+def ValidUsersGroup(edit=False, old_data={}):
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_group': _(u'Invalid users group name'),
+            'group_exist': _(u'Users group "%(usersgroup)s" already exists'),
+            'invalid_usersgroup_name':
+                _(u'users group name may only contain  alphanumeric '
+                  'characters underscores, periods or dashes and must begin '
+                  'with alphanumeric character')
+        }
+
+        def validate_python(self, value, state):
+            if value in ['default']:
+                msg = M(self, 'invalid_group', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(users_group_name=msg)
+                )
+            #check if group is unique
+            old_ugname = None
+            if edit:
+                old_id = old_data.get('users_group_id')
+                old_ugname = UsersGroup.get(old_id).users_group_name
+
+            if old_ugname != value or not edit:
+                is_existing_group = UsersGroup.get_by_group_name(value,
+                                                        case_insensitive=True)
+                if is_existing_group:
+                    msg = M(self, 'group_exist', state, usersgroup=value)
+                    raise formencode.Invalid(msg, value, state,
+                        error_dict=dict(users_group_name=msg)
+                    )
+
+            if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
+                msg = M(self, 'invalid_usersgroup_name', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(users_group_name=msg)
+                )
+
+    return _validator
+
+
+def ValidReposGroup(edit=False, old_data={}):
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'group_parent_id': _(u'Cannot assign this group as parent'),
+            'group_exists': _(u'Group "%(group_name)s" already exists'),
+            'repo_exists':
+                _(u'Repository with name "%(group_name)s" already exists')
+        }
+
+        def validate_python(self, value, state):
+            # TODO WRITE VALIDATIONS
+            group_name = value.get('group_name')
+            group_parent_id = value.get('group_parent_id')
+
+            # slugify repo group just in case :)
+            slug = repo_name_slug(group_name)
+
+            # check for parent of self
+            parent_of_self = lambda: (
+                old_data['group_id'] == int(group_parent_id)
+                if group_parent_id else False
+            )
+            if edit and parent_of_self():
+                msg = M(self, 'group_parent_id', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(group_parent_id=msg)
+                )
+
+            old_gname = None
+            if edit:
+                old_gname = RepoGroup.get(old_data.get('group_id')).group_name
+
+            if old_gname != group_name or not edit:
+
+                # check group
+                gr = RepoGroup.query()\
+                      .filter(RepoGroup.group_name == slug)\
+                      .filter(RepoGroup.group_parent_id == group_parent_id)\
+                      .scalar()
+
+                if gr:
+                    msg = M(self, 'group_exists', state, group_name=slug)
+                    raise formencode.Invalid(msg, value, state,
+                            error_dict=dict(group_name=msg)
+                    )
+
+                # check for same repo
+                repo = Repository.query()\
+                      .filter(Repository.repo_name == slug)\
+                      .scalar()
+
+                if repo:
+                    msg = M(self, 'repo_exists', state, group_name=slug)
+                    raise formencode.Invalid(msg, value, state,
+                            error_dict=dict(group_name=msg)
+                    )
+
+    return _validator
+
+
+def ValidPassword():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_password':
+                _(u'Invalid characters (non-ascii) in password')
+        }
+
+        def validate_python(self, value, state):
+            try:
+                (value or '').decode('ascii')
+            except UnicodeError:
+                msg = M(self, 'invalid_password', state)
+                raise formencode.Invalid(msg, value, state,)
+    return _validator
+
+
+def ValidPasswordsMatch():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'password_mismatch': _(u'Passwords do not match'),
+        }
+
+        def validate_python(self, value, state):
+
+            pass_val = value.get('password') or value.get('new_password')
+            if pass_val != value['password_confirmation']:
+                msg = M(self, 'password_mismatch', state)
+                raise formencode.Invalid(msg, value, state,
+                     error_dict=dict(password_confirmation=msg)
+                )
+    return _validator
+
+
+def ValidAuth():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_password': _(u'invalid password'),
+            'invalid_username': _(u'invalid user name'),
+            'disabled_account': _(u'Your account is disabled')
+        }
+
+        def validate_python(self, value, state):
+            password = value['password']
+            username = value['username']
+
+            if not authenticate(username, password):
+                user = User.get_by_username(username)
+                if user and user.active is False:
+                    log.warning('user %s is disabled' % username)
+                    msg = M(self, 'disabled_account', state)
+                    raise formencode.Invalid(msg, value, state,
+                        error_dict=dict(username=msg)
+                    )
+                else:
+                    log.warning('user %s failed to authenticate' % username)
+                    msg = M(self, 'invalid_username', state)
+                    msg2 = M(self, 'invalid_password', state)
+                    raise formencode.Invalid(msg, value, state,
+                        error_dict=dict(username=msg, password=msg2)
+                    )
+    return _validator
+
+
+def ValidAuthToken():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_token': _(u'Token mismatch')
+        }
+
+        def validate_python(self, value, state):
+            if value != authentication_token():
+                msg = M(self, 'invalid_token', state)
+                raise formencode.Invalid(msg, value, state)
+    return _validator
+
+
+def ValidRepoName(edit=False, old_data={}):
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_repo_name':
+                _(u'Repository name %(repo)s is disallowed'),
+            'repository_exists':
+                _(u'Repository named %(repo)s already exists'),
+            'repository_in_group_exists': _(u'Repository "%(repo)s" already '
+                                            'exists in group "%(group)s"'),
+            'same_group_exists': _(u'Repositories group with name "%(repo)s" '
+                                   'already exists')
+        }
+
+        def _to_python(self, value, state):
+            repo_name = repo_name_slug(value.get('repo_name', ''))
+            repo_group = value.get('repo_group')
+            if repo_group:
+                gr = RepoGroup.get(repo_group)
+                group_path = gr.full_path
+                group_name = gr.group_name
+                # value needs to be aware of group name in order to check
+                # db key This is an actual just the name to store in the
+                # database
+                repo_name_full = group_path + RepoGroup.url_sep() + repo_name
+            else:
+                group_name = group_path = ''
+                repo_name_full = repo_name
+
+            value['repo_name'] = repo_name
+            value['repo_name_full'] = repo_name_full
+            value['group_path'] = group_path
+            value['group_name'] = group_name
+            return value
+
+        def validate_python(self, value, state):
+
+            repo_name = value.get('repo_name')
+            repo_name_full = value.get('repo_name_full')
+            group_path = value.get('group_path')
+            group_name = value.get('group_name')
+
+            if repo_name in [ADMIN_PREFIX, '']:
+                msg = M(self, 'invalid_repo_name', state, repo=repo_name)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(repo_name=msg)
+                )
+
+            rename = old_data.get('repo_name') != repo_name_full
+            create = not edit
+            if rename or create:
+
+                if group_path != '':
+                    if Repository.get_by_repo_name(repo_name_full):
+                        msg = M(self, 'repository_in_group_exists', state,
+                                repo=repo_name, group=group_name)
+                        raise formencode.Invalid(msg, value, state,
+                            error_dict=dict(repo_name=msg)
+                        )
+                elif RepoGroup.get_by_group_name(repo_name_full):
+                        msg = M(self, 'same_group_exists', state,
+                                repo=repo_name)
+                        raise formencode.Invalid(msg, value, state,
+                            error_dict=dict(repo_name=msg)
+                        )
+
+                elif Repository.get_by_repo_name(repo_name_full):
+                        msg = M(self, 'repository_exists', state,
+                                repo=repo_name)
+                        raise formencode.Invalid(msg, value, state,
+                            error_dict=dict(repo_name=msg)
+                        )
+            return value
+    return _validator
+
+
+def ValidForkName(*args, **kwargs):
+    return ValidRepoName(*args, **kwargs)
+
+
+def SlugifyName():
+    class _validator(formencode.validators.FancyValidator):
+
+        def _to_python(self, value, state):
+            return repo_name_slug(value)
+
+        def validate_python(self, value, state):
+            pass
+
+    return _validator
+
+
+def ValidCloneUri():
+    from rhodecode.lib.utils import make_ui
+
+    def url_handler(repo_type, url, proto, ui=None):
+        if repo_type == 'hg':
+            from mercurial.httprepo import httprepository, httpsrepository
+            if proto == 'https':
+                httpsrepository(make_ui('db'), url).capabilities
+            elif proto == 'http':
+                httprepository(make_ui('db'), url).capabilities
+        elif repo_type == 'git':
+            #TODO: write a git url validator
+            pass
+
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'clone_uri': _(u'invalid clone url'),
+            'invalid_clone_uri': _(u'Invalid clone url, provide a '
+                                    'valid clone http\s url')
+        }
+
+        def validate_python(self, value, state):
+            repo_type = value.get('repo_type')
+            url = value.get('clone_uri')
+
+            if not url:
+                pass
+            elif url.startswith('https') or url.startswith('http'):
+                _type = 'https' if url.startswith('https') else 'http'
+                try:
+                    url_handler(repo_type, url, _type, make_ui('db'))
+                except Exception:
+                    log.exception('Url validation failed')
+                    msg = M(self, 'clone_uri')
+                    raise formencode.Invalid(msg, value, state,
+                        error_dict=dict(clone_uri=msg)
+                    )
+            else:
+                msg = M(self, 'invalid_clone_uri', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(clone_uri=msg)
+                )
+    return _validator
+
+
+def ValidForkType(old_data={}):
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_fork_type': _(u'Fork have to be the same type as parent')
+        }
+
+        def validate_python(self, value, state):
+            if old_data['repo_type'] != value:
+                msg = M(self, 'invalid_fork_type', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(repo_type=msg)
+                )
+    return _validator
+
+
+def ValidPerms(type_='repo'):
+    if type_ == 'group':
+        EMPTY_PERM = 'group.none'
+    elif type_ == 'repo':
+        EMPTY_PERM = 'repository.none'
+
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'perm_new_member_name':
+                _(u'This username or users group name is not valid')
+        }
+
+        def to_python(self, value, state):
+            perms_update = []
+            perms_new = []
+            # build a list of permission to update and new permission to create
+            for k, v in value.items():
+                # means new added member to permissions
+                if k.startswith('perm_new_member'):
+                    new_perm = value.get('perm_new_member', False)
+                    new_member = value.get('perm_new_member_name', False)
+                    new_type = value.get('perm_new_member_type')
+
+                    if new_member and new_perm:
+                        if (new_member, new_perm, new_type) not in perms_new:
+                            perms_new.append((new_member, new_perm, new_type))
+                elif k.startswith('u_perm_') or k.startswith('g_perm_'):
+                    member = k[7:]
+                    t = {'u': 'user',
+                         'g': 'users_group'
+                    }[k[0]]
+                    if member == 'default':
+                        if value.get('private'):
+                            # set none for default when updating to
+                            # private repo
+                            v = EMPTY_PERM
+                    perms_update.append((member, v, t))
+
+            value['perms_updates'] = perms_update
+            value['perms_new'] = perms_new
+
+            # update permissions
+            for k, v, t in perms_new:
+                try:
+                    if t is 'user':
+                        self.user_db = User.query()\
+                            .filter(User.active == True)\
+                            .filter(User.username == k).one()
+                    if t is 'users_group':
+                        self.user_db = UsersGroup.query()\
+                            .filter(UsersGroup.users_group_active == True)\
+                            .filter(UsersGroup.users_group_name == k).one()
+
+                except Exception:
+                    log.exception('Updated permission failed')
+                    msg = M(self, 'perm_new_member_type', state)
+                    raise formencode.Invalid(msg, value, state,
+                        error_dict=dict(perm_new_member_name=msg)
+                    )
+            return value
+    return _validator
+
+
+def ValidSettings():
+    class _validator(formencode.validators.FancyValidator):
+        def _to_python(self, value, state):
+            # settings  form can't edit user
+            if 'user' in value:
+                del value['user']
+            return value
+
+        def validate_python(self, value, state):
+            pass
+    return _validator
+
+
+def ValidPath():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_path': _(u'This is not a valid path')
+        }
+
+        def validate_python(self, value, state):
+            if not os.path.isdir(value):
+                msg = M(self, 'invalid_path', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(paths_root_path=msg)
+                )
+    return _validator
+
+
+def UniqSystemEmail(old_data={}):
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'email_taken': _(u'This e-mail address is already taken')
+        }
+
+        def _to_python(self, value, state):
+            return value.lower()
+
+        def validate_python(self, value, state):
+            if (old_data.get('email') or '').lower() != value:
+                user = User.get_by_email(value, case_insensitive=True)
+                if user:
+                    msg = M(self, 'email_taken', state)
+                    raise formencode.Invalid(msg, value, state,
+                        error_dict=dict(email=msg)
+                    )
+    return _validator
+
+
+def ValidSystemEmail():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
+        }
+
+        def _to_python(self, value, state):
+            return value.lower()
+
+        def validate_python(self, value, state):
+            user = User.get_by_email(value, case_insensitive=True)
+            if user is None:
+                msg = M(self, 'non_existing_email', state, email=value)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(email=msg)
+                )
+
+    return _validator
+
+
+def LdapLibValidator():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+
+        }
+
+        def validate_python(self, value, state):
+            try:
+                import ldap
+                ldap  # pyflakes silence !
+            except ImportError:
+                raise LdapImportError()
+
+    return _validator
+
+
+def AttrLoginValidator():
+    class _validator(formencode.validators.FancyValidator):
+        messages = {
+            'invalid_cn':
+                  _(u'The LDAP Login attribute of the CN must be specified - '
+                    'this is the name of the attribute that is equivalent '
+                    'to "username"')
+        }
+
+        def validate_python(self, value, state):
+            if not value or not isinstance(value, (str, unicode)):
+                msg = M(self, 'invalid_cn', state)
+                raise formencode.Invalid(msg, value, state,
+                    error_dict=dict(ldap_attr_login=msg)
+                )
+
+    return _validator
--- a/rhodecode/public/css/style.css	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/public/css/style.css	Mon Jun 18 00:35:13 2012 +0200
@@ -2352,7 +2352,7 @@
     padding: 2px 0px 2px 0px;
 }
 
-.cs_files .cs_added {
+.cs_files .cs_added,.cs_files .cs_A {
 	background: url("../images/icons/page_white_add.png") no-repeat scroll
 		3px;
 	height: 16px;
@@ -2361,7 +2361,7 @@
 	text-align: left;
 }
 
-.cs_files .cs_changed {
+.cs_files .cs_changed,.cs_files .cs_M {
 	background: url("../images/icons/page_white_edit.png") no-repeat scroll
 		3px;
 	height: 16px;
@@ -2370,7 +2370,7 @@
 	text-align: left;
 }
 
-.cs_files .cs_removed {
+.cs_files .cs_removed,.cs_files .cs_D {
 	background: url("../images/icons/page_white_delete.png") no-repeat
 		scroll 3px;
 	height: 16px;
@@ -2466,6 +2466,31 @@
     font-weight: bold !important;
 }
 
+.changeset-status-container{
+    padding-right: 5px;
+    margin-top:1px;
+    float:right;
+    height:14px;
+}
+.code-header .changeset-status-container{
+	float:left;
+	padding:2px 0px 0px 2px;
+}
+.changeset-status-container .changeset-status-lbl{
+	color: rgb(136, 136, 136);
+    float: left;
+    padding: 3px 4px 0px 0px
+}
+.code-header .changeset-status-container .changeset-status-lbl{
+    float: left;
+    padding: 0px 4px 0px 0px;   
+}
+.changeset-status-container .changeset-status-ico{
+    float: left;
+}
+.code-header .changeset-status-container .changeset-status-ico, .container .changeset-status-ico{
+    float: left;
+}
 .right .comments-container{
 	padding-right: 5px;
 	margin-top:1px;
@@ -3713,6 +3738,21 @@
 	padding:0px 0px 0px 10px;
 }
 
+.emails_wrap{
+	padding: 0px 20px;
+}
+
+.emails_wrap .email_entry{
+    height: 30px;
+    padding:0px 0px 0px 10px;
+}
+.emails_wrap .email_entry .email{
+	float: left
+}
+.emails_wrap .email_entry .email_action{
+	float: left
+}
+
 /*README STYLE*/
 
 div.readme {
@@ -3917,6 +3957,7 @@
     background: #f8f8f8;
     padding: 4px;
     border-bottom: 1px solid #ddd;
+    height: 18px;
 }
 
 .comments .comment .meta img {
@@ -3925,9 +3966,13 @@
 
 .comments .comment .meta .user {
     font-weight: bold;
+    float: left;
+    padding: 4px 2px 2px 2px;
 }
 
 .comments .comment .meta .date {
+	float: left;
+	padding:4px 4px 0px 4px;
 }
 
 .comments .comment .text {
@@ -3946,6 +3991,11 @@
 
 /** comment form **/
 
+.status-block{
+    height:80px;
+    clear:both	
+}
+
 .comment-form .clearfix{
 	background: #EEE;
     -webkit-border-radius: 4px;
@@ -4115,6 +4165,7 @@
     background: #f8f8f8;
     padding: 4px;
     border-bottom: 1px solid #ddd;
+    height: 20px;
 }
 
 .inline-comments .comment .meta img {
@@ -4123,9 +4174,13 @@
 
 .inline-comments .comment .meta .user {
     font-weight: bold;
+    float:left;
+    padding: 3px;
 }
 
 .inline-comments .comment .meta .date {
+    float:left;
+    padding: 3px;
 }
 
 .inline-comments .comment .text {
Binary file rhodecode/public/images/arrow_right_64.png has changed
Binary file rhodecode/public/images/icons/flag_status_approved.png has changed
Binary file rhodecode/public/images/icons/flag_status_not_reviewed.png has changed
Binary file rhodecode/public/images/icons/flag_status_rejected.png has changed
Binary file rhodecode/public/images/icons/flag_status_under_review.png has changed
--- a/rhodecode/public/js/rhodecode.js	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/public/js/rhodecode.js	Mon Jun 18 00:35:13 2012 +0200
@@ -205,7 +205,7 @@
 		success:s_wrapper,
 		failure:function(o){
 			console.log(o);
-			YUD.get(container).innerHTML='ERROR';
+			YUD.get(container).innerHTML='ERROR '+o.status;
 			YUD.setStyle(container,'opacity','1.0');
 			YUD.setStyle(container,'color','red');
 		}
@@ -1326,4 +1326,142 @@
     var comp = YAHOO.util.Sort.compare;
     var compState = comp(a_, b_, desc);
     return compState;
-};
\ No newline at end of file
+};
+
+
+
+/* Multi selectors */
+
+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 choosen
+	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 choosen = 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(choosen,c[0]);
+	        fill_with(available,c[1]);
+	    }
+	    // remove action
+	    if (this.id=='remove_element'){
+	        var c = get_checked(choosen);
+	        add_to(available,c[0]);
+	        fill_with(choosen,c[1]);
+	    }
+	    // add all elements
+	    if(this.id=='add_all_elements'){
+	        for(var i=0; node = available.options[i];i++){
+	                choosen.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 = choosen.options[i];i++){
+	            available.appendChild(new Option(node.text,
+	                    node.value, false, false));
+	        }
+	        choosen.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 choosen = YUD.get(selected_container);
+		    for (var i = 0; i < choosen.options.length; i++) {
+		        choosen.options[i].selected = 'selected';
+		    }
+		});
+	}
+}
\ No newline at end of file
--- a/rhodecode/templates/admin/notifications/notifications.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/admin/notifications/notifications.html	Mon Jun 18 00:35:13 2012 +0200
@@ -25,6 +25,10 @@
         ##</ul>
     </div>
     %if c.notifications:
+      <div style="padding:14px 18px;text-align: right;float:left">
+      <span id='all' class="ui-btn"><a href="${h.url.current()}">${_('All')}</a></span>
+      <span id='pull_request' class="ui-btn"><a href="${h.url.current(type=c.pull_request_type)}">${_('Pull requests')}</a></span>
+      </div>
       <div style="padding:14px 18px;text-align: right;float:right">
       <span id='mark_all_read' class="ui-btn">${_('Mark all read')}</span>
       </div>
@@ -40,7 +44,7 @@
  deleteNotification(url_del,notification_id)
 })
 YUE.on('mark_all_read','click',function(e){
-    var url = "${h.url('notifications_mark_all_read')}";
+    var url = "${h.url('notifications_mark_all_read', **request.GET)}";
     ypjax(url,'notification_data',function(){
     	var notification_counter = YUD.get('notification_counter');
     	if(notification_counter){
--- a/rhodecode/templates/admin/users/user_edit.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/admin/users/user_edit.html	Mon Jun 18 00:35:13 2012 +0200
@@ -204,4 +204,48 @@
            %endfor
     </div>
 </div>
+<div class="box box-right">
+    <!-- box / title -->
+    <div class="title">
+        <h5>${_('Email addresses')}</h5>
+    </div>
+    
+    <div class="emails_wrap">
+      <table class="noborder">
+      %for em in c.user_email_map:
+        <tr>
+            <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(em.user.email,16)}"/> </div></td>
+            <td><div class="email">${em.email}</div></td>
+            <td>
+              ${h.form(url('user_emails_delete', id=c.user.user_id),method='delete')}
+                  ${h.hidden('del_email',em.email_id)}
+                  ${h.submit('remove_',_('delete'),id="remove_email_%s" % em.email_id,
+                  class_="delete_icon action_button", onclick="return  confirm('"+_('Confirm to delete this email: %s') % em.email+"');")}
+              ${h.end_form()}            
+            </td>
+        </tr>
+      %endfor
+      </table>
+    </div>
+    
+    ${h.form(url('user_emails', id=c.user.user_id),method='put')}
+    <div class="form">
+        <!-- fields -->
+        <div class="fields">
+             <div class="field">
+                <div class="label">
+                    <label for="email">${_('New email address')}:</label>
+                </div>
+                <div class="input">
+                    ${h.text('new_email', class_='medium')}
+                </div>
+             </div>        
+            <div class="buttons">
+              ${h.submit('save',_('Add'),class_="ui-button")}
+              ${h.reset('reset',_('Reset'),class_="ui-button")}
+            </div>
+        </div>
+    </div>
+    ${h.end_form()}
+</div>
 </%def>
--- a/rhodecode/templates/admin/users_groups/users_group_edit.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/admin/users_groups/users_group_edit.html	Mon Jun 18 00:35:13 2012 +0200
@@ -140,141 +140,6 @@
     </div>
 </div>
 <script type="text/javascript">
-YAHOO.util.Event.onDOMReady(function(){
-  var D = YAHOO.util.Dom;
-  var E = YAHOO.util.Event;
-
-  //definition of containers ID's
-  var available_container = 'available_members';
-  var selected_container = 'users_group_members';
-
-  //form containing containers id
-  var form_id = 'edit_users_group';
-
-  //temp container for selected storage.
-  var cache = new Array();
-  var av_cache = new Array();
-  var c =  D.get(selected_container);
-  var ac = D.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 choosen
-  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 choosen = D.get(selected_container);
-      var available = D.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(choosen,c[0]);
-          fill_with(available,c[1]);
-      }
-      // remove action
-      if (this.id=='remove_element'){
-          var c = get_checked(choosen);
-          add_to(available,c[0]);
-          fill_with(choosen,c[1]);
-      }
-      // add all elements
-      if(this.id=='add_all_elements'){
-          for(var i=0; node = available.options[i];i++){
-                  choosen.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 = choosen.options[i];i++){
-              available.appendChild(new Option(node.text,
-                      node.value, false, false));
-          }
-          choosen.options.length = 0;
-      }
-
-  }
-
-  E.addListener(['add_element','remove_element',
-                 'add_all_elements','remove_all_elements'],'click',
-                 prompts_action_callback)
-
-  E.addListener(form_id,'submit',function(){
-      var choosen = D.get(selected_container);
-      for (var i = 0; i < choosen.options.length; i++) {
-          choosen.options[i].selected = 'selected';
-      }
-  });
-});
+  MultiSelectWidget('users_group_members','available_members','edit_users_group');
 </script>
 </%def>
--- a/rhodecode/templates/base/base.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/base/base.html	Mon Jun 18 00:35:13 2012 +0200
@@ -247,6 +247,14 @@
                     <span class="short">${c.repository_forks}</span>
                     </a>
                 </li>
+                <li>
+                    <a class="menu_link" title="${_('Pull requests')}" href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}">
+                    <span class="icon_short">
+                        <img src="${h.url('/images/icons/arrow_join.png')}" alt="${_('Pull requests')}" />
+                    </span>
+                    <span class="short">${c.repository_pull_requests}</span>
+                    </a>
+                </li>
                 ${usermenu()}
 	        </ul>
             <script type="text/javascript">
--- a/rhodecode/templates/branches/branches.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/branches/branches.html	Mon Jun 18 00:35:13 2012 +0200
@@ -25,12 +25,27 @@
         ${self.breadcrumbs()}
     </div>
     <!-- end box / title -->
+    %if c.repo_branches:
+    <div class="info_box" id="compare_branches" style="clear: both;padding: 10px 19px;vertical-align: right;text-align: right;"><a href="#" class="ui-btn small">${_('Compare branches')}</a></div>
+    %endif
     <div class="table">
         <%include file='branches_data.html'/>
     </div>
 </div>
 <script type="text/javascript">
+YUE.on('compare_branches','click',function(e){
+	YUE.preventDefault(e);
+	var org = YUQ('input[name=compare_org]:checked')[0];
+	var other = YUQ('input[name=compare_other]:checked')[0];
 
+	if(org && other){
+	    var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='__ORG__',other_ref_type='branch',other_ref='__OTHER__')}";
+		var u = compare_url.replace('__ORG__',org.value)
+		                   .replace('__OTHER__',other.value);
+		window.location=u;
+	}
+	
+})
 // main table sorting
 var myColumnDefs = [
     {key:"name",label:"${_('Name')}",sortable:true},
@@ -39,6 +54,7 @@
     {key:"author",label:"${_('Author')}",sortable:true},
     {key:"revision",label:"${_('Revision')}",sortable:true,
         sortOptions: { sortFunction: revisionSort }},
+    {key:"compare",label:"${_('Compare')}",sortable:false,},
 ];
 
 var myDataSource = new YAHOO.util.DataSource(YUD.get("branches_data"));
@@ -51,6 +67,7 @@
         {key:"date"},
         {key:"author"},
         {key:"revision"},
+        {key:"compare"},
     ]
 };
 
--- a/rhodecode/templates/branches/branches_data.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/branches/branches_data.html	Mon Jun 18 00:35:13 2012 +0200
@@ -7,6 +7,7 @@
             <th class="left">${_('date')}</th>
             <th class="left">${_('author')}</th>
             <th class="left">${_('revision')}</th>
+            <th class="left">${_('compare')}</th>
         </tr>
       </thead>
 		%for cnt,branch in enumerate(c.repo_branches.items()):
@@ -24,6 +25,10 @@
                     <pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id)}">r${branch[1].revision}:${h.short_id(branch[1].raw_id)}</a></pre>
                 </div>
             </td>
+            <td>
+                <input class="branch-compare" type="radio" name="compare_org" value="${branch[0]}"/>
+                <input class="branch-compare" type="radio" name="compare_other" value="${branch[0]}"/>
+            </td>
 		</tr>
 		%endfor
         % if hasattr(c,'repo_closed_branches') and c.repo_closed_branches:
@@ -42,6 +47,7 @@
                     <pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id)}">r${branch[1].revision}:${h.short_id(branch[1].raw_id)}</a></pre>
                 </div>
               </td>
+              <td></td>
           </tr>
           %endfor
         %endif
--- a/rhodecode/templates/changelog/changelog.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/changelog/changelog.html	Mon Jun 18 00:35:13 2012 +0200
@@ -32,6 +32,12 @@
 					<canvas id="graph_canvas"></canvas>
 				</div>
 				<div id="graph_content">
+                    <div class="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;">
+                    %if c.rhodecode_db_repo.fork:
+                        <a title="${_('compare fork with %s' % c.rhodecode_db_repo.fork.repo_name)}" href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='default',other_ref_type='branch',other_ref='default',repo=c.rhodecode_db_repo.fork.repo_name)}" class="ui-btn small">${_('Compare fork')}</a>
+                    %endif
+                    <a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a>
+                    </div>
 					<div class="container_header">
 				        ${h.form(h.url.current(),method='get')}
 				        <div class="info_box" style="float:left">
@@ -76,6 +82,18 @@
                                             </div>
                                         %endif
                                         </div>
+                                        <div class="changeset-status-container">
+                                            %if c.statuses.get(cs.raw_id):
+                                              <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
+                                              <div class="changeset-status-ico">
+                                              %if c.statuses.get(cs.raw_id)[2]:
+                                                <a class="tooltip" title="${_('Click to open associated pull request')}" 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
+                                        </div>
 									</div>
 								   %if cs.parents:
 									%for p_cs in reversed(cs.parents):
--- a/rhodecode/templates/changelog/changelog_details.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/changelog/changelog_details.html	Mon Jun 18 00:35:13 2012 +0200
@@ -1,3 +1,5 @@
+## small box that displays changed/added/removed details fetched by AJAX
+
 % if len(c.cs.affected_files) <= c.affected_files_cut_off:
 <span class="removed tooltip" title="<b>${h.tooltip(_('removed'))}</b>${h.changed_tooltip(c.cs.removed)}">${len(c.cs.removed)}</span>
 <span class="changed tooltip" title="<b>${h.tooltip(_('changed'))}</b>${h.changed_tooltip(c.cs.changed)}">${len(c.cs.changed)}</span>
--- a/rhodecode/templates/changeset/changeset.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/changeset/changeset.html	Mon Jun 18 00:35:13 2012 +0200
@@ -33,6 +33,12 @@
                 <div class="date">
                   ${h.fmt_date(c.changeset.date)}
                 </div>
+                <div class="changeset-status-container">
+                    %if c.statuses:
+                      <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.statuses[0])}]</div>
+                      <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[0])}" /></div>
+                    %endif
+                </div>
                 <div class="diff-actions">
                   <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show')}"  class="tooltip" title="${h.tooltip(_('raw diff'))}"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
                   <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}"  class="tooltip" title="${h.tooltip(_('download diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_get.png')}"/></a>
@@ -128,8 +134,10 @@
     <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
     ${comment.comment_inline_form(c.changeset)}
 
-    ## render comments
-    ${comment.comments(c.changeset)}
+    ## render comments main comments form and it status
+    ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.changeset.raw_id),
+                       h.changeset_status(c.rhodecode_db_repo, c.changeset.raw_id))}
+
     <script type="text/javascript">
       YUE.onDOMReady(function(){
     	  AJAX_COMMENT_URL = "${url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id)}";
@@ -159,6 +167,7 @@
           // inject comments into they proper positions
           var file_comments = YUQ('.inline-comment-placeholder');
           renderInlineComments(file_comments);
+
       })
 
     </script>
--- a/rhodecode/templates/changeset/changeset_file_comment.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/changeset/changeset_file_comment.html	Mon Jun 18 00:35:13 2012 +0200
@@ -7,17 +7,24 @@
   <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
     <div class="comment-wrapp">
   	<div class="meta">
-  		<span class="user">
-  			<img src="${h.gravatar_url(co.author.email, 20)}" />
+        <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
+  		<div class="user">
   			${co.author.username}
-  		</span>
-  		<span class="date">
+  		</div>
+  		<div class="date">
   			${h.age(co.modified_at)}
-  		</span>
+  		</div>
+        %if co.status_change:
+           <div  style="float:left" class="changeset-status-container">
+             <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
+             <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change.status_lbl}</div>
+             <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change.status))}" /></div>             
+           </div>
+        %endif      
       %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
-        <span class="buttons">
+        <div class="buttons">
           <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
-        </span>
+        </div>
       %endif
   	</div>
   	<div class="text">
@@ -70,7 +77,8 @@
 </%def>
 
 
-<%def name="inlines(changeset)">
+## generates inlines taken from c.comments var
+<%def name="inlines()">
     <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
     %for path, lines in c.inline_comments:
         % for line,comments in lines.iteritems():
@@ -84,11 +92,13 @@
 
 </%def>
 
-<%def name="comments(changeset)">
+## MAIN COMMENT FORM
+<%def name="comments(post_url, cur_status)">
 
 <div class="comments">
     <div id="inline-comments-container">
-     ${inlines(changeset)}
+    ## generate inlines for this changeset
+     ${inlines()}
     </div>
 
     %for co in c.comments:
@@ -98,16 +108,26 @@
     %endfor
     %if c.rhodecode_user.username != 'default':
     <div class="comment-form ac">
-        ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id))}
+        ${h.form(post_url)}
         <strong>${_('Leave a comment')}</strong>
         <div 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>' %
           		_('Use @username inside this text to send notification to this RhodeCode user')))|n}
+                | <span class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}
+                  <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
+                  </span> 
             </div>
-                <div class="mentions-container" id="mentions_container"></div>
-                ${h.textarea('text')}
+            <div id="status_block_container" class="status-block" style="display:none">
+                %for status,lbl in c.changeset_statuses:
+                    <div class="">
+                        <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" name="changeset_status" value="${status}"> <label>${lbl}</label>
+                    </div>                    
+                %endfor
+            </div>                 
+            <div class="mentions-container" id="mentions_container"></div>
+             ${h.textarea('text')}
         </div>
         <div class="comment-button">
         ${h.submit('save', _('Comment'), class_='ui-button')}
@@ -119,6 +139,17 @@
 <script>
 YUE.onDOMReady(function () {
    MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
+   
+   // changeset status box listener
+   YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
+       if(e.currentTarget.checked){
+           YUD.setStyle('status_block_container','display','');  
+       }
+       else{
+           YUD.setStyle('status_block_container','display','none');
+       }
+   })
+   
 });
 </script>
 </%def>
--- a/rhodecode/templates/changeset/changeset_range.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/changeset/changeset_range.html	Mon Jun 18 00:35:13 2012 +0200
@@ -35,13 +35,18 @@
 	    <div id="changeset_compare_view_content">
 			<div class="container">
 			<table class="compare_view_commits noborder">
-            %for cs in c.cs_ranges:
+            %for cnt,cs in enumerate(c.cs_ranges):
                 <tr>
                 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
                 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
                 <td><div class="author">${h.person(cs.author)}</div></td>
-                <td><span class="tooltip" title="${h.tooltip(h.age(cs.date))}">${h.fmt_date(cs.date)}</span></td>
-                <td><div class="message">${h.urlify_commit(cs.message, c.repo_name)}</div></td>
+                <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
+                <td>
+                  %if c.statuses:
+                    <div title="${h.tooltip(_('Changeset status'))}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cnt])}" /></div>
+                  %endif
+                </td>
+                <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
                 </tr>
             %endfor
             </table>
--- a/rhodecode/templates/changeset/diff_block.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/changeset/diff_block.html	Mon Jun 18 00:35:13 2012 +0200
@@ -1,12 +1,12 @@
 ## -*- coding: utf-8 -*-
 ##usage:
 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
-## ${diff_block.diff_block(changes)}
+## ${diff_block.diff_block(change)}
 ##
-<%def name="diff_block(changes)">
+<%def name="diff_block(change)">
 
-%for change,filenode,diff,cs1,cs2,stat in changes:
-    %if change !='removed':
+%for op,filenode,diff,cs1,cs2,stat in change:
+    %if op !='removed':
     <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}_target" style="clear:both;margin-top:25px"></div>
     <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" class="diffblock  margined comm">
         <div class="code-header">
@@ -39,3 +39,23 @@
 %endfor
 
 </%def>
+
+<%def name="diff_block_simple(change)">
+
+  %for op,filenode_path,diff in change:
+    <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
+    <div id="${h.FID('',filenode_path)}" class="diffblock  margined comm">      
+      <div class="code-header">
+          <div class="changeset_header">
+              <div class="changeset_file">
+                  <a href="#">${h.safe_unicode(filenode_path)}</a>
+              </div>
+          </div>
+      </div>
+        <div class="code-body">
+            <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
+            ${diff|n}
+        </div>
+    </div>
+  %endfor
+</%def>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/compare/compare_cs.html	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,27 @@
+## Changesets table !
+<div class="container">
+  <table class="compare_view_commits noborder">
+  %if not c.cs_ranges:
+    <tr><td>${_('No changesets')}</td></tr>
+  %else:
+    %for cnt, cs in enumerate(c.cs_ranges):
+        <tr>
+        <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
+        <td>
+          %if cs.raw_id in c.statuses:
+            <div title="${c.statuses[cs.raw_id][1]}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cs.raw_id][0])}" /></div>
+          %endif
+        </td>
+        <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
+        %if c.as_form:
+          ${h.hidden('revisions',cs.raw_id)}
+        %endif
+        </td>
+        <td><div class="author">${h.person(cs.author)}</div></td>
+        <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
+        <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
+        </tr>
+    %endfor
+  %endif
+  </table>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/compare/compare_diff.html	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,77 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${c.repo_name} ${_('Compare')} ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${h.link_to(u'Home',h.url('/'))}
+    &raquo;
+    ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
+    &raquo;
+    ${_('Compare')}
+</%def>
+
+<%def name="page_nav()">
+    ${self.menu('changelog')}
+</%def>
+
+<%def name="main()">
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}
+    </div>
+    <div class="table">
+        <div id="body" class="diffblock">
+            <div class="code-header cv">
+                <h3 class="code-header-title">${_('Compare View')}</h3>
+                <div>
+                ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}  <a href="${c.swap_url}">[swap]</a>
+                </div>
+            </div>
+        </div>
+        <div id="changeset_compare_view_content">
+            ##CS
+            <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Outgoing changesets')}</div>
+            <%include file="compare_cs.html" />
+
+            ## FILES
+            <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
+            <div class="cs_files">
+              %for fid, change, f, stat in c.files:
+                  <div class="cs_${change}">
+                    <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
+                    <div class="changes">${h.fancy_file_stats(stat)}</div>
+                  </div>
+              %endfor
+            </div>
+        </div>
+    </div>
+
+    ## diff block
+    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
+    %for fid, change, f, stat in c.files:
+      ${diff_block.diff_block_simple([c.changes[fid]])}
+    %endfor
+
+     <script type="text/javascript">
+
+      YUE.onDOMReady(function(){
+
+          YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
+              var act = e.currentTarget.nextElementSibling;
+
+              if(YUD.hasClass(act,'active')){
+                  YUD.removeClass(act,'active');
+                  YUD.setStyle(act,'display','none');
+              }else{
+                  YUD.addClass(act,'active');
+                  YUD.setStyle(act,'display','');
+              }
+          });
+      })
+    </script>
+    </div>
+</%def>
--- a/rhodecode/templates/email_templates/changeset_comment.html	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/templates/email_templates/changeset_comment.html	Mon Jun 18 00:35:13 2012 +0200
@@ -4,3 +4,9 @@
 <h4>${subject}</h4>
 
 ${body}
+
+% if status_change is not None:
+<div>
+    New status -> ${status_change}
+</div>    
+% endif
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/pullrequests/pullrequest.html	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,192 @@
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${c.repo_name} ${_('New pull request')}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${h.link_to(u'Home',h.url('/'))}
+    &raquo;
+    ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
+    &raquo;
+    ${_('New pull request')}
+</%def>
+
+<%def name="main()">
+
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}
+    </div>
+    ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
+    <div style="float:left;padding:0px 30px 30px 30px">
+       <div style="padding:0px 5px 5px 5px">
+         <span>
+           <a id="refresh" href="#">
+             <img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
+             ${_('refresh overview')}
+           </a>
+         </span>
+       </div>
+        ##ORG
+        <div style="float:left">
+            <div class="fork_user">
+                <div class="gravatar">
+                    <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_db_repo.user.email,24)}"/>
+                </div>
+                <span style="font-size: 20px">
+                ${h.select('org_repo','',c.org_repos,class_='refs')}:${h.select('org_ref','',c.org_refs,class_='refs')}
+                </span>
+                 <div style="padding:5px 3px 3px 42px;">${c.rhodecode_db_repo.description}</div>
+            </div>
+            <div style="clear:both;padding-top: 10px"></div>
+        </div>
+          <div style="float:left;font-size:24px;padding:0px 20px">
+          <img height=32 width=32 src="${h.url('/images/arrow_right_64.png')}"/>
+          </div>
+
+        ##OTHER, most Probably the PARENT OF THIS FORK
+        <div style="float:left">
+            <div class="fork_user">
+                <div class="gravatar">
+                    <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_db_repo.user.email,24)}"/>
+                </div>
+                <span style="font-size: 20px">
+                ${h.select('other_repo',c.default_pull_request ,c.other_repos,class_='refs')}:${h.select('other_ref','',c.other_refs,class_='refs')}
+                </span>
+                 <div style="padding:5px 3px 3px 42px;">${c.rhodecode_db_repo.description}</div>
+            </div>
+            <div style="clear:both;padding-top: 10px"></div>
+        </div>
+       <div style="clear:both;padding-top: 10px"></div>
+       ## overview pulled by ajax
+       <div style="float:left" id="pull_request_overview"></div>
+       <div style="float:left;clear:both;padding:10px 10px 10px 0px;display:none">
+            <a id="pull_request_overview_url" href="#">${_('Detailed compare view')}</a>
+       </div>
+     </div>
+    <div style="float:left; border-left:1px dashed #eee">
+        <h4>${_('Pull request reviewers')}</h4>
+        <div id="reviewers" style="padding:0px 0px 0px 15px">
+        ##TODO: make this nicer :)
+          <table class="table noborder">
+                  <tr>
+                      <td>
+                          <div>
+                              <div style="float:left">
+                                  <div class="text" style="padding: 0px 0px 6px;">${_('Chosen reviewers')}</div>
+                                  ${h.select('review_members',[x[0] for x in c.review_members],c.review_members,multiple=True,size=8,style="min-width:210px")}
+                                 <div  id="remove_all_elements" style="cursor:pointer;text-align:center">
+                                     ${_('Remove all elements')}
+                                     <img alt="remove" style="vertical-align:text-bottom" src="${h.url('/images/icons/arrow_right.png')}"/>
+                                 </div>
+                              </div>
+                              <div style="float:left;width:20px;padding-top:50px">
+                                  <img alt="add" id="add_element"
+                                      style="padding:2px;cursor:pointer"
+                                      src="${h.url('/images/icons/arrow_left.png')}"/>
+                                  <br />
+                                  <img alt="remove" id="remove_element"
+                                      style="padding:2px;cursor:pointer"
+                                      src="${h.url('/images/icons/arrow_right.png')}"/>
+                              </div>
+                              <div style="float:left">
+                                   <div class="text" style="padding: 0px 0px 6px;">${_('Available reviewers')}</div>
+                                   ${h.select('available_members',[],c.available_members,multiple=True,size=8,style="min-width:210px")}
+                                   <div id="add_all_elements" style="cursor:pointer;text-align:center">
+                                         <img alt="add" style="vertical-align:text-bottom" src="${h.url('/images/icons/arrow_left.png')}"/>
+                                          ${_('Add all elements')}
+                                   </div>
+                              </div>
+                          </div>
+                      </td>
+                  </tr>
+          </table>
+        </div>
+    </div>
+    <h3>${_('Create new pull request')}</h3>
+
+    <div class="form">
+        <!-- fields -->
+
+        <div class="fields">
+
+             <div class="field">
+                <div class="label">
+                    <label for="pullrequest_title">${_('Title')}:</label>
+                </div>
+                <div class="input">
+                    ${h.text('pullrequest_title',size=30)}
+                </div>
+             </div>
+
+            <div class="field">
+                <div class="label label-textarea">
+                    <label for="pullrequest_desc">${_('description')}:</label>
+                </div>
+                <div class="textarea text-area editor">
+                    ${h.textarea('pullrequest_desc',size=30)}
+                </div>
+            </div>
+
+            <div class="buttons">
+                ${h.submit('save',_('Send pull request'),class_="ui-button")}
+                ${h.reset('reset',_('Reset'),class_="ui-button")}
+           </div>
+        </div>
+    </div>
+    ${h.end_form()}
+
+</div>
+
+<script type="text/javascript">
+  MultiSelectWidget('review_members','available_members','pull_request_form');
+
+  var loadPreview = function(){
+	  YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','none');
+      var url = "${h.url('compare_url',
+          repo_name='org_repo',
+          org_ref_type='branch', org_ref='org_ref',
+          other_ref_type='branch', other_ref='other_ref',
+          repo='other_repo',
+          as_form=True)}";
+
+      var select_refs = YUQ('#pull_request_form select.refs')
+
+      for(var i=0;i<select_refs.length;i++){
+        var select_ref = select_refs[i];
+        var select_ref_data = select_ref.value.split(':');
+        var key = null;
+        var val = null;
+        if(select_ref_data.length>1){
+          key = select_ref.name+"_type";
+          val = select_ref_data[0];
+          url = url.replace(key,val);
+
+          key = select_ref.name;
+          val = select_ref_data[1];
+          url = url.replace(key,val);
+
+        }else{
+          key = select_ref.name;
+          val = select_ref.value;
+          url = url.replace(key,val);
+        }
+      }
+
+      ypjax(url,'pull_request_overview', function(data){
+    	  YUD.get('pull_request_overview_url').href = url;
+    	  YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','');
+      })
+  }
+  YUE.on('refresh','click',function(e){
+     loadPreview()
+  })
+
+  //lazy load overview after 0.5s
+  setTimeout(loadPreview, 500)
+
+</script>
+
+</%def>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/pullrequests/pullrequest_show.html	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,83 @@
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${h.link_to(u'Home',h.url('/'))}
+    &raquo;
+    ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
+    &raquo;
+    ${_('Pull request #%s') % c.pull_request.pull_request_id}
+</%def>
+
+<%def name="main()">
+
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}
+    </div>
+    
+    <h3>${_('Title')}: ${c.pull_request.title}</h3>
+    <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
+        %if c.current_changeset_status:
+          <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
+          <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
+        %endif
+    </div>    
+    <div style="padding:4px">
+      <div>${h.fmt_date(c.pull_request.created_on)}</div>
+    </div>
+    
+    ##DIFF
+    
+    <div class="table">
+        <div id="body" class="diffblock">
+            <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
+        </div>
+        <div id="changeset_compare_view_content">
+            ##CS
+            <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
+            <%include file="/compare/compare_cs.html" />
+
+            ## FILES
+            <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
+            <div class="cs_files">
+              %for fid, change, f, stat in c.files:
+                  <div class="cs_${change}">
+                    <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
+                    <div class="changes">${h.fancy_file_stats(stat)}</div>
+                  </div>
+              %endfor
+            </div>
+        </div>
+    </div>
+    <script>
+    var _USERS_AC_DATA = ${c.users_array|n};
+    var _GROUPS_AC_DATA = ${c.users_groups_array|n};
+    </script>
+
+    ## diff block
+    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
+    %for fid, change, f, stat in c.files:
+      ${diff_block.diff_block_simple([c.changes[fid]])}
+    %endfor
+
+    ## template for inline comment form
+    <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
+    ##${comment.comment_inline_form(c.changeset)}
+
+    ## render comments main comments form and it status
+    ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
+                       c.current_changeset_status)}
+    
+</div>
+
+<script type="text/javascript">
+
+
+</script>
+
+</%def>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/pullrequests/pullrequest_show_all.html	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,31 @@
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${c.repo_name} ${_('All pull requests')}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${h.link_to(u'Home',h.url('/'))}
+    &raquo;
+    ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
+    &raquo;
+    ${_('All pull requests')}
+</%def>
+
+<%def name="main()">
+
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}
+    </div>
+    
+    %for pr in c.pull_requests:
+        <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">#${pr.pull_request_id}</a>
+    %endfor
+    
+</div>
+
+<script type="text/javascript"></script>
+
+</%def>
--- a/rhodecode/tests/__init__.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/tests/__init__.py	Mon Jun 18 00:35:13 2012 +0200
@@ -27,7 +27,8 @@
 from rhodecode import is_windows
 from rhodecode.model.meta import Session
 from rhodecode.model.db import User
-
+from rhodecode.tests.nose_parametrized import parameterized
+ 
 import pylons.test
 
 
@@ -38,9 +39,9 @@
 log = logging.getLogger(__name__)
 
 __all__ = [
-    'environ', 'url', 'get_new_dir', 'TestController', 'TESTS_TMP_PATH',
-    'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK',
-    'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN',
+    'parameterized', 'environ', 'url', 'get_new_dir', 'TestController',
+    'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
+    'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN',
     'TEST_USER_REGULAR_PASS', 'TEST_USER_REGULAR_EMAIL',
     'TEST_USER_REGULAR2_LOGIN', 'TEST_USER_REGULAR2_PASS',
     'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE',
@@ -54,6 +55,7 @@
 ##RUNNING DESIRED TESTS
 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
 # nosetests --pdb --pdb-failures
+# nosetests --with-coverage --cover-package=rhodecode.model.validators rhodecode.tests.test_validators
 environ = {}
 
 #SOME GLOBALS FOR TESTS
--- a/rhodecode/tests/functional/test_admin_settings.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/tests/functional/test_admin_settings.py	Mon Jun 18 00:35:13 2012 +0200
@@ -3,6 +3,8 @@
 from rhodecode.lib.auth import get_crypt_password, check_password
 from rhodecode.model.db import User, RhodeCodeSetting
 from rhodecode.tests import *
+from rhodecode.lib import helpers as h
+
 
 class TestAdminSettingsController(TestController):
 
@@ -47,7 +49,6 @@
         response = self.app.get(url('formatted_admin_edit_setting',
                                     setting_id=1, format='xml'))
 
-
     def test_ga_code_active(self):
         self.log_user()
         old_title = 'RhodeCode'
@@ -92,7 +93,6 @@
         self.assertTrue("""_gaq.push(['_setAccount', '%s']);""" % new_ga_code
                         not in response.body)
 
-
     def test_title_change(self):
         self.log_user()
         old_title = 'RhodeCode'
@@ -117,7 +117,6 @@
             self.assertTrue("""<h1><a href="/">%s</a></h1>""" % new_title
                         in response.body)
 
-
     def test_my_account(self):
         self.log_user()
         response = self.app.get(url('admin_settings_my_account'))
@@ -132,12 +131,11 @@
         new_lastname = 'NewLastname'
         new_password = 'test123'
 
-
         response = self.app.post(url('admin_settings_my_account_update'),
                                  params=dict(_method='put',
                                              username='test_admin',
                                              new_password=new_password,
-                                             password_confirmation = new_password,
+                                             password_confirmation=new_password,
                                              password='',
                                              name=new_name,
                                              lastname=new_lastname,
@@ -146,7 +144,7 @@
 
         assert 'Your account was updated successfully' in response.session['flash'][0][1], 'no flash message about success of change'
         user = self.Session.query(User).filter(User.username == 'test_admin').one()
-        assert user.email == new_email , 'incorrect user email after update got %s vs %s' % (user.email, new_email)
+        assert user.email == new_email, 'incorrect user email after update got %s vs %s' % (user.email, new_email)
         assert user.name == new_name, 'updated field mismatch %s vs %s' % (user.name, new_name)
         assert user.lastname == new_lastname, 'updated field mismatch %s vs %s' % (user.lastname, new_lastname)
         assert check_password(new_password, user.password) is True, 'password field mismatch %s vs %s' % (user.password, new_password)
@@ -161,7 +159,7 @@
                                                             _method='put',
                                                             username='test_admin',
                                                             new_password=old_password,
-                                                            password_confirmation = old_password,
+                                                            password_confirmation=old_password,
                                                             password='',
                                                             name=old_name,
                                                             lastname=old_lastname,
@@ -172,41 +170,46 @@
                                'Your account was updated successfully')
 
         user = self.Session.query(User).filter(User.username == 'test_admin').one()
-        assert user.email == old_email , 'incorrect user email after update got %s vs %s' % (user.email, old_email)
+        assert user.email == old_email, 'incorrect user email after update got %s vs %s' % (user.email, old_email)
 
-        assert user.email == old_email , 'incorrect user email after update got %s vs %s' % (user.email, old_email)
+        assert user.email == old_email, 'incorrect user email after update got %s vs %s' % (user.email, old_email)
         assert user.name == old_name, 'updated field mismatch %s vs %s' % (user.name, old_name)
         assert user.lastname == old_lastname, 'updated field mismatch %s vs %s' % (user.lastname, old_lastname)
-        assert check_password(old_password, user.password) is True , 'password updated field mismatch %s vs %s' % (user.password, old_password)
-
+        assert check_password(old_password, user.password) is True, 'password updated field mismatch %s vs %s' % (user.password, old_password)
 
     def test_my_account_update_err_email_exists(self):
         self.log_user()
 
-        new_email = 'test_regular@mail.com'#already exisitn email
+        new_email = 'test_regular@mail.com'  # already exisitn email
         response = self.app.post(url('admin_settings_my_account_update'), params=dict(
                                                             _method='put',
                                                             username='test_admin',
                                                             new_password='test12',
-                                                            password_confirmation = 'test122',
+                                                            password_confirmation='test122',
                                                             name='NewName',
                                                             lastname='NewLastname',
                                                             email=new_email,))
 
         assert 'This e-mail address is already taken' in response.body, 'Missing error message about existing email'
 
-
     def test_my_account_update_err(self):
         self.log_user('test_regular2', 'test12')
 
         new_email = 'newmail.pl'
-        response = self.app.post(url('admin_settings_my_account_update'), params=dict(
-                                                            _method='put',
-                                                            username='test_admin',
-                                                            new_password='test12',
-                                                            password_confirmation = 'test122',
-                                                            name='NewName',
-                                                            lastname='NewLastname',
-                                                            email=new_email,))
-        assert 'An email address must contain a single @' in response.body, 'Missing error message about wrong email'
-        assert 'This username already exists' in response.body, 'Missing error message about existing user'
+        response = self.app.post(url('admin_settings_my_account_update'),
+                                 params=dict(
+                                            _method='put',
+                                            username='test_admin',
+                                            new_password='test12',
+                                            password_confirmation='test122',
+                                            name='NewName',
+                                            lastname='NewLastname',
+                                            email=new_email,)
+                                 )
+
+        response.mustcontain('An email address must contain a single @')
+        from rhodecode.model import validators
+        msg = validators.ValidUsername(edit=False,
+                                    old_data={})._messages['username_exists']
+        msg = h.html_escape(msg % {'username': 'test_admin'})
+        response.mustcontain(u"%s" % msg)
--- a/rhodecode/tests/functional/test_admin_users.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/tests/functional/test_admin_users.py	Mon Jun 18 00:35:13 2012 +0200
@@ -1,8 +1,12 @@
+from sqlalchemy.orm.exc import NoResultFound
+
 from rhodecode.tests import *
 from rhodecode.model.db import User, Permission
 from rhodecode.lib.auth import check_password
-from sqlalchemy.orm.exc import NoResultFound
 from rhodecode.model.user import UserModel
+from rhodecode.model import validators
+from rhodecode.lib import helpers as h
+
 
 class TestAdminUsersController(TestController):
 
@@ -24,26 +28,25 @@
         email = 'mail@mail.com'
 
         response = self.app.post(url('users'),
-                                 {'username':username,
-                                   'password':password,
-                                   'password_confirmation':password_confirmation,
-                                   'name':name,
-                                   'active':True,
-                                   'lastname':lastname,
-                                   'email':email})
+                             {'username': username,
+                               'password': password,
+                               'password_confirmation': password_confirmation,
+                               'name': name,
+                               'active': True,
+                               'lastname': lastname,
+                               'email': email})
 
+        self.checkSessionFlash(response, '''created user %s''' % (username))
 
-        self.assertTrue('''created user %s''' % (username) in
-                        response.session['flash'][0])
 
         new_user = self.Session.query(User).\
             filter(User.username == username).one()
 
-        self.assertEqual(new_user.username,username)
-        self.assertEqual(check_password(password, new_user.password),True)
-        self.assertEqual(new_user.name,name)
-        self.assertEqual(new_user.lastname,lastname)
-        self.assertEqual(new_user.email,email)
+        self.assertEqual(new_user.username, username)
+        self.assertEqual(check_password(password, new_user.password), True)
+        self.assertEqual(new_user.name, name)
+        self.assertEqual(new_user.lastname, lastname)
+        self.assertEqual(new_user.email, email)
 
         response.follow()
         response = response.follow()
@@ -57,16 +60,18 @@
         lastname = 'lastname'
         email = 'errmail.com'
 
-        response = self.app.post(url('users'), {'username':username,
-                                               'password':password,
-                                               'name':name,
-                                               'active':False,
-                                               'lastname':lastname,
-                                               'email':email})
+        response = self.app.post(url('users'), {'username': username,
+                                               'password': password,
+                                               'name': name,
+                                               'active': False,
+                                               'lastname': lastname,
+                                               'email': email})
 
-        self.assertTrue("""<span class="error-message">Invalid username</span>""" in response.body)
-        self.assertTrue("""<span class="error-message">Please enter a value</span>""" in response.body)
-        self.assertTrue("""<span class="error-message">An email address must contain a single @</span>""" in response.body)
+        msg = validators.ValidUsername(False, {})._messages['system_invalid_username']
+        msg = h.html_escape(msg % {'username': 'new_user'})
+        response.mustcontain("""<span class="error-message">%s</span>""" % msg)
+        response.mustcontain("""<span class="error-message">Please enter a value</span>""")
+        response.mustcontain("""<span class="error-message">An email address must contain a single @</span>""")
 
         def get_user():
             self.Session.query(User).filter(User.username == username).one()
@@ -94,13 +99,13 @@
         lastname = 'lastname'
         email = 'todeletemail@mail.com'
 
-        response = self.app.post(url('users'), {'username':username,
-                                               'password':password,
-                                               'password_confirmation':password,
-                                               'name':name,
-                                               'active':True,
-                                               'lastname':lastname,
-                                               'email':email})
+        response = self.app.post(url('users'), {'username': username,
+                                               'password': password,
+                                               'password_confirmation': password,
+                                               'name': name,
+                                               'active': True,
+                                               'lastname': lastname,
+                                               'email': email})
 
         response = response.follow()
 
@@ -111,7 +116,6 @@
         self.assertTrue("""successfully deleted user""" in
                         response.session['flash'][0])
 
-
     def test_delete_browser_fakeout(self):
         response = self.app.post(url('user', id=1),
                                  params=dict(_method='delete'))
@@ -127,7 +131,6 @@
         user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
         response = self.app.get(url('edit_user', id=user.user_id))
 
-
     def test_add_perm_create_repo(self):
         self.log_user()
         perm_none = Permission.get_by_key('hg.create.none')
@@ -135,7 +138,6 @@
 
         user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
 
-
         #User should have None permission on creation repository
         self.assertEqual(UserModel().has_perm(user, perm_none), False)
         self.assertEqual(UserModel().has_perm(user, perm_create), False)
@@ -159,7 +161,6 @@
 
         user = User.get_by_username(TEST_USER_REGULAR2_LOGIN)
 
-
         #User should have None permission on creation repository
         self.assertEqual(UserModel().has_perm(user, perm_none), False)
         self.assertEqual(UserModel().has_perm(user, perm_create), False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/tests/functional/test_compare.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,52 @@
+from rhodecode.tests import *
+
+
+class TestCompareController(TestController):
+
+    def test_index_tag(self):
+        self.log_user()
+        tag1='0.1.3'
+        tag2='0.1.2'
+        response = self.app.get(url(controller='compare', action='index',
+                                    repo_name=HG_REPO,
+                                    org_ref_type="tag",
+                                    org_ref=tag1,
+                                    other_ref_type="tag",
+                                    other_ref=tag2,
+                                    ))
+        response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
+        ## outgoing changesets between tags
+        response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
+        response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
+        response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
+        response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
+        response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
+        response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
+        response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
+        
+        ## files diff
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1,  tag2))
+        response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1,  tag2))
+
+    def test_index_branch(self):
+        self.log_user()
+        response = self.app.get(url(controller='compare', action='index',
+                                    repo_name=HG_REPO,
+                                    org_ref_type="branch",
+                                    org_ref='default',
+                                    other_ref_type="branch",
+                                    other_ref='default',
+                                    ))
+
+        response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
+        # branch are equal
+        response.mustcontain('<tr><td>No changesets</td></tr>')
--- a/rhodecode/tests/functional/test_login.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/tests/functional/test_login.py	Mon Jun 18 00:35:13 2012 +0200
@@ -4,6 +4,8 @@
 from rhodecode.lib.utils2 import generate_api_key
 from rhodecode.lib.auth import check_password
 from rhodecode.model.meta import Session
+from rhodecode.lib import helpers as h
+from rhodecode.model import validators
 
 
 class TestLoginController(TestController):
@@ -22,21 +24,21 @@
 
     def test_login_admin_ok(self):
         response = self.app.post(url(controller='login', action='index'),
-                                 {'username':'test_admin',
-                                  'password':'test12'})
+                                 {'username': 'test_admin',
+                                  'password': 'test12'})
         self.assertEqual(response.status, '302 Found')
-        self.assertEqual(response.session['rhodecode_user'].get('username') ,
+        self.assertEqual(response.session['rhodecode_user'].get('username'),
                          'test_admin')
         response = response.follow()
         self.assertTrue('%s repository' % HG_REPO in response.body)
 
     def test_login_regular_ok(self):
         response = self.app.post(url(controller='login', action='index'),
-                                 {'username':'test_regular',
-                                  'password':'test12'})
+                                 {'username': 'test_regular',
+                                  'password': 'test12'})
 
         self.assertEqual(response.status, '302 Found')
-        self.assertEqual(response.session['rhodecode_user'].get('username') ,
+        self.assertEqual(response.session['rhodecode_user'].get('username'),
                          'test_regular')
         response = response.follow()
         self.assertTrue('%s repository' % HG_REPO in response.body)
@@ -46,8 +48,8 @@
         test_came_from = '/_admin/users'
         response = self.app.post(url(controller='login', action='index',
                                      came_from=test_came_from),
-                                 {'username':'test_admin',
-                                  'password':'test12'})
+                                 {'username': 'test_admin',
+                                  'password': 'test12'})
         self.assertEqual(response.status, '302 Found')
         response = response.follow()
 
@@ -56,17 +58,16 @@
 
     def test_login_short_password(self):
         response = self.app.post(url(controller='login', action='index'),
-                                 {'username':'test_admin',
-                                  'password':'as'})
+                                 {'username': 'test_admin',
+                                  'password': 'as'})
         self.assertEqual(response.status, '200 OK')
 
         self.assertTrue('Enter 3 characters or more' in response.body)
 
     def test_login_wrong_username_password(self):
         response = self.app.post(url(controller='login', action='index'),
-                                 {'username':'error',
-                                  'password':'test12'})
-        self.assertEqual(response.status , '200 OK')
+                                 {'username': 'error',
+                                  'password': 'test12'})
 
         self.assertTrue('invalid user name' in response.body)
         self.assertTrue('invalid password' in response.body)
@@ -79,62 +80,63 @@
         self.assertTrue('Sign Up to RhodeCode' in response.body)
 
     def test_register_err_same_username(self):
+        uname = 'test_admin'
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'test_admin',
-                                             'password':'test12',
-                                             'password_confirmation':'test12',
-                                             'email':'goodmail@domain.com',
-                                             'name':'test',
-                                             'lastname':'test'})
+                                            {'username': uname,
+                                             'password': 'test12',
+                                             'password_confirmation': 'test12',
+                                             'email': 'goodmail@domain.com',
+                                             'name': 'test',
+                                             'lastname': 'test'})
 
-        self.assertEqual(response.status , '200 OK')
-        self.assertTrue('This username already exists' in response.body)
+        msg = validators.ValidUsername()._messages['username_exists']
+        msg = h.html_escape(msg % {'username': uname})
+        response.mustcontain(msg)
 
     def test_register_err_same_email(self):
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'test_admin_0',
-                                             'password':'test12',
-                                             'password_confirmation':'test12',
-                                             'email':'test_admin@mail.com',
-                                             'name':'test',
-                                             'lastname':'test'})
+                                            {'username': 'test_admin_0',
+                                             'password': 'test12',
+                                             'password_confirmation': 'test12',
+                                             'email': 'test_admin@mail.com',
+                                             'name': 'test',
+                                             'lastname': 'test'})
 
-        self.assertEqual(response.status , '200 OK')
-        response.mustcontain('This e-mail address is already taken')
+        msg = validators.UniqSystemEmail()()._messages['email_taken']
+        response.mustcontain(msg)
 
     def test_register_err_same_email_case_sensitive(self):
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'test_admin_1',
-                                             'password':'test12',
-                                             'password_confirmation':'test12',
-                                             'email':'TesT_Admin@mail.COM',
-                                             'name':'test',
-                                             'lastname':'test'})
-        self.assertEqual(response.status , '200 OK')
-        response.mustcontain('This e-mail address is already taken')
+                                            {'username': 'test_admin_1',
+                                             'password': 'test12',
+                                             'password_confirmation': 'test12',
+                                             'email': 'TesT_Admin@mail.COM',
+                                             'name': 'test',
+                                             'lastname': 'test'})
+        msg = validators.UniqSystemEmail()()._messages['email_taken']
+        response.mustcontain(msg)
 
     def test_register_err_wrong_data(self):
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'xs',
-                                             'password':'test',
-                                             'password_confirmation':'test',
-                                             'email':'goodmailm',
-                                             'name':'test',
-                                             'lastname':'test'})
-        self.assertEqual(response.status , '200 OK')
+                                            {'username': 'xs',
+                                             'password': 'test',
+                                             'password_confirmation': 'test',
+                                             'email': 'goodmailm',
+                                             'name': 'test',
+                                             'lastname': 'test'})
+        self.assertEqual(response.status, '200 OK')
         response.mustcontain('An email address must contain a single @')
         response.mustcontain('Enter a value 6 characters long or more')
 
     def test_register_err_username(self):
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'error user',
-                                             'password':'test12',
-                                             'password_confirmation':'test12',
-                                             'email':'goodmailm',
-                                             'name':'test',
-                                             'lastname':'test'})
+                                            {'username': 'error user',
+                                             'password': 'test12',
+                                             'password_confirmation': 'test12',
+                                             'email': 'goodmailm',
+                                             'name': 'test',
+                                             'lastname': 'test'})
 
-        self.assertEqual(response.status , '200 OK')
         response.mustcontain('An email address must contain a single @')
         response.mustcontain('Username may only contain '
                 'alphanumeric characters underscores, '
@@ -142,41 +144,42 @@
                 'alphanumeric character')
 
     def test_register_err_case_sensitive(self):
+        usr = 'Test_Admin'
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'Test_Admin',
-                                             'password':'test12',
-                                             'password_confirmation':'test12',
-                                             'email':'goodmailm',
-                                             'name':'test',
-                                             'lastname':'test'})
+                                            {'username': usr,
+                                             'password': 'test12',
+                                             'password_confirmation': 'test12',
+                                             'email': 'goodmailm',
+                                             'name': 'test',
+                                             'lastname': 'test'})
 
-        self.assertEqual(response.status , '200 OK')
-        self.assertTrue('An email address must contain a single @' in response.body)
-        self.assertTrue('This username already exists' in response.body)
+        response.mustcontain('An email address must contain a single @')
+        msg = validators.ValidUsername()._messages['username_exists']
+        msg = h.html_escape(msg % {'username': usr})
+        response.mustcontain(msg)
 
     def test_register_special_chars(self):
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'xxxaxn',
-                                             'password':'ąćźżąśśśś',
-                                             'password_confirmation':'ąćźżąśśśś',
-                                             'email':'goodmailm@test.plx',
-                                             'name':'test',
-                                             'lastname':'test'})
+                                        {'username': 'xxxaxn',
+                                         'password': 'ąćźżąśśśś',
+                                         'password_confirmation': 'ąćźżąśśśś',
+                                         'email': 'goodmailm@test.plx',
+                                         'name': 'test',
+                                         'lastname': 'test'})
 
-        self.assertEqual(response.status , '200 OK')
-        self.assertTrue('Invalid characters in password' in response.body)
+        msg = validators.ValidPassword()._messages['invalid_password']
+        response.mustcontain(msg)
 
     def test_register_password_mismatch(self):
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':'xs',
-                                             'password':'123qwe',
-                                             'password_confirmation':'qwe123',
-                                             'email':'goodmailm@test.plxa',
-                                             'name':'test',
-                                             'lastname':'test'})
-
-        self.assertEqual(response.status, '200 OK')
-        response.mustcontain('Passwords do not match')
+                                            {'username': 'xs',
+                                             'password': '123qwe',
+                                             'password_confirmation': 'qwe123',
+                                             'email': 'goodmailm@test.plxa',
+                                             'name': 'test',
+                                             'lastname': 'test'})
+        msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
+        response.mustcontain(msg)
 
     def test_register_ok(self):
         username = 'test_regular4'
@@ -186,13 +189,13 @@
         lastname = 'testlastname'
 
         response = self.app.post(url(controller='login', action='register'),
-                                            {'username':username,
-                                             'password':password,
-                                             'password_confirmation':password,
-                                             'email':email,
-                                             'name':name,
-                                             'lastname':lastname,
-                                             'admin':True}) # This should be overriden
+                                            {'username': username,
+                                             'password': password,
+                                             'password_confirmation': password,
+                                             'email': email,
+                                             'name': name,
+                                             'lastname': lastname,
+                                             'admin': True})  # This should be overriden
         self.assertEqual(response.status, '302 Found')
         self.checkSessionFlash(response, 'You have successfully registered into rhodecode')
 
@@ -206,12 +209,15 @@
         self.assertEqual(ret.admin, False)
 
     def test_forgot_password_wrong_mail(self):
+        bad_email = 'marcin@wrongmail.org'
         response = self.app.post(
                         url(controller='login', action='password_reset'),
-                            {'email': 'marcin@wrongmail.org',}
+                            {'email': bad_email, }
         )
 
-        response.mustcontain("This e-mail address doesn't exist")
+        msg = validators.ValidSystemEmail()._messages['non_existing_email']
+        msg = h.html_escape(msg % {'email': bad_email})
+        response.mustcontain()
 
     def test_forgot_password(self):
         response = self.app.get(url(controller='login',
@@ -236,7 +242,7 @@
 
         response = self.app.post(url(controller='login',
                                      action='password_reset'),
-                                 {'email':email, })
+                                 {'email': email, })
 
         self.checkSessionFlash(response, 'Your password reset link was sent')
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/tests/functional/test_pullrequests.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,9 @@
+from rhodecode.tests import *
+
+
+class TestPullrequestsController(TestController):
+
+    def test_index(self):
+        self.log_user()
+        response = self.app.get(url(controller='pullrequests', action='index',
+                                    repo_name=HG_REPO))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/tests/nose_parametrized.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,238 @@
+import re
+import new
+import inspect
+import logging
+import logging.handlers
+from functools import wraps
+
+from nose.tools import nottest
+from unittest import TestCase
+
+
+def _terrible_magic_get_defining_classes():
+    """ Returns the set of parent classes of the class currently being defined.
+        Will likely only work if called from the ``parameterized`` decorator.
+        This function is entirely @brandon_rhodes's fault, as he suggested
+        the implementation: http://stackoverflow.com/a/8793684/71522
+        """
+    stack = inspect.stack()
+    if len(stack) <= 4:
+        return []
+    frame = stack[3]
+    code_context = frame[4][0].strip()
+    if not code_context.startswith("class "):
+        return []
+    _, parents = code_context.split("(", 1)
+    parents, _ = parents.rsplit(")", 1)
+    return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals)
+
+
+def parameterized(input):
+    """ Parameterize a test case:
+        >>> add1_tests = [(1, 2), (2, 3)]
+        >>> class TestFoo(object):
+        ...     @parameterized(add1_tests)
+        ...     def test_add1(self, input, expected):
+        ...         assert_equal(add1(input), expected)
+        >>> @parameterized(add1_tests)
+        ... def test_add1(input, expected):
+        ...     assert_equal(add1(input), expected)
+        >>>
+        """
+
+    if not hasattr(input, "__iter__"):
+        raise ValueError("expected iterable input; got %r" % (input,))
+
+    def parameterized_helper(f):
+        attached_instance_method = [False]
+
+        parent_classes = _terrible_magic_get_defining_classes()
+        if any(issubclass(cls, TestCase) for cls in parent_classes):
+            raise Exception("Warning: '@parameterized' tests won't work "
+                            "inside subclasses of 'TestCase' - use "
+                            "'@parameterized.expand' instead")
+
+        @wraps(f)
+        def parameterized_helper_method(self=None):
+            if self is not None and not attached_instance_method[0]:
+                # confusingly, we need to create a named instance method and
+                # attach that to the class...
+                cls = self.__class__
+                im_f = new.instancemethod(f, None, cls)
+                setattr(cls, f.__name__, im_f)
+                attached_instance_method[0] = True
+            for args in input:
+                if isinstance(args, basestring):
+                    args = [args]
+                # ... then pull that named instance method off, turning it into
+                # a bound method ...
+                if self is not None:
+                    args = [getattr(self, f.__name__)] + list(args)
+                else:
+                    args = [f] + list(args)
+                # ... then yield that as a tuple. If those steps aren't
+                # followed precicely, Nose gets upset and doesn't run the test
+                # or doesn't run setup methods.
+                yield tuple(args)
+
+        f.__name__ = "_helper_for_%s" % (f.__name__,)
+        parameterized_helper_method.parameterized_input = input
+        parameterized_helper_method.parameterized_func = f
+        return parameterized_helper_method
+
+    return parameterized_helper
+
+
+def to_safe_name(s):
+    return re.sub("[^a-zA-Z0-9_]", "", s)
+
+
+def parameterized_expand_helper(func_name, func, args):
+    def parameterized_expand_helper_helper(self=()):
+        if self != ():
+            self = (self,)
+        return func(*(self + args))
+    parameterized_expand_helper_helper.__name__ = func_name
+    return parameterized_expand_helper_helper
+
+
+def parameterized_expand(input):
+    """ A "brute force" method of parameterizing test cases. Creates new test
+        cases and injects them into the namespace that the wrapped function
+        is being defined in. Useful for parameterizing tests in subclasses
+        of 'UnitTest', where Nose test generators don't work.
+
+        >>> @parameterized.expand([("foo", 1, 2)])
+        ... def test_add1(name, input, expected):
+        ...     actual = add1(input)
+        ...     assert_equal(actual, expected)
+        ...
+        >>> locals()
+        ... 'test_add1_foo_0': <function ...> ...
+        >>>
+        """
+
+    def parameterized_expand_wrapper(f):
+        stack = inspect.stack()
+        frame = stack[1]
+        frame_locals = frame[0].f_locals
+
+        base_name = f.__name__
+        for num, args in enumerate(input):
+            name_suffix = "_%s" % (num,)
+            if len(args) > 0 and isinstance(args[0], basestring):
+                name_suffix += "_" + to_safe_name(args[0])
+            name = base_name + name_suffix
+            new_func = parameterized_expand_helper(name, f, args)
+            frame_locals[name] = new_func
+        return nottest(f)
+    return parameterized_expand_wrapper
+
+parameterized.expand = parameterized_expand
+
+
+def assert_contains(haystack, needle):
+    if needle not in haystack:
+        raise AssertionError("%r not in %r" % (needle, haystack))
+
+
+def assert_not_contains(haystack, needle):
+    if needle in haystack:
+        raise AssertionError("%r in %r" % (needle, haystack))
+
+
+def imported_from_test():
+    """ Returns true if it looks like this module is being imported by unittest
+        or nose. """
+    import re
+    import inspect
+    nose_re = re.compile(r"\bnose\b")
+    unittest_re = re.compile(r"\bunittest2?\b")
+    for frame in inspect.stack():
+        file = frame[1]
+        if nose_re.search(file) or unittest_re.search(file):
+            return True
+    return False
+
+
+def assert_raises(func, exc_type, str_contains=None, repr_contains=None):
+    try:
+        func()
+    except exc_type as e:
+        if str_contains is not None and str_contains not in str(e):
+            raise AssertionError("%s raised, but %r does not contain %r"
+                                 % (exc_type, str(e), str_contains))
+        if repr_contains is not None and repr_contains not in repr(e):
+            raise AssertionError("%s raised, but %r does not contain %r"
+                                 % (exc_type, repr(e), repr_contains))
+        return e
+    else:
+        raise AssertionError("%s not raised" % (exc_type,))
+
+
+log_handler = None
+
+
+def setup_logging():
+    """ Configures a log handler which will capure log messages during a test.
+        The ``logged_messages`` and ``assert_no_errors_logged`` functions can be
+        used to make assertions about these logged messages.
+
+        For example::
+
+            from ensi_common.testing import (
+                setup_logging, teardown_logging, assert_no_errors_logged,
+                assert_logged,
+            )
+
+            class TestWidget(object):
+                def setup(self):
+                    setup_logging()
+
+                def teardown(self):
+                    assert_no_errors_logged()
+                    teardown_logging()
+
+                def test_that_will_fail(self):
+                    log.warning("this warning message will trigger a failure")
+
+                def test_that_will_pass(self):
+                    log.info("but info messages are ok")
+                    assert_logged("info messages are ok")
+        """
+
+    global log_handler
+    if log_handler is not None:
+        logging.getLogger().removeHandler(log_handler)
+    log_handler = logging.handlers.BufferingHandler(1000)
+    formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s")
+    log_handler.setFormatter(formatter)
+    logging.getLogger().addHandler(log_handler)
+
+
+def teardown_logging():
+    global log_handler
+    if log_handler is not None:
+        logging.getLogger().removeHandler(log_handler)
+        log_handler = None
+
+
+def logged_messages():
+    assert log_handler, "setup_logging not called"
+    return [(log_handler.format(record), record) for record in log_handler.buffer]
+
+
+def assert_no_errors_logged():
+    for _, record in logged_messages():
+        if record.levelno >= logging.WARNING:
+            # Assume that the nose log capture plugin is being used, so it will
+            # show the exception.
+            raise AssertionError("an unexpected error was logged")
+
+
+def assert_logged(expected_msg_contents):
+    for msg, _ in logged_messages():
+        if expected_msg_contents in msg:
+            return
+    raise AssertionError("no logged message contains %r"
+                         % (expected_msg_contents,))
--- a/rhodecode/tests/test_models.py	Mon Jun 18 00:33:19 2012 +0200
+++ b/rhodecode/tests/test_models.py	Mon Jun 18 00:35:13 2012 +0200
@@ -6,8 +6,8 @@
 from rhodecode.model.repo import RepoModel
 from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
     UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm,\
-    Repository
-from sqlalchemy.exc import IntegrityError
+    Repository, UserEmailMap
+from sqlalchemy.exc import IntegrityError, DatabaseError
 from rhodecode.model.user import UserModel
 
 from rhodecode.model.meta import Session
@@ -182,7 +182,8 @@
         super(TestUser, self).__init__(methodName=methodName)
 
     def test_create_and_remove(self):
-        usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe',
+        usr = UserModel().create_or_update(username=u'test_user',
+                                           password=u'qweqwe',
                                      email=u'u232@rhodecode.org',
                                      name=u'u1', lastname=u'u1')
         Session.commit()
@@ -202,6 +203,50 @@
 
         self.assertEqual(UsersGroupMember.query().all(), [])
 
+    def test_additonal_email_as_main(self):
+        usr = UserModel().create_or_update(username=u'test_user',
+                                           password=u'qweqwe',
+                                     email=u'main_email@rhodecode.org',
+                                     name=u'u1', lastname=u'u1')
+        Session.commit()
+
+        def do():
+            m = UserEmailMap()
+            m.email = u'main_email@rhodecode.org'
+            m.user = usr
+            Session.add(m)
+            Session.commit()
+        self.assertRaises(AttributeError, do)
+
+        UserModel().delete(usr.user_id)
+        Session.commit()
+
+    def test_extra_email_map(self):
+        usr = UserModel().create_or_update(username=u'test_user',
+                                           password=u'qweqwe',
+                                     email=u'main_email@rhodecode.org',
+                                     name=u'u1', lastname=u'u1')
+        Session.commit()
+
+        m = UserEmailMap()
+        m.email = u'main_email2@rhodecode.org'
+        m.user = usr
+        Session.add(m)
+        Session.commit()
+
+        u = User.get_by_email(email='main_email@rhodecode.org')
+        self.assertEqual(usr.user_id, u.user_id)
+        self.assertEqual(usr.username, u.username)
+
+        u = User.get_by_email(email='main_email2@rhodecode.org')
+        self.assertEqual(usr.user_id, u.user_id)
+        self.assertEqual(usr.username, u.username)
+        u = User.get_by_email(email='main_email3@rhodecode.org')
+        self.assertEqual(None, u)
+
+        UserModel().delete(usr.user_id)
+        Session.commit()
+
 
 class TestNotifications(unittest.TestCase):
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/tests/test_validators.py	Mon Jun 18 00:35:13 2012 +0200
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+import unittest
+import formencode
+
+from rhodecode.tests import *
+
+from rhodecode.model import validators as v
+from rhodecode.model.users_group import UsersGroupModel
+
+from rhodecode.model.meta import Session
+from rhodecode.model.repos_group import ReposGroupModel
+from rhodecode.config.routing import ADMIN_PREFIX
+
+
+class TestReposGroups(unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_Message_extractor(self):
+        validator = v.ValidUsername()
+        self.assertRaises(formencode.Invalid, validator.to_python, 'default')
+
+        class StateObj(object):
+            pass
+
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, 'default', StateObj)
+
+    def test_ValidUsername(self):
+        validator = v.ValidUsername()
+
+        self.assertRaises(formencode.Invalid, validator.to_python, 'default')
+        self.assertRaises(formencode.Invalid, validator.to_python, 'new_user')
+        self.assertRaises(formencode.Invalid, validator.to_python, '.,')
+        self.assertRaises(formencode.Invalid, validator.to_python,
+                          TEST_USER_ADMIN_LOGIN)
+        self.assertEqual('test', validator.to_python('test'))
+
+        validator = v.ValidUsername(edit=True, old_data={'user_id': 1})
+
+    def test_ValidRepoUser(self):
+        validator = v.ValidRepoUser()
+        self.assertRaises(formencode.Invalid, validator.to_python, 'nouser')
+        self.assertEqual(TEST_USER_ADMIN_LOGIN,
+                         validator.to_python(TEST_USER_ADMIN_LOGIN))
+
+    def test_ValidUsersGroup(self):
+        validator = v.ValidUsersGroup()
+        self.assertRaises(formencode.Invalid, validator.to_python, 'default')
+        self.assertRaises(formencode.Invalid, validator.to_python, '.,')
+
+        gr = UsersGroupModel().create('test')
+        gr2 = UsersGroupModel().create('tes2')
+        Session.commit()
+        self.assertRaises(formencode.Invalid, validator.to_python, 'test')
+        assert gr.users_group_id != None
+        validator = v.ValidUsersGroup(edit=True,
+                                    old_data={'users_group_id':
+                                              gr2.users_group_id})
+
+        self.assertRaises(formencode.Invalid, validator.to_python, 'test')
+        self.assertRaises(formencode.Invalid, validator.to_python, 'TesT')
+        self.assertRaises(formencode.Invalid, validator.to_python, 'TEST')
+        UsersGroupModel().delete(gr)
+        UsersGroupModel().delete(gr2)
+        Session.commit()
+
+    def test_ValidReposGroup(self):
+        validator = v.ValidReposGroup()
+        model = ReposGroupModel()
+        self.assertRaises(formencode.Invalid, validator.to_python,
+                          {'group_name': HG_REPO, })
+        gr = model.create(group_name='test_gr', group_description='desc',
+                          parent=None,
+                          just_db=True)
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, {'group_name': gr.group_name, })
+
+        validator = v.ValidReposGroup(edit=True,
+                                      old_data={'group_id':  gr.group_id})
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, {
+                                        'group_name': gr.group_name + 'n',
+                                        'group_parent_id': gr.group_id
+                                        })
+        model.delete(gr)
+
+    def test_ValidPassword(self):
+        validator = v.ValidPassword()
+        self.assertEqual('lol', validator.to_python('lol'))
+        self.assertEqual(None, validator.to_python(None))
+        self.assertRaises(formencode.Invalid, validator.to_python, 'ąćżź')
+
+    def test_ValidPasswordsMatch(self):
+        validator = v.ValidPasswordsMatch()
+        self.assertRaises(formencode.Invalid,
+                    validator.to_python, {'password': 'pass',
+                                          'password_confirmation': 'pass2'})
+
+        self.assertRaises(formencode.Invalid,
+                    validator.to_python, {'new_password': 'pass',
+                                          'password_confirmation': 'pass2'})
+
+        self.assertEqual({'new_password': 'pass',
+                          'password_confirmation': 'pass'},
+                    validator.to_python({'new_password': 'pass',
+                                         'password_confirmation': 'pass'}))
+
+        self.assertEqual({'password': 'pass',
+                          'password_confirmation': 'pass'},
+                    validator.to_python({'password': 'pass',
+                                         'password_confirmation': 'pass'}))
+
+    def test_ValidAuth(self):
+        validator = v.ValidAuth()
+        valid_creds = {
+            'username': TEST_USER_REGULAR2_LOGIN,
+            'password': TEST_USER_REGULAR2_PASS,
+        }
+        invalid_creds = {
+            'username': 'err',
+            'password': 'err',
+        }
+        self.assertEqual(valid_creds, validator.to_python(valid_creds))
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, invalid_creds)
+
+    def test_ValidAuthToken(self):
+        validator = v.ValidAuthToken()
+        # this is untestable without a threadlocal
+#        self.assertRaises(formencode.Invalid,
+#                          validator.to_python, 'BadToken')
+        validator
+
+    def test_ValidRepoName(self):
+        validator = v.ValidRepoName()
+
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, {'repo_name': ''})
+
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, {'repo_name': HG_REPO})
+
+        gr = ReposGroupModel().create(group_name='group_test',
+                                      group_description='desc',
+                                      parent=None,)
+        self.assertRaises(formencode.Invalid,
+                          validator.to_python, {'repo_name': gr.group_name})
+
+        #TODO: write an error case for that ie. create a repo withinh a group
+#        self.assertRaises(formencode.Invalid,
+#                          validator.to_python, {'repo_name': 'some',
+#                                                'repo_group': gr.group_id})
+
+    def test_ValidForkName(self):
+        # this uses ValidRepoName validator
+        assert True
+
+    @parameterized.expand([
+        ('test', 'test'), ('lolz!', 'lolz'), ('  aavv', 'aavv'),
+        ('ala ma kota', 'ala-ma-kota'), ('@nooo', 'nooo'),
+        ('$!haha lolz !', 'haha-lolz'), ('$$$$$', ''), ('{}OK!', 'OK'),
+        ('/]re po', 're-po')])
+    def test_SlugifyName(self, name, expected):
+        validator = v.SlugifyName()
+        self.assertEqual(expected, validator.to_python(name))
+
+    def test_ValidCloneUri(self):
+            assert False
+
+    def test_ValidForkType(self):
+            validator = v.ValidForkType(old_data={'repo_type': 'hg'})
+            self.assertEqual('hg', validator.to_python('hg'))
+            self.assertRaises(formencode.Invalid, validator.to_python, 'git')
+
+    def test_ValidPerms(self):
+            assert False
+
+    def test_ValidSettings(self):
+        validator = v.ValidSettings()
+        self.assertEqual({'pass': 'pass'},
+                         validator.to_python(value={'user': 'test',
+                                                    'pass': 'pass'}))
+
+        self.assertEqual({'user2': 'test', 'pass': 'pass'},
+                         validator.to_python(value={'user2': 'test',
+                                                    'pass': 'pass'}))
+
+    def test_ValidPath(self):
+            validator = v.ValidPath()
+            self.assertEqual(TESTS_TMP_PATH,
+                             validator.to_python(TESTS_TMP_PATH))
+            self.assertRaises(formencode.Invalid, validator.to_python,
+                              '/no_such_dir')
+
+    def test_UniqSystemEmail(self):
+        validator = v.UniqSystemEmail(old_data={})
+
+        self.assertEqual('mail@python.org',
+                         validator.to_python('MaiL@Python.org'))
+
+        email = TEST_USER_REGULAR2_EMAIL
+        self.assertRaises(formencode.Invalid, validator.to_python, email)
+
+    def test_ValidSystemEmail(self):
+        validator = v.ValidSystemEmail()
+        email = TEST_USER_REGULAR2_EMAIL
+
+        self.assertEqual(email, validator.to_python(email))
+        self.assertRaises(formencode.Invalid, validator.to_python, 'err')
+
+    def test_LdapLibValidator(self):
+        validator = v.LdapLibValidator()
+        self.assertRaises(v.LdapImportError, validator.to_python, 'err')
+
+    def test_AttrLoginValidator(self):
+        validator = v.AttrLoginValidator()
+        self.assertRaises(formencode.Invalid, validator.to_python, 123)