changeset 2726:aa17c7a1b8a5 beta

Implemented basic locking functionality. - Reimplemented how githooks behave - emaulate pre-receive hook - install missing git hooks if they aren't already in repo
author Marcin Kuzminski <marcin@python-works.com>
date Wed, 22 Aug 2012 00:30:02 +0200
parents 3853e37db97c
children 5899fe08f063
files docs/index.rst docs/usage/locking.rst rhodecode/config/pre_receive_tmpl.py rhodecode/config/routing.py rhodecode/controllers/admin/repos.py rhodecode/lib/auth.py rhodecode/lib/base.py rhodecode/lib/db_manage.py rhodecode/lib/exceptions.py rhodecode/lib/helpers.py rhodecode/lib/hooks.py rhodecode/lib/middleware/pygrack.py rhodecode/lib/middleware/simplegit.py rhodecode/lib/middleware/simplehg.py rhodecode/lib/utils2.py rhodecode/model/db.py rhodecode/model/forms.py rhodecode/model/scm.py rhodecode/templates/admin/repos/repo_edit.html rhodecode/templates/admin/settings/settings.html
diffstat 20 files changed, 489 insertions(+), 107 deletions(-) [+]
line wrap: on
line diff
--- a/docs/index.rst	Tue Aug 21 19:36:21 2012 +0200
+++ b/docs/index.rst	Wed Aug 22 00:30:02 2012 +0200
@@ -22,6 +22,7 @@
    usage/general
    usage/git_support
    usage/performance
+   usage/locking
    usage/statistics
    usage/backup
    usage/debugging
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/usage/locking.rst	Wed Aug 22 00:30:02 2012 +0200
@@ -0,0 +1,41 @@
+.. _locking:
+
+===================================
+RhodeCode repository locking system
+===================================
+
+
+| Repos with **locking function=disabled** is the default, that's how repos work 
+  today.
+| Repos with **locking function=enabled** behaves like follows:
+
+Repos have a state called `locked` that can be true or false.
+The hg/git commands `hg/git clone`, `hg/git pull`, and `hg/git push` 
+influence this state:
+
+- The command `hg/git pull <repo>` will lock that repo (locked=true) 
+  if the user has write/admin permissions on this repo
+
+- The command `hg/git clone <repo>` will lock that repo (locked=true) if the 
+  user has write/admin permissions on this repo
+
+
+RhodeCode will remember the user id who locked the repo
+only this specific user can unlock the repo (locked=false) by calling 
+
+- `hg/git push <repo>` 
+
+every other command on that repo from this user and 
+every command from any other user will result in http return code 423 (locked)
+
+
+additionally the http error includes the <user> that locked the repo 
+(e.g. “repository <repo> locked by user <user>”)
+
+
+So the scenario of use for repos with `locking function` enabled is that 
+every initial clone and every pull gives users (with write permission)
+the exclusive right to do a push.
+
+
+Each repo can be manually unlocked by admin from the repo settings menu.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/config/pre_receive_tmpl.py	Wed Aug 22 00:30:02 2012 +0200
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+import os
+import sys
+
+try:
+    import rhodecode
+    RC_HOOK_VER = '_TMPL_'
+    os.environ['RC_HOOK_VER'] = RC_HOOK_VER
+    from rhodecode.lib.hooks import handle_git_pre_receive
+except ImportError:
+    rhodecode = None
+
+
+def main():
+    if rhodecode is None:
+        # exit with success if we cannot import rhodecode !!
+        # this allows simply push to this repo even without
+        # rhodecode
+        sys.exit(0)
+
+    repo_path = os.path.abspath('.')
+    push_data = sys.stdin.readlines()
+    # os.environ is modified here by a subprocess call that
+    # runs git and later git executes this hook.
+    # Environ get's some additional info from rhodecode system
+    # like IP or username from basic-auth
+    handle_git_pre_receive(repo_path, push_data, os.environ)
+    sys.exit(0)
+
+if __name__ == '__main__':
+    main()
--- a/rhodecode/config/routing.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/config/routing.py	Wed Aug 22 00:30:02 2012 +0200
@@ -138,7 +138,9 @@
         m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}",
                   action="repo_as_fork", conditions=dict(method=["PUT"],
                                                       function=check_repo))
-
+        m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
+                  action="repo_locking", conditions=dict(method=["PUT"],
+                                                      function=check_repo))
     with rmap.submapper(path_prefix=ADMIN_PREFIX,
                         controller='admin/repos_groups') as m:
         m.connect("repos_groups", "/repos_groups",
--- a/rhodecode/controllers/admin/repos.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/controllers/admin/repos.py	Wed Aug 22 00:30:02 2012 +0200
@@ -381,6 +381,7 @@
             RepoModel().delete_stats(repo_name)
             Session().commit()
         except Exception, e:
+            log.error(traceback.format_exc())
             h.flash(_('An error occurred during deletion of repository stats'),
                     category='error')
         return redirect(url('edit_repo', repo_name=repo_name))
@@ -397,11 +398,32 @@
             ScmModel().mark_for_invalidation(repo_name)
             Session().commit()
         except Exception, e:
+            log.error(traceback.format_exc())
             h.flash(_('An error occurred during cache invalidation'),
                     category='error')
         return redirect(url('edit_repo', repo_name=repo_name))
 
     @HasPermissionAllDecorator('hg.admin')
+    def repo_locking(self, repo_name):
+        """
+        Unlock repository when it is locked !
+
+        :param repo_name:
+        """
+
+        try:
+            repo = Repository.get_by_repo_name(repo_name)
+            if request.POST.get('set_lock'):
+                Repository.lock(repo, c.rhodecode_user.user_id)
+            elif request.POST.get('set_unlock'):
+                Repository.unlock(repo)
+        except Exception, e:
+            log.error(traceback.format_exc())
+            h.flash(_('An error occurred during unlocking'),
+                    category='error')
+        return redirect(url('edit_repo', repo_name=repo_name))
+
+    @HasPermissionAllDecorator('hg.admin')
     def repo_public_journal(self, repo_name):
         """
         Set's this repository to be visible in public journal,
--- a/rhodecode/lib/auth.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/auth.py	Wed Aug 22 00:30:02 2012 +0200
@@ -807,7 +807,7 @@
         return self.check_permissions()
 
     def check_permissions(self):
-        log.debug('checking mercurial protocol '
+        log.debug('checking VCS protocol '
                   'permissions %s for user:%s repository:%s', self.user_perms,
                                                 self.username, self.repo_name)
         if self.required_perms.intersection(self.user_perms):
--- a/rhodecode/lib/base.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/base.py	Wed Aug 22 00:30:02 2012 +0200
@@ -8,6 +8,7 @@
 
 from paste.auth.basic import AuthBasicAuthenticator
 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
+from webob.exc import HTTPClientError
 from paste.httpheaders import WWW_AUTHENTICATE
 
 from pylons import config, tmpl_context as c, request, session, url
@@ -17,15 +18,17 @@
 
 from rhodecode import __version__, BACKENDS
 
-from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
+from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
+    safe_str
 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
     HasPermissionAnyMiddleware, CookieStoreWrapper
 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
 from rhodecode.model import meta
 
-from rhodecode.model.db import Repository, RhodeCodeUi
+from rhodecode.model.db import Repository, RhodeCodeUi, User
 from rhodecode.model.notification import NotificationModel
 from rhodecode.model.scm import ScmModel
+from rhodecode.model.meta import Session
 
 log = logging.getLogger(__name__)
 
@@ -159,6 +162,49 @@
             return False
         return True
 
+    def _check_locking_state(self, environ, action, repo, user_id):
+        """
+        Checks locking on this repository, if locking is enabled and lock is
+        present returns a tuple of make_lock, locked, locked_by.
+        make_lock can have 3 states None (do nothing) True, make lock
+        False release lock, This value is later propagated to hooks, which
+        do the locking. Think about this as signals passed to hooks what to do.
+
+        """
+        locked = False
+        make_lock = None
+        repo = Repository.get_by_repo_name(repo)
+        user = User.get(user_id)
+
+        # this is kind of hacky, but due to how mercurial handles client-server
+        # server see all operation on changeset; bookmarks, phases and
+        # obsolescence marker in different transaction, we don't want to check
+        # locking on those
+        obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
+        locked_by = repo.locked
+        if repo and repo.enable_locking and not obsolete_call:
+            if action == 'push':
+                #check if it's already locked !, if it is compare users
+                user_id, _date = repo.locked
+                if user.user_id == user_id:
+                    log.debug('Got push from user, now unlocking' % (user))
+                    # unlock if we have push from user who locked
+                    make_lock = False
+                else:
+                    # we're not the same user who locked, ban with 423 !
+                    locked = True
+            if action == 'pull':
+                if repo.locked[0] and repo.locked[1]:
+                    locked = True
+                else:
+                    log.debug('Setting lock on repo %s by %s' % (repo, user))
+                    make_lock = True
+
+        else:
+            log.debug('Repository %s do not have locking enabled' % (repo))
+
+        return make_lock, locked, locked_by
+
     def __call__(self, environ, start_response):
         start = time.time()
         try:
--- a/rhodecode/lib/db_manage.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/db_manage.py	Wed Aug 22 00:30:02 2012 +0200
@@ -307,37 +307,47 @@
         hooks1.ui_key = hooks1_key
         hooks1.ui_value = 'hg update >&2'
         hooks1.ui_active = False
+        self.sa.add(hooks1)
 
         hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
         hooks2_ = self.sa.query(RhodeCodeUi)\
             .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
-
         hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
         hooks2.ui_section = 'hooks'
         hooks2.ui_key = hooks2_key
         hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
+        self.sa.add(hooks2)
 
         hooks3 = RhodeCodeUi()
         hooks3.ui_section = 'hooks'
         hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
         hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
+        self.sa.add(hooks3)
 
         hooks4 = RhodeCodeUi()
         hooks4.ui_section = 'hooks'
-        hooks4.ui_key = RhodeCodeUi.HOOK_PULL
-        hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
+        hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
+        hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
+        self.sa.add(hooks4)
 
-        # For mercurial 1.7 set backward comapatibility with format
-        dotencode_disable = RhodeCodeUi()
-        dotencode_disable.ui_section = 'format'
-        dotencode_disable.ui_key = 'dotencode'
-        dotencode_disable.ui_value = 'false'
+        hooks5 = RhodeCodeUi()
+        hooks5.ui_section = 'hooks'
+        hooks5.ui_key = RhodeCodeUi.HOOK_PULL
+        hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
+        self.sa.add(hooks5)
+
+        hooks6 = RhodeCodeUi()
+        hooks6.ui_section = 'hooks'
+        hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
+        hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
+        self.sa.add(hooks6)
 
         # enable largefiles
         largefiles = RhodeCodeUi()
         largefiles.ui_section = 'extensions'
         largefiles.ui_key = 'largefiles'
         largefiles.ui_value = ''
+        self.sa.add(largefiles)
 
         # enable hgsubversion disabled by default
         hgsubversion = RhodeCodeUi()
@@ -345,6 +355,7 @@
         hgsubversion.ui_key = 'hgsubversion'
         hgsubversion.ui_value = ''
         hgsubversion.ui_active = False
+        self.sa.add(hgsubversion)
 
         # enable hggit disabled by default
         hggit = RhodeCodeUi()
@@ -352,13 +363,6 @@
         hggit.ui_key = 'hggit'
         hggit.ui_value = ''
         hggit.ui_active = False
-
-        self.sa.add(hooks1)
-        self.sa.add(hooks2)
-        self.sa.add(hooks3)
-        self.sa.add(hooks4)
-        self.sa.add(largefiles)
-        self.sa.add(hgsubversion)
         self.sa.add(hggit)
 
     def create_ldap_options(self, skip_existing=False):
@@ -461,6 +465,11 @@
         paths.ui_key = '/'
         paths.ui_value = path
 
+        phases = RhodeCodeUi()
+        phases.ui_section = 'phases'
+        phases.ui_key = 'publish'
+        phases.ui_value = False
+
         sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
         sett2 = RhodeCodeSetting('title', 'RhodeCode')
         sett3 = RhodeCodeSetting('ga_code', '')
--- a/rhodecode/lib/exceptions.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/exceptions.py	Wed Aug 22 00:30:02 2012 +0200
@@ -23,6 +23,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+from webob.exc import HTTPClientError
+
 
 class LdapUsernameError(Exception):
     pass
@@ -53,4 +55,17 @@
 
 
 class StatusChangeOnClosedPullRequestError(Exception):
-    pass
\ No newline at end of file
+    pass
+
+
+class HTTPLockedRC(HTTPClientError):
+    """
+    Special Exception For locked Repos in RhodeCode
+    """
+    code = 423
+    title = explanation = 'Repository Locked'
+
+    def __init__(self, reponame, username, *args, **kwargs):
+        self.title = self.explanation = ('Repository `%s` locked by '
+                                         'user `%s`' % (reponame, username))
+        super(HTTPLockedRC, self).__init__(*args, **kwargs)
--- a/rhodecode/lib/helpers.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/helpers.py	Wed Aug 22 00:30:02 2012 +0200
@@ -41,7 +41,7 @@
 from rhodecode.lib.annotate import annotate_highlight
 from rhodecode.lib.utils import repo_name_slug
 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
-    get_changeset_safe
+    get_changeset_safe, datetime_to_time, time_to_datetime
 from rhodecode.lib.markup_renderer import MarkupRenderer
 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
 from rhodecode.lib.vcs.backends.base import BaseChangeset
@@ -439,6 +439,19 @@
     return _author
 
 
+def person_by_id(id_):
+    # attr to return from fetched user
+    person_getter = lambda usr: usr.username
+
+    #maybe it's an ID ?
+    if str(id_).isdigit() or isinstance(id_, int):
+        id_ = int(id_)
+        user = User.get(id_)
+        if user is not None:
+            return person_getter(user)
+    return id_
+
+
 def desc_stylize(value):
     """
     converts tags from value into html equivalent
--- a/rhodecode/lib/hooks.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/hooks.py	Wed Aug 22 00:30:02 2012 +0200
@@ -34,6 +34,9 @@
 from rhodecode.lib.utils import action_logger
 from rhodecode.lib.vcs.backends.base import EmptyChangeset
 from rhodecode.lib.compat import json
+from rhodecode.model.db import Repository, User
+from rhodecode.lib.utils2 import safe_str
+from rhodecode.lib.exceptions import HTTPLockedRC
 
 
 def _get_scm_size(alias, root_path):
@@ -84,6 +87,59 @@
     sys.stdout.write(msg)
 
 
+def pre_push(ui, repo, **kwargs):
+    # pre push function, currently used to ban pushing when
+    # repository is locked
+    try:
+        rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
+    except:
+        rc_extras = {}
+    extras = dict(repo.ui.configitems('rhodecode_extras'))
+
+    if 'username' in extras:
+        username = extras['username']
+        repository = extras['repository']
+        scm = extras['scm']
+        locked_by = extras['locked_by']
+    elif 'username' in rc_extras:
+        username = rc_extras['username']
+        repository = rc_extras['repository']
+        scm = rc_extras['scm']
+        locked_by = rc_extras['locked_by']
+    else:
+        raise Exception('Missing data in repo.ui and os.environ')
+
+    usr = User.get_by_username(username)
+
+    if locked_by[0] and usr.user_id != int(locked_by[0]):
+        raise HTTPLockedRC(username, repository)
+
+
+def pre_pull(ui, repo, **kwargs):
+    # pre push function, currently used to ban pushing when
+    # repository is locked
+    try:
+        rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
+    except:
+        rc_extras = {}
+    extras = dict(repo.ui.configitems('rhodecode_extras'))
+    if 'username' in extras:
+        username = extras['username']
+        repository = extras['repository']
+        scm = extras['scm']
+        locked_by = extras['locked_by']
+    elif 'username' in rc_extras:
+        username = rc_extras['username']
+        repository = rc_extras['repository']
+        scm = rc_extras['scm']
+        locked_by = rc_extras['locked_by']
+    else:
+        raise Exception('Missing data in repo.ui and os.environ')
+
+    if locked_by[0]:
+        raise HTTPLockedRC(username, repository)
+
+
 def log_pull_action(ui, repo, **kwargs):
     """
     Logs user last pull action
@@ -100,15 +156,17 @@
         username = extras['username']
         repository = extras['repository']
         scm = extras['scm']
+        make_lock = extras['make_lock']
     elif 'username' in rc_extras:
         username = rc_extras['username']
         repository = rc_extras['repository']
         scm = rc_extras['scm']
+        make_lock = rc_extras['make_lock']
     else:
         raise Exception('Missing data in repo.ui and os.environ')
-
+    user = User.get_by_username(username)
     action = 'pull'
-    action_logger(username, action, repository, extras['ip'], commit=True)
+    action_logger(user, action, repository, extras['ip'], commit=True)
     # extension hook call
     from rhodecode import EXTENSIONS
     callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
@@ -117,6 +175,12 @@
         kw = {}
         kw.update(extras)
         callback(**kw)
+
+    if make_lock is True:
+        Repository.lock(Repository.get_by_repo_name(repository), user.user_id)
+        #msg = 'Made lock on repo `%s`' % repository
+        #sys.stdout.write(msg)
+
     return 0
 
 
@@ -138,10 +202,12 @@
         username = extras['username']
         repository = extras['repository']
         scm = extras['scm']
+        make_lock = extras['make_lock']
     elif 'username' in rc_extras:
         username = rc_extras['username']
         repository = rc_extras['repository']
         scm = rc_extras['scm']
+        make_lock = rc_extras['make_lock']
     else:
         raise Exception('Missing data in repo.ui and os.environ')
 
@@ -179,6 +245,12 @@
         kw = {'pushed_revs': revs}
         kw.update(extras)
         callback(**kw)
+
+    if make_lock is False:
+        Repository.unlock(Repository.get_by_repo_name(repository))
+        msg = 'Released lock on repo `%s`\n' % repository
+        sys.stdout.write(msg)
+
     return 0
 
 
@@ -219,8 +291,13 @@
 
     return 0
 
+handle_git_pre_receive = (lambda repo_path, revs, env:
+    handle_git_receive(repo_path, revs, env, hook_type='pre'))
+handle_git_post_receive = (lambda repo_path, revs, env:
+    handle_git_receive(repo_path, revs, env, hook_type='post'))
 
-def handle_git_post_receive(repo_path, revs, env):
+
+def handle_git_receive(repo_path, revs, env, hook_type='post'):
     """
     A really hacky method that is runned by git post-receive hook and logs
     an push action together with pushed revisions. It's executed by subprocess
@@ -240,7 +317,6 @@
     from rhodecode.model import init_model
     from rhodecode.model.db import RhodeCodeUi
     from rhodecode.lib.utils import make_ui
-    from rhodecode.model.db import Repository
 
     path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE'])
     conf = appconfig('config:%s' % ini_name, relative_to=path)
@@ -255,20 +331,18 @@
         repo_path = repo_path[:-4]
     repo = Repository.get_by_full_path(repo_path)
     _hooks = dict(baseui.configitems('hooks')) or {}
-    # if push hook is enabled via web interface
-    if repo and _hooks.get(RhodeCodeUi.HOOK_PUSH):
 
-        extras = {
-         'username': env['RHODECODE_USER'],
-         'repository': repo.repo_name,
-         'scm': 'git',
-         'action': 'push',
-         'ip': env['RHODECODE_CONFIG_IP'],
-        }
-        for k, v in extras.items():
-            baseui.setconfig('rhodecode_extras', k, v)
-        repo = repo.scm_instance
-        repo.ui = baseui
+    extras = json.loads(env['RHODECODE_EXTRAS'])
+    for k, v in extras.items():
+        baseui.setconfig('rhodecode_extras', k, v)
+    repo = repo.scm_instance
+    repo.ui = baseui
+
+    if hook_type == 'pre':
+        pre_push(baseui, repo)
+
+    # if push hook is enabled via web interface
+    elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
 
         rev_data = []
         for l in revs:
--- a/rhodecode/lib/middleware/pygrack.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/middleware/pygrack.py	Wed Aug 22 00:30:02 2012 +0200
@@ -41,7 +41,7 @@
     git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
     commands = ['git-upload-pack', 'git-receive-pack']
 
-    def __init__(self, repo_name, content_path, username):
+    def __init__(self, repo_name, content_path, extras):
         files = set([f.lower() for f in os.listdir(content_path)])
         if  not (self.git_folder_signature.intersection(files)
                 == self.git_folder_signature):
@@ -50,7 +50,7 @@
         self.valid_accepts = ['application/x-%s-result' %
                               c for c in self.commands]
         self.repo_name = repo_name
-        self.username = username
+        self.extras = extras
 
     def _get_fixedpath(self, path):
         """
@@ -67,7 +67,7 @@
         HTTP /info/refs request.
         """
 
-        git_command = request.GET['service']
+        git_command = request.GET.get('service')
         if git_command not in self.commands:
             log.debug('command %s not allowed' % git_command)
             return exc.HTTPMethodNotAllowed()
@@ -119,9 +119,8 @@
         try:
             gitenv = os.environ
             from rhodecode import CONFIG
-            from rhodecode.lib.base import _get_ip_addr
-            gitenv['RHODECODE_USER'] = self.username
-            gitenv['RHODECODE_CONFIG_IP'] = _get_ip_addr(environ)
+            from rhodecode.lib.compat import json
+            gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras)
             # forget all configs
             gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
             # we need current .ini file used to later initialize rhodecode
@@ -174,7 +173,7 @@
 
 class GitDirectory(object):
 
-    def __init__(self, repo_root, repo_name, username):
+    def __init__(self, repo_root, repo_name, extras):
         repo_location = os.path.join(repo_root, repo_name)
         if not os.path.isdir(repo_location):
             raise OSError(repo_location)
@@ -182,12 +181,12 @@
         self.content_path = repo_location
         self.repo_name = repo_name
         self.repo_location = repo_location
-        self.username = username
+        self.extras = extras
 
     def __call__(self, environ, start_response):
         content_path = self.content_path
         try:
-            app = GitRepository(self.repo_name, content_path, self.username)
+            app = GitRepository(self.repo_name, content_path, self.extras)
         except (AssertionError, OSError):
             if os.path.isdir(os.path.join(content_path, '.git')):
                 app = GitRepository(self.repo_name,
@@ -198,5 +197,5 @@
         return app(environ, start_response)
 
 
-def make_wsgi_app(repo_name, repo_root, username):
-    return GitDirectory(repo_root, repo_name, username)
+def make_wsgi_app(repo_name, repo_root, extras):
+    return GitDirectory(repo_root, repo_name, extras)
--- a/rhodecode/lib/middleware/simplegit.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/middleware/simplegit.py	Wed Aug 22 00:30:02 2012 +0200
@@ -31,6 +31,8 @@
 
 from dulwich import server as dulserver
 from dulwich.web import LimitedInputFilter, GunzipFilter
+from rhodecode.lib.exceptions import HTTPLockedRC
+from rhodecode.lib.hooks import pre_pull
 
 
 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
@@ -102,11 +104,11 @@
 class SimpleGit(BaseVCSController):
 
     def _handle_request(self, environ, start_response):
-
         if not is_git(environ):
             return self.application(environ, start_response)
         if not self._check_ssl(environ, start_response):
             return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
+
         ipaddr = self._get_ip_addr(environ)
         username = None
         self._git_first_op = False
@@ -184,21 +186,39 @@
                 if perm is not True:
                     return HTTPForbidden()(environ, start_response)
 
+        # extras are injected into UI object and later available
+        # in hooks executed by rhodecode
         extras = {
             'ip': ipaddr,
             'username': username,
             'action': action,
             'repository': repo_name,
             'scm': 'git',
+            'make_lock': None,
+            'locked_by': [None, None]
         }
-        # set the environ variables for this request
-        os.environ['RC_SCM_DATA'] = json.dumps(extras)
+
         #===================================================================
         # GIT REQUEST HANDLING
         #===================================================================
         repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
         log.debug('Repository path is %s' % repo_path)
 
+        # CHECK LOCKING only if it's not ANONYMOUS USER
+        if username != User.DEFAULT_USER:
+            log.debug('Checking locking on repository')
+            (make_lock,
+             locked,
+             locked_by) = self._check_locking_state(
+                            environ=environ, action=action,
+                            repo=repo_name, user_id=user.user_id
+                       )
+            # store the make_lock for later evaluation in hooks
+            extras.update({'make_lock': make_lock,
+                           'locked_by': locked_by})
+        # set the environ variables for this request
+        os.environ['RC_SCM_DATA'] = json.dumps(extras)
+        log.debug('HOOKS extras is %s' % extras)
         baseui = make_ui('db')
         self.__inject_extras(repo_path, baseui, extras)
 
@@ -209,13 +229,16 @@
             self._handle_githooks(repo_name, action, baseui, environ)
 
             log.info('%s action on GIT repo "%s"' % (action, repo_name))
-            app = self.__make_app(repo_name, repo_path, username)
+            app = self.__make_app(repo_name, repo_path, extras)
             return app(environ, start_response)
+        except HTTPLockedRC, e:
+            log.debug('Repositry LOCKED ret code 423!')
+            return e(environ, start_response)
         except Exception:
             log.error(traceback.format_exc())
             return HTTPInternalServerError()(environ, start_response)
 
-    def __make_app(self, repo_name, repo_path, username):
+    def __make_app(self, repo_name, repo_path, extras):
         """
         Make an wsgi application using dulserver
 
@@ -227,7 +250,7 @@
         app = make_wsgi_app(
             repo_root=safe_str(self.basepath),
             repo_name=repo_name,
-            username=username,
+            extras=extras,
         )
         app = GunzipFilter(LimitedInputFilter(app))
         return app
@@ -279,6 +302,7 @@
         """
         from rhodecode.lib.hooks import log_pull_action
         service = environ['QUERY_STRING'].split('=')
+
         if len(service) < 2:
             return
 
@@ -288,6 +312,9 @@
         _repo._repo.ui = baseui
 
         _hooks = dict(baseui.configitems('hooks')) or {}
+        if action == 'pull':
+            # stupid git, emulate pre-pull hook !
+            pre_pull(ui=baseui, repo=_repo._repo)
         if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
             log_pull_action(ui=baseui, repo=_repo._repo)
 
--- a/rhodecode/lib/middleware/simplehg.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/middleware/simplehg.py	Wed Aug 22 00:30:02 2012 +0200
@@ -42,6 +42,7 @@
 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
 from rhodecode.lib.compat import json
 from rhodecode.model.db import User
+from rhodecode.lib.exceptions import HTTPLockedRC
 
 
 log = logging.getLogger(__name__)
@@ -157,15 +158,31 @@
             'action': action,
             'repository': repo_name,
             'scm': 'hg',
+            'make_lock': None,
+            'locked_by': [None, None]
         }
