#!/usr/bin/env python
# encoding: utf-8
# custom hooks for application
# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
#
# 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; version 2
# of the License or (at your opinion) any later version of the license.
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
"""
Created on Aug 6, 2010
@author: marcink
import sys
from mercurial.cmdutil import revrange
from mercurial.node import nullrev
from rhodecode.lib import helpers as h
from rhodecode.lib.utils import action_logger
import os
from rhodecode.model import meta
from rhodecode.model.db import UserLog, User
def repo_size(ui, repo, hooktype=None, **kwargs):
if hooktype != 'changegroup':
return False
size_hg, size_root = 0, 0
for path, dirs, files in os.walk(repo.root):
if path.find('.hg') != -1:
for f in files:
size_hg += os.path.getsize(os.path.join(path, f))
else:
size_root += os.path.getsize(os.path.join(path, f))
size_hg_f = h.format_byte_size(size_hg)
size_root_f = h.format_byte_size(size_root)
size_total_f = h.format_byte_size(size_root + size_hg)
sys.stdout.write('Repository size .hg:%s repo:%s total:%s\n' \
% (size_hg_f, size_root_f, size_total_f))
user_action_mapper(ui, repo, hooktype, **kwargs)
def log_pull_action(ui, repo, **kwargs):
Logs user last pull action
:param ui:
:param repo:
extra_params = dict(repo.ui.configitems('rhodecode_extras'))
username = extra_params['username']
repository = extra_params['repository']
action = 'pull'
action_logger(username, action, repository, extra_params['ip'])
return 0
def user_action_mapper(ui, repo, hooktype=None, **kwargs):
def log_push_action(ui, repo, **kwargs):
Maps user last push action to new changeset id, from mercurial
:param hooktype:
try:
sa = meta.Session()
username = kwargs['url'].split(':')[-1]
user_log = sa.query(UserLog)\
.filter(UserLog.user == sa.query(User)\
.filter(User.username == username).one())\
.order_by(UserLog.user_log_id.desc()).first()
action = 'push:%s'
node = kwargs['node']
def get_revs(repo, rev_opt):
if rev_opt:
revs = revrange(repo, rev_opt)
if len(revs) == 0:
return (nullrev, nullrev)
return (max(revs), min(revs))
return (len(repo) - 1, 0)
stop, start = get_revs(repo, [node + ':'])
revs = (str(repo[r]) for r in xrange(start, stop + 1))
action = action % ','.join(revs)
if user_log and not user_log.revision:
user_log.revision = str(repo['tip'])
sa.add(user_log)
sa.commit()
except Exception, e:
sa.rollback()
raise
finally:
meta.Session.remove()
@@ -4,224 +4,238 @@
Created on 2010-04-28
SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
It's implemented with basic auth function
from itertools import chain
from mercurial.error import RepoError
from mercurial.hgweb import hgweb
from mercurial.hgweb.request import wsgiapplication
from paste.auth.basic import AuthBasicAuthenticator
from paste.httpheaders import REMOTE_USER, AUTH_TYPE
from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
from rhodecode.lib.utils import is_mercurial, make_ui, invalidate_cache, \
check_repo_fast, ui_sections
from rhodecode.model.user import UserModel
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
import logging
import traceback
log = logging.getLogger(__name__)
class SimpleHg(object):
def __init__(self, application, config):
self.application = application
self.config = config
#authenticate this mercurial request using
self.authenticate = AuthBasicAuthenticator('', authfunc)
self.ipaddr = '0.0.0.0'
self.repository = None
self.username = None
self.action = None
def __call__(self, environ, start_response):
if not is_mercurial(environ):
return self.application(environ, start_response)
proxy_key = 'HTTP_X_REAL_IP'
def_key = 'REMOTE_ADDR'
self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
#===================================================================
# AUTHENTICATE THIS MERCURIAL REQUEST
username = REMOTE_USER(environ)
if not username:
self.authenticate.realm = self.config['rhodecode_realm']
result = self.authenticate(environ)
if isinstance(result, str):
AUTH_TYPE.update(environ, 'basic')
REMOTE_USER.update(environ, result)
return result.wsgi_application(environ, start_response)
#=======================================================================
# GET REPOSITORY
repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
if repo_name.endswith('/'):
repo_name = repo_name.rstrip('/')
self.repository = repo_name
except:
log.error(traceback.format_exc())
return HTTPInternalServerError()(environ, start_response)
# CHECK PERMISSIONS FOR THIS REQUEST
action = self.__get_action(environ)
if action:
self.action = self.__get_action(environ)
if self.action:
username = self.__get_environ_user(environ)
user = self.__get_user(username)
self.username = user.username
#check permissions for this repository
if action == 'push':
if self.action == 'push':
if not HasPermissionAnyMiddleware('repository.write',
'repository.admin')\
(user, repo_name):
return HTTPForbidden()(environ, start_response)
#any other action need at least read permission
if not HasPermissionAnyMiddleware('repository.read',
'repository.write',
#log action
if action in ('push', 'pull', 'clone'):
ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
self.__log_user_action(user, action, repo_name, ipaddr)
self.extras = {'ip':self.ipaddr,
'username':self.username,
'action':self.action,
'repository':self.repository}
print self.extras
# MERCURIAL REQUEST HANDLING
environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
self.baseui = make_ui('db')
self.basepath = self.config['base_path']
self.repo_path = os.path.join(self.basepath, repo_name)
#quick check if that dir exists...
if check_repo_fast(repo_name, self.basepath):
return HTTPNotFound()(environ, start_response)
app = wsgiapplication(self.__make_app)
except RepoError, e:
if str(e).find('not found') != -1:
except Exception:
#invalidate cache on push
self.__invalidate_cache(repo_name)
messages = []
messages.append('thank you for using rhodecode')
return self.msg_wrapper(app, environ, start_response, messages)
return app(environ, start_response)
def msg_wrapper(self, app, environ, start_response, messages=[]):
Wrapper for custom messages that come out of mercurial respond messages
is a list of messages that the user will see at the end of response
from merurial protocol actions that involves remote answers
:param app:
:param environ:
:param start_response:
def custom_messages(msg_list):
for msg in msg_list:
yield msg + '\n'
org_response = app(environ, start_response)
return chain(org_response, custom_messages(messages))
def __make_app(self):
hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
return self.__load_web_settings(hgserve)
return self.__load_web_settings(hgserve, self.extras)
def __get_environ_user(self, environ):
return environ.get('REMOTE_USER')
def __get_user(self, username):
return UserModel().get_by_username(username, cache=True)
def __get_action(self, environ):
Maps mercurial request commands into a clone,pull or push command.
This should always return a valid command string
mapping = {'changegroup': 'pull',
'changegroupsubset': 'pull',
'stream_out': 'pull',
#'listkeys': 'pull',
'listkeys': 'pull',
'unbundle': 'push',
'pushkey': 'push', }
for qry in environ['QUERY_STRING'].split('&'):
if qry.startswith('cmd'):
cmd = qry.split('=')[-1]
if mapping.has_key(cmd):
return mapping[cmd]
return cmd
def __log_user_action(self, user, action, repo, ipaddr):
action_logger(user, action, repo, ipaddr)
def __invalidate_cache(self, repo_name):
"""we know that some change was made to repositories and we should
invalidate the cache to see the changes right away but only for
push requests"""
invalidate_cache('cached_repo_list')
invalidate_cache('full_changelog', repo_name)
def __load_web_settings(self, hgserve):
def __load_web_settings(self, hgserve, extras={}):
#set the global ui for hgserve instance passed
hgserve.repo.ui = self.baseui
hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
#inject some additional parameters that will be available in ui
#for hooks
for k, v in extras.items():
hgserve.repo.ui.setconfig('rhodecode_extras', k, v)
repoui = make_ui('file', hgrc, False)
if repoui:
#overwrite our ui instance with the section from hgrc file
for section in ui_sections:
for k, v in repoui.configitems(section):
hgserve.repo.ui.setconfig(section, k, v)
return hgserve
# Utilities for RhodeCode
Created on April 18, 2010
Utilities for RhodeCode
from UserDict import DictMixin
from mercurial import ui, config, hg
from rhodecode.model.caching_query import FromCache
from rhodecode.model.db import Repository, User, RhodeCodeUi, RhodeCodeSettings, \
UserLog
from rhodecode.model.repo import RepoModel
from vcs.backends.base import BaseChangeset
from vcs.backends.git import GitRepository
from vcs.backends.hg import MercurialRepository
from vcs.utils.lazy import LazyProperty
import datetime
def get_repo_slug(request):
return request.environ['pylons.routes_dict'].get('repo_name')
def is_mercurial(environ):
Returns True if request's target is mercurial server - header
``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
http_accept = environ.get('HTTP_ACCEPT')
if http_accept and http_accept.startswith('application/mercurial'):
return True
def is_git(environ):
Returns True if request's target is git server. ``HTTP_USER_AGENT`` would
then have git client version given.
http_user_agent = environ.get('HTTP_USER_AGENT')
if http_user_agent.startswith('git'):
def action_logger(user, action, repo, ipaddr, sa=None):
Action logger for various action made by users
if not sa:
if hasattr(user, 'user_id'):
user_id = user.user_id
user_obj = user
elif isinstance(user, basestring):
user_id = UserModel(sa).get_by_username(user, cache=False).user_id
user_obj = UserModel(sa).get_by_username(user, cache=False)
raise Exception('You have to provide user object or username')
repo_name = repo.lstrip('/')
user_log = UserLog()
user_log.user_id = user_id
user_log.user_id = user_obj.user_id
user_log.action = action
user_log.repository_name = repo_name
user_log.repository = RepoModel(sa).get(repo_name, cache=False)
user_log.action_date = datetime.datetime.now()
user_log.user_ip = ipaddr
log.info('Adding user %s, action %s on %s',
user.username, action, repo)
user_obj.username, action, repo)
log.error('could not log user action:%s', str(e))
def get_repos(path, recursive=False, initial=False):
Scans given path for repos and return (name,(type,path)) tuple
:param prefix:
:param path:
:param recursive:
:param initial:
from vcs.utils.helpers import get_scm
from vcs.exceptions import VCSError
scm = get_scm(path)
pass
raise Exception('The given path %s should not be a repository got %s',
path, scm)
for dirpath in os.listdir(path):
yield dirpath, get_scm(os.path.join(path, dirpath))
except VCSError:
if __name__ == '__main__':
get_repos('', '/home/marcink/workspace-python')
def check_repo_fast(repo_name, base_path):
if os.path.isdir(os.path.join(base_path, repo_name)):return False
def check_repo(repo_name, base_path, verify=True):
repo_path = os.path.join(base_path, repo_name)
if not check_repo_fast(repo_name, base_path):
r = hg.repository(ui.ui(), repo_path)
if verify:
hg.verify(r)
#here we hnow that repo exists it was verified
log.info('%s repo is already created', repo_name)
except RepoError:
Status change: