aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/index.rst1
-rw-r--r--docs/usage/locking.rst41
-rw-r--r--rhodecode/config/pre_receive_tmpl.py31
-rw-r--r--rhodecode/config/routing.py4
-rw-r--r--rhodecode/controllers/admin/repos.py22
-rw-r--r--rhodecode/lib/auth.py2
-rw-r--r--rhodecode/lib/base.py50
-rw-r--r--rhodecode/lib/db_manage.py39
-rw-r--r--rhodecode/lib/exceptions.py17
-rw-r--r--rhodecode/lib/helpers.py15
-rw-r--r--rhodecode/lib/hooks.py108
-rw-r--r--rhodecode/lib/middleware/pygrack.py21
-rw-r--r--rhodecode/lib/middleware/simplegit.py39
-rw-r--r--rhodecode/lib/middleware/simplehg.py24
-rw-r--r--rhodecode/lib/utils2.py13
-rwxr-xr-xrhodecode/model/db.py53
-rw-r--r--rhodecode/model/forms.py3
-rw-r--r--rhodecode/model/scm.py63
-rw-r--r--rhodecode/templates/admin/repos/repo_edit.html48
-rw-r--r--rhodecode/templates/admin/settings/settings.html4
20 files changed, 490 insertions, 108 deletions
diff --git a/docs/index.rst b/docs/index.rst
index 18786c72..e56e2bbd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -22,6 +22,7 @@ Users Guide
usage/general
usage/git_support
usage/performance
+ usage/locking
usage/statistics
usage/backup
usage/debugging
diff --git a/docs/usage/locking.rst b/docs/usage/locking.rst
new file mode 100644
index 00000000..5ed4c359
--- /dev/null
+++ b/docs/usage/locking.rst
@@ -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
diff --git a/rhodecode/config/pre_receive_tmpl.py b/rhodecode/config/pre_receive_tmpl.py
new file mode 100644
index 00000000..e306367d
--- /dev/null
+++ b/rhodecode/config/pre_receive_tmpl.py
@@ -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()
diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py
index 09088fc3..e54efabd 100644
--- a/rhodecode/config/routing.py
+++ b/rhodecode/config/routing.py
@@ -138,7 +138,9 @@ def make_map(config):
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",
diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py
index 8089d6cb..1b763010 100644
--- a/rhodecode/controllers/admin/repos.py
+++ b/rhodecode/controllers/admin/repos.py
@@ -381,6 +381,7 @@ class ReposController(BaseController):
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 @@ class ReposController(BaseController):
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,
diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py
index adbbe831..d234e3e2 100644
--- a/rhodecode/lib/auth.py
+++ b/rhodecode/lib/auth.py
@@ -807,7 +807,7 @@ class HasPermissionAnyMiddleware(object):
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):
diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py
index cbb6cbb2..226c4d6a 100644
--- a/rhodecode/lib/base.py
+++ b/rhodecode/lib/base.py
@@ -8,6 +8,7 @@ import traceback
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 pylons.templating import render_mako as render
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 @@ class BaseVCSController(object):
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:
diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py
index 580976f8..0d6f2350 100644
--- a/rhodecode/lib/db_manage.py
+++ b/rhodecode/lib/db_manage.py
@@ -307,37 +307,47 @@ class DbManage(object):
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)
+
+ 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)
- # 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'
+ 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 @@ class DbManage(object):
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 @@ class DbManage(object):
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 @@ class DbManage(object):
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', '')
diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py
index 0e09bf62..19349fda 100644
--- a/rhodecode/lib/exceptions.py
+++ b/rhodecode/lib/exceptions.py
@@ -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 UsersGroupsAssignedException(Exception):
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)
diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py
index a976d1b4..b10b36b8 100644
--- a/rhodecode/lib/helpers.py
+++ b/rhodecode/lib/helpers.py
@@ -41,7 +41,7 @@ from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
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 @@ def person(author):
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
diff --git a/rhodecode/lib/hooks.py b/rhodecode/lib/hooks.py
index c8e38c73..5f84e769 100644
--- a/rhodecode/lib/hooks.py
+++ b/rhodecode/lib/hooks.py
@@ -34,6 +34,9 @@ from rhodecode.lib import helpers as h
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 @@ def repo_size(ui, repo, hooktype=None, **kwargs):
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 @@ def log_pull_action(ui, repo, **kwargs):
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 @@ def log_pull_action(ui, repo, **kwargs):
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 @@ def log_push_action(ui, repo, **kwargs):
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 @@ def log_push_action(ui, repo, **kwargs):
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 @@ def log_create_repository(repository_dict, created_by, **kwargs):
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 @@ def handle_git_post_receive(repo_path, revs, env):
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 @@ def handle_git_post_receive(repo_path, revs, env):
repo_path = repo_path[:-4]
repo = Repository.get_by_full_path(repo_path)
_hooks = dict(baseui.configitems('hooks')) or {}
+
+ 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
- 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
+ elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
rev_data = []
for l in revs:
diff --git a/rhodecode/lib/middleware/pygrack.py b/rhodecode/lib/middleware/pygrack.py
index 82c8116c..d68a6906 100644
--- a/rhodecode/lib/middleware/pygrack.py
+++ b/rhodecode/lib/middleware/pygrack.py
@@ -41,7 +41,7 @@ class GitRepository(object):
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 @@ class GitRepository(object):
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 @@ class GitRepository(object):
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 @@ class GitRepository(object):
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 GitRepository(object):
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 @@ class GitDirectory(object):
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 @@ class GitDirectory(object):
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)
diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py
index 9a9717e0..5ac4378f 100644
--- a/rhodecode/lib/middleware/simplegit.py
+++ b/rhodecode/lib/middleware/simplegit.py
@@ -31,6 +31,8 @@ import traceback
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 @@ def is_git(environ):
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 @@ class SimpleGit(BaseVCSController):
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 @@ class SimpleGit(BaseVCSController):
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 @@ class SimpleGit(BaseVCSController):
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 @@ class SimpleGit(BaseVCSController):
"""
from rhodecode.lib.hooks import log_pull_action
service = environ['QUERY_STRING'].split('=')
+
if len(service) < 2:
return
@@ -288,6 +312,9 @@ class SimpleGit(BaseVCSController):
_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)
diff --git a/rhodecode/lib/middleware/simplehg.py b/rhodecode/lib/middleware/simplehg.py
index 4ca07edb..1a6f32ff 100644
--- a/rhodecode/lib/middleware/simplehg.py
+++ b/rhodecode/lib/middleware/simplehg.py
@@ -42,6 +42,7 @@ from rhodecode.lib.auth import get_container_username
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 @@ class SimpleHg(BaseVCSController):
'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 @@ class SimpleHg(BaseVCSController):
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)
diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py
index 5198e0ba..16dc37d4 100644
--- a/rhodecode/lib/utils2.py
+++ b/rhodecode/lib/utils2.py
@@ -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 @@ def age(prevdate):
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 @@ def datetime_to_time(dt):
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})'
diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py
index 236d3b52..c243d96d 100755
--- a/rhodecode/model/db.py
+++ b/rhodecode/model/db.py
@@ -28,6 +28,7 @@ import logging
import datetime
import traceback
import hashlib
+import time
from collections import defaultdict
from sqlalchemy import *
@@ -232,7 +233,9 @@ class RhodeCodeUi(Base, BaseModel):
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 @@ class RhodeCodeUi(Base, BaseModel):
@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 @@ class User(Base, BaseModel):
__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 @@ class Repository(Base, BaseModel):
__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 @@ class Repository(Base, BaseModel):
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 @@ class Repository(Base, BaseModel):
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 @@ class Repository(Base, BaseModel):
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 @@ class Repository(Base, BaseModel):
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
#==========================================================================
diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py
index d957acd9..980ce8a9 100644
--- a/rhodecode/model/forms.py
+++ b/rhodecode/model/forms.py
@@ -182,6 +182,7 @@ def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
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 @@ def ApplicationUiSettingsForm():
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)
diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py
index c6631c6d..62287bc4 100644
--- a/rhodecode/model/scm.py
+++ b/rhodecode/model/scm.py
@@ -571,34 +571,41 @@ class ScmModel(BaseModel):
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())
-
- 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')
+ 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 %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')
diff --git a/rhodecode/templates/admin/repos/repo_edit.html b/rhodecode/templates/admin/repos/repo_edit.html
index 94a46093..d7b3edf7 100644
--- a/rhodecode/templates/admin/repos/repo_edit.html
+++ b/rhodecode/templates/admin/repos/repo_edit.html
@@ -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>
diff --git a/rhodecode/templates/admin/settings/settings.html b/rhodecode/templates/admin/settings/settings.html
index fe2080ef..2693e5c3 100644
--- a/rhodecode/templates/admin/settings/settings.html
+++ b/rhodecode/templates/admin/settings/settings.html
@@ -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">