-        # set the environ variables for this request
-        os.environ['RC_SCM_DATA'] = json.dumps(extras)
         #======================================================================
         # MERCURIAL REQUEST HANDLING
         #======================================================================
         repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
         log.debug('Repository path is %s' % repo_path)
 
+        # CHECK LOCKING only if it's not ANONYMOUS USER
+        if username != User.DEFAULT_USER:
+            log.debug('Checking locking on repository')
+            (make_lock,
+             locked,
+             locked_by) = self._check_locking_state(
+                            environ=environ, action=action,
+                            repo=repo_name, user_id=user.user_id
+                       )
+            # store the make_lock for later evaluation in hooks
+            extras.update({'make_lock': make_lock,
+                           'locked_by': locked_by})
+
+        # set the environ variables for this request
+        os.environ['RC_SCM_DATA'] = json.dumps(extras)
+        log.debug('HOOKS extras is %s' % extras)
         baseui = make_ui('db')
         self.__inject_extras(repo_path, baseui, extras)
 
@@ -179,6 +196,9 @@
         except RepoError, e:
             if str(e).find('not found') != -1:
                 return HTTPNotFound()(environ, start_response)
+        except HTTPLockedRC, e:
+            log.debug('Repositry LOCKED ret code 423!')
+            return e(environ, start_response)
         except Exception:
             log.error(traceback.format_exc())
             return HTTPInternalServerError()(environ, start_response)
--- a/rhodecode/lib/utils2.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/lib/utils2.py	Wed Aug 22 00:30:02 2012 +0200
@@ -25,7 +25,7 @@
 
 import re
 import time
-from datetime import datetime
+import datetime
 from pylons.i18n.translation import _, ungettext
 from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
@@ -300,7 +300,7 @@
     deltas = {}
 
     # Get date parts deltas
-    now = datetime.now()
+    now = datetime.datetime.now()
     for part in order:
         deltas[part] = getattr(now, part) - getattr(prevdate, part)
 
@@ -435,6 +435,15 @@
         return time.mktime(dt.timetuple())
 
 
+def time_to_datetime(tm):
+    if tm:
+        if isinstance(tm, basestring):
+            try:
+                tm = float(tm)
+            except ValueError:
+                return
+        return datetime.datetime.fromtimestamp(tm)
+
 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
 
 
--- a/rhodecode/model/db.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/model/db.py	Wed Aug 22 00:30:02 2012 +0200
@@ -28,6 +28,7 @@
 import datetime
 import traceback
 import hashlib
+import time
 from collections import defaultdict
 
 from sqlalchemy import *
@@ -232,7 +233,9 @@
     HOOK_UPDATE = 'changegroup.update'
     HOOK_REPO_SIZE = 'changegroup.repo_size'
     HOOK_PUSH = 'changegroup.push_logger'
-    HOOK_PULL = 'preoutgoing.pull_logger'
+    HOOK_PRE_PUSH = 'prechangegroup.pre_push'
+    HOOK_PULL = 'outgoing.pull_logger'
+    HOOK_PRE_PULL = 'preoutgoing.pre_pull'
 
     ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
     ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
@@ -247,17 +250,17 @@
     @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]))
+        q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
+                                     cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
+                                     cls.HOOK_PULL, cls.HOOK_PRE_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_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
+                                      cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
+                                      cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
         q = q.filter(cls.ui_section == 'hooks')
         return q.all()
 
@@ -280,9 +283,13 @@
     __tablename__ = 'users'
     __table_args__ = (
         UniqueConstraint('username'), UniqueConstraint('email'),
+        Index('u_username_idx', 'username'),
+        Index('u_email_idx', 'email'),
         {'extend_existing': True, 'mysql_engine': 'InnoDB',
          'mysql_charset': 'utf8'}
     )
+    DEFAULT_USER = 'default'
+
     user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
     username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
@@ -572,6 +579,7 @@
     __tablename__ = 'repositories'
     __table_args__ = (
         UniqueConstraint('repo_name'),
+        Index('r_repo_name_idx', 'repo_name'),
         {'extend_existing': True, 'mysql_engine': 'InnoDB',
          'mysql_charset': 'utf8'},
     )
@@ -587,6 +595,8 @@
     description = Column("description", String(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)
     landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
+    enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
+    _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
 
     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)
@@ -617,6 +627,21 @@
         return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
                                    self.repo_name)
 
+    @hybrid_property
+    def locked(self):
+        # always should return [user_id, timelocked]
+        if self._locked:
+            _lock_info = self._locked.split(':')
+            return int(_lock_info[0]), _lock_info[1]
+        return [None, None]
+
+    @locked.setter
+    def locked(self, val):
+        if val and isinstance(val, (list, tuple)):
+            self._locked = ':'.join(map(str, val))
+        else:
+            self._locked = None
+
     @classmethod
     def url_sep(cls):
         return URL_SEP
@@ -744,7 +769,7 @@
             if ui_.ui_key == 'push_ssl':
                 # force set push_ssl requirement to False, rhodecode
                 # handles that
-                baseui.setconfig(ui_.ui_section, ui_.ui_key, False)                
+                baseui.setconfig(ui_.ui_section, ui_.ui_key, False)
 
         return baseui
 
@@ -793,6 +818,18 @@
 
         return data
 
+    @classmethod
+    def lock(cls, repo, user_id):
+        repo.locked = [user_id, time.time()]
+        Session().add(repo)
+        Session().commit()
+
+    @classmethod
+    def unlock(cls, repo):
+        repo.locked = None
+        Session().add(repo)
+        Session().commit()
+
     #==========================================================================
     # SCM PROPERTIES
     #==========================================================================
--- a/rhodecode/model/forms.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/model/forms.py	Wed Aug 22 00:30:02 2012 +0200
@@ -182,6 +182,7 @@
         private = v.StringBoolean(if_missing=False)
         enable_statistics = v.StringBoolean(if_missing=False)
         enable_downloads = v.StringBoolean(if_missing=False)
+        enable_locking = v.StringBoolean(if_missing=False)
         landing_rev = v.OneOf(landing_revs, hideList=True)
 
         if edit:
@@ -265,7 +266,7 @@
         hooks_changegroup_update = v.StringBoolean(if_missing=False)
         hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
         hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
-        hooks_preoutgoing_pull_logger = v.StringBoolean(if_missing=False)
+        hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
 
         extensions_largefiles = v.StringBoolean(if_missing=False)
         extensions_hgsubversion = v.StringBoolean(if_missing=False)
--- a/rhodecode/model/scm.py	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/model/scm.py	Wed Aug 22 00:30:02 2012 +0200
@@ -571,34 +571,41 @@
         if not os.path.isdir(loc):
             os.makedirs(loc)
 
-        tmpl = pkg_resources.resource_string(
+        tmpl_post = pkg_resources.resource_string(
             'rhodecode', jn('config', 'post_receive_tmpl.py')
         )
+        tmpl_pre = pkg_resources.resource_string(
+            'rhodecode', jn('config', 'pre_receive_tmpl.py')
+        )
 
-        _hook_file = jn(loc, 'post-receive')
-        _rhodecode_hook = False
-        log.debug('Installing git hook in repo %s' % repo)
-        if os.path.exists(_hook_file):
-            # let's take a look at this hook, maybe it's rhodecode ?
-            log.debug('hook exists, checking if it is from rhodecode')
-            _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
-            with open(_hook_file, 'rb') as f:
-                data = f.read()
-                matches = re.compile(r'(?:%s)\s*=\s*(.*)'
-                                     % 'RC_HOOK_VER').search(data)
-                if matches:
-                    try:
-                        ver = matches.groups()[0]
-                        log.debug('got %s it is rhodecode' % (ver))
-                        _rhodecode_hook = True
-                    except:
-                        log.error(traceback.format_exc())
+        for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
+            _hook_file = jn(loc, '%s-receive' % h_type)
+            _rhodecode_hook = False
+            log.debug('Installing git hook in repo %s' % repo)
+            if os.path.exists(_hook_file):
+                # let's take a look at this hook, maybe it's rhodecode ?
+                log.debug('hook exists, checking if it is from rhodecode')
+                _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
+                with open(_hook_file, 'rb') as f:
+                    data = f.read()
+                    matches = re.compile(r'(?:%s)\s*=\s*(.*)'
+                                         % 'RC_HOOK_VER').search(data)
+                    if matches:
+                        try:
+                            ver = matches.groups()[0]
+                            log.debug('got %s it is rhodecode' % (ver))
+                            _rhodecode_hook = True
+                        except:
+                            log.error(traceback.format_exc())
+            else:
+                # there is no hook in this dir, so we want to create one
+                _rhodecode_hook = True
 
-        if _rhodecode_hook or force_create:
-            log.debug('writing hook file !')
-            with open(_hook_file, 'wb') as f:
-                tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
-                f.write(tmpl)
-            os.chmod(_hook_file, 0755)
-        else:
-            log.debug('skipping writing hook file')
+            if _rhodecode_hook or force_create:
+                log.debug('writing %s hook file !' % h_type)
+                with open(_hook_file, 'wb') as f:
+                    tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
+                    f.write(tmpl)
+                os.chmod(_hook_file, 0755)
+            else:
+                log.debug('skipping writing hook file')
--- a/rhodecode/templates/admin/repos/repo_edit.html	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/templates/admin/repos/repo_edit.html	Wed Aug 22 00:30:02 2012 +0200
@@ -108,6 +108,15 @@
                 </div>
             </div>
             <div class="field">
+                <div class="label label-checkbox">
+                    <label for="enable_locking">${_('Enable locking')}:</label>
+                </div>
+                <div class="checkboxes">
+                    ${h.checkbox('enable_locking',value="True")}
+                    <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span>
+                </div>
+            </div>            
+            <div class="field">
                 <div class="label">
                     <label for="user">${_('Owner')}:</label>
                 </div>
@@ -196,26 +205,31 @@
                 </div>
                <div class="field" style="border:none;color:#888">
                <ul>
-                    <li>${_('''All actions made on this repository will be accessible to everyone in public journal''')}
+                    <li>${_('All actions made on this repository will be accessible to everyone in public journal')}
                     </li>
                </ul>
                </div>
         </div>
         ${h.end_form()}
 
-        <h3>${_('Delete')}</h3>
-        ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')}
+        <h3>${_('Locking')}</h3>
+        ${h.form(url('repo_locking', repo_name=c.repo_info.repo_name),method='put')}
         <div class="form">
            <div class="fields">
-               ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")}
+              %if c.repo_info.locked[0]:
+               ${h.submit('set_unlock' ,_('Unlock locked repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to unlock repository')+"');")}
+               ${'Locked by %s on %s' % (h.person_by_id(c.repo_info.locked[0]),h.fmt_date(h.time_to_datetime(c.repo_info.locked[1])))}
+              %else:
+                ${h.submit('set_lock',_('lock repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to lock repository')+"');")}
+                ${_('Repository is not locked')}
+              %endif
            </div>
            <div class="field" style="border:none;color:#888">
            <ul>
-                <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems.
-                         If you need fully delete it from filesystem please do it manually''')}
+                <li>${_('Force locking on repository. Works only when anonymous access is disabled')}
                 </li>
            </ul>
-           </div>
+           </div>           
         </div>
         ${h.end_form()}
 
@@ -231,10 +245,24 @@
                     <li>${_('''Manually set this repository as a fork of another from the list''')}</li>
                </ul>
                </div>
-        </div>
+        </div>        
         ${h.end_form()}
-
+        
+        <h3>${_('Delete')}</h3>
+        ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')}
+        <div class="form">
+           <div class="fields">
+               ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")}
+           </div>
+           <div class="field" style="border:none;color:#888">
+           <ul>
+                <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems.
+                         If you need fully delete it from filesystem please do it manually''')}
+                </li>
+           </ul>
+           </div>
+        </div>
+        ${h.end_form()}        
 </div>
 
-
 </%def>
--- a/rhodecode/templates/admin/settings/settings.html	Tue Aug 21 19:36:21 2012 +0200
+++ b/rhodecode/templates/admin/settings/settings.html	Wed Aug 22 00:30:02 2012 +0200
@@ -211,8 +211,8 @@
                         <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label>
                     </div>
                     <div class="checkbox">
-                        ${h.checkbox('hooks_preoutgoing_pull_logger','True')}
-                        <label for="hooks_preoutgoing_pull_logger">${_('Log user pull commands')}</label>
+                        ${h.checkbox('hooks_outgoing_pull_logger','True')}
+                        <label for="hooks_outgoing_pull_logger">${_('Log user pull commands')}</label>
                     </div>
 				</div>
                 <div class="input" style="margin-top:10px">