diff --git a/CONTRIBUTORS b/CONTRIBUTORS --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -15,4 +15,5 @@ List of contributors to RhodeCode projec Les Peabody Jonas Oberschweiber Matt Zuba - Aras Pranckevicius \ No newline at end of file + Aras Pranckevicius + Tony Bussieres diff --git a/docs/changelog.rst b/docs/changelog.rst --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,25 @@ Changelog ========= +1.3.2 (**2012-02-28**) +---------------------- + +news +++++ + + +fixes ++++++ + +- fixed git protocol issues with repos-groups +- fixed git remote repos validator that prevented from cloning remote git repos +- fixes #370 ending slashes fixes for repo and groups +- fixes #368 improved git-protocol detection to handle other clients +- fixes #366 When Setting Repository Group To Blank Repo Group Wont Be + Moved To Root +- fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys +- fixed #373 missing cascade drop on user_group_to_perm table + 1.3.1 (**2012-02-27**) ---------------------- diff --git a/docs/theme/nature/layout.html b/docs/theme/nature/layout.html --- a/docs/theme/nature/layout.html +++ b/docs/theme/nature/layout.html @@ -13,6 +13,6 @@
Flattr this -
+ {% endblock %}} diff --git a/rhodecode/controllers/admin/repos_groups.py b/rhodecode/controllers/admin/repos_groups.py --- a/rhodecode/controllers/admin/repos_groups.py +++ b/rhodecode/controllers/admin/repos_groups.py @@ -263,6 +263,11 @@ class ReposGroupsController(BaseControll raise HTTPInternalServerError() def show_by_name(self, group_name): + """ + This is a proxy that does a lookup group_name -> id, and shows + the group by id view instead + """ + group_name = group_name.rstrip('/') id_ = RepoGroup.get_by_group_name(group_name).group_id return self.show(id_) diff --git a/rhodecode/controllers/admin/users_groups.py b/rhodecode/controllers/admin/users_groups.py --- a/rhodecode/controllers/admin/users_groups.py +++ b/rhodecode/controllers/admin/users_groups.py @@ -160,11 +160,12 @@ class UsersGroupsController(BaseControll try: UsersGroupModel().delete(id) + Session.commit() h.flash(_('successfully deleted users group'), category='success') - Session.commit() except UsersGroupsAssignedException, e: h.flash(e, category='error') except Exception: + log.error(traceback.format_exc()) h.flash(_('An error occurred during deletion of users group'), category='error') return redirect(url('users_groups')) diff --git a/rhodecode/lib/__init__.py b/rhodecode/lib/__init__.py --- a/rhodecode/lib/__init__.py +++ b/rhodecode/lib/__init__.py @@ -231,7 +231,7 @@ def safe_str(unicode_, to_encoding=None) :rtype: str :returns: str object """ - + # if it's not basestr cast to str if not isinstance(unicode_, basestring): return str(unicode_) diff --git a/rhodecode/lib/caching_query.py b/rhodecode/lib/caching_query.py --- a/rhodecode/lib/caching_query.py +++ b/rhodecode/lib/caching_query.py @@ -24,6 +24,7 @@ from beaker.exceptions import BeakerExce from sqlalchemy.orm.interfaces import MapperOption from sqlalchemy.orm.query import Query from sqlalchemy.sql import visitors +from rhodecode.lib import safe_str class CachingQuery(Query): @@ -137,9 +138,10 @@ def _get_cache_parameters(query): if cache_key is None: # cache key - the value arguments from this query's parameters. - args = [str(x) for x in _params_from_query(query)] - args.extend(filter(lambda k:k not in ['None', None, u'None'], + args = [safe_str(x) for x in _params_from_query(query)] + args.extend(filter(lambda k: k not in ['None', None, u'None'], [str(query._limit), str(query._offset)])) + cache_key = " ".join(args) if cache_key is None: diff --git a/rhodecode/lib/middleware/https_fixup.py b/rhodecode/lib/middleware/https_fixup.py --- a/rhodecode/lib/middleware/https_fixup.py +++ b/rhodecode/lib/middleware/https_fixup.py @@ -42,13 +42,21 @@ class HttpsFixup(object): middleware you should set this header inside your proxy ie. nginx, apache etc. """ - proto = environ.get('HTTP_X_URL_SCHEME') if str2bool(self.config.get('force_https')): proto = 'https' - + else: + if 'HTTP_X_URL_SCHEME' in environ: + proto = environ.get('HTTP_X_URL_SCHEME') + elif 'HTTP_X_FORWARDED_SCHEME' in environ: + proto = environ.get('HTTP_X_FORWARDED_SCHEME') + elif 'HTTP_X_FORWARDED_PROTO' in environ: + proto = environ.get('HTTP_X_FORWARDED_PROTO') + else: + proto = 'http' if proto == 'https': environ['wsgi.url_scheme'] = proto else: environ['wsgi.url_scheme'] = 'http' + return None diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -25,6 +25,7 @@ # along with this program. If not, see . import os +import re import logging import traceback @@ -79,21 +80,20 @@ from webob.exc import HTTPNotFound, HTTP log = logging.getLogger(__name__) -def is_git(environ): - """Returns True if request's target is git server. - ``HTTP_USER_AGENT`` would then have git client version given. +GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)') + - :param environ: - """ - http_user_agent = environ.get('HTTP_USER_AGENT') - if http_user_agent and http_user_agent.startswith('git'): - return True - return False +def is_git(environ): + path_info = environ['PATH_INFO'] + isgit_path = GIT_PROTO_PAT.match(path_info) + log.debug('is a git path %s pathinfo : %s' % (isgit_path, path_info)) + return isgit_path class SimpleGit(BaseVCSController): def _handle_request(self, environ, start_response): + if not is_git(environ): return self.application(environ, start_response) @@ -218,13 +218,11 @@ class SimpleGit(BaseVCSController): """ try: environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO']) - repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) - if repo_name.endswith('/'): - repo_name = repo_name.rstrip('/') + repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1) except: log.error(traceback.format_exc()) raise - repo_name = repo_name.split('/')[0] + return repo_name def __get_user(self, username): @@ -238,9 +236,10 @@ class SimpleGit(BaseVCSController): service = environ['QUERY_STRING'].split('=') if len(service) > 1: service_cmd = service[1] - mapping = {'git-receive-pack': 'push', - 'git-upload-pack': 'pull', - } + mapping = { + 'git-receive-pack': 'push', + 'git-upload-pack': 'pull', + } return mapping.get(service_cmd, service_cmd if service_cmd else 'other') diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -92,11 +92,17 @@ def repo_name_slug(value): def get_repo_slug(request): - return request.environ['pylons.routes_dict'].get('repo_name') + _repo = request.environ['pylons.routes_dict'].get('repo_name') + if _repo: + _repo = _repo.rstrip('/') + return _repo def get_repos_group_slug(request): - return request.environ['pylons.routes_dict'].get('group_name') + _group = request.environ['pylons.routes_dict'].get('group_name') + if _group: + _group = _group.rstrip('/') + return _group def action_logger(user, action, repo, ipaddr='', sa=None, commit=False): diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -44,6 +44,7 @@ from rhodecode.lib.compat import json from rhodecode.lib.caching_query import FromCache from rhodecode.model.meta import Base, Session +import hashlib log = logging.getLogger(__name__) @@ -52,6 +53,8 @@ log = logging.getLogger(__name__) # BASE CLASSES #============================================================================== +_hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest() + class ModelSerializer(json.JSONEncoder): """ @@ -337,8 +340,11 @@ class User(Base, BaseModel): q = cls.query().filter(cls.username == username) if cache: - q = q.options(FromCache("sql_cache_short", - "get_user_%s" % username)) + q = q.options(FromCache( + "sql_cache_short", + "get_user_%s" % _hash_key(username) + ) + ) return q.scalar() @classmethod @@ -394,7 +400,7 @@ class UserLog(Base, BaseModel): return datetime.date(*self.action_date.timetuple()[:3]) user = relationship('User') - repository = relationship('Repository',cascade='') + repository = relationship('Repository', cascade='') class UsersGroup(Base, BaseModel): @@ -406,6 +412,7 @@ class UsersGroup(Base, BaseModel): users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined") + users_group_to_perm = relationship('UsersGroupToPerm', cascade='all') def __repr__(self): return '' % (self.users_group_name) @@ -418,8 +425,11 @@ class UsersGroup(Base, BaseModel): else: q = cls.query().filter(cls.users_group_name == group_name) if cache: - q = q.options(FromCache("sql_cache_short", - "get_user_%s" % group_name)) + q = q.options(FromCache( + "sql_cache_short", + "get_user_%s" % _hash_key(group_name) + ) + ) return q.scalar() @classmethod @@ -748,8 +758,11 @@ class RepoGroup(Base, BaseModel): gr = cls.query()\ .filter(cls.group_name == group_name) if cache: - gr = gr.options(FromCache("sql_cache_short", - "get_group_%s" % group_name)) + gr = gr.options(FromCache( + "sql_cache_short", + "get_group_%s" % _hash_key(group_name) + ) + ) return gr.scalar() @property @@ -1038,7 +1051,7 @@ class CacheInvalidation(Base, BaseModel) prefix = '' iid = rhodecode.CONFIG.get('instance_id') if iid: - prefix = iid + prefix = iid return "%s%s" % (prefix, key) @classmethod diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -345,32 +345,46 @@ def SlugifyName(): def ValidCloneUri(): - from mercurial.httprepo import httprepository, httpsrepository from rhodecode.lib.utils import make_ui + def url_handler(repo_type, url, proto, ui=None): + if repo_type == 'hg': + from mercurial.httprepo import httprepository, httpsrepository + if proto == 'https': + httpsrepository(make_ui('db'), url).capabilities + elif proto == 'http': + httprepository(make_ui('db'), url).capabilities + elif repo_type == 'git': + #TODO: write a git url validator + pass + class _ValidCloneUri(formencode.validators.FancyValidator): def to_python(self, value, state): - if not value: + + repo_type = value.get('repo_type') + url = value.get('clone_uri') + e_dict = {'clone_uri': _('invalid clone url')} + + if not url: pass - elif value.startswith('https'): + elif url.startswith('https'): try: - httpsrepository(make_ui('db'), value).capabilities + url_handler(repo_type, url, 'https', make_ui('db')) except Exception: log.error(traceback.format_exc()) - raise formencode.Invalid(_('invalid clone url'), value, - state) - elif value.startswith('http'): + raise formencode.Invalid('', value, state, error_dict=e_dict) + elif url.startswith('http'): try: - httprepository(make_ui('db'), value).capabilities + url_handler(repo_type, url, 'http', make_ui('db')) except Exception: log.error(traceback.format_exc()) - raise formencode.Invalid(_('invalid clone url'), value, - state) + raise formencode.Invalid('', value, state, error_dict=e_dict) else: - raise formencode.Invalid(_('Invalid clone url, provide a ' - 'valid clone http\s url'), value, - state) + e_dict = {'clone_uri': _('Invalid clone url, provide a ' + 'valid clone http\s url')} + raise formencode.Invalid('', value, state, error_dict=e_dict) + return value return _ValidCloneUri @@ -645,8 +659,7 @@ def RepoForm(edit=False, old_data={}, su filter_extra_fields = False repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), SlugifyName()) - clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False), - ValidCloneUri()()) + clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False)) repo_group = OneOf(repo_groups, hideList=True) repo_type = OneOf(supported_backends) description = UnicodeString(strip=True, min=1, not_empty=True) @@ -658,7 +671,9 @@ def RepoForm(edit=False, old_data={}, su #this is repo owner user = All(UnicodeString(not_empty=True), ValidRepoUser) - chained_validators = [ValidRepoName(edit, old_data), ValidPerms()] + chained_validators = [ValidCloneUri()(), + ValidRepoName(edit, old_data), + ValidPerms()] return _RepoForm diff --git a/rhodecode/model/repos_group.py b/rhodecode/model/repos_group.py --- a/rhodecode/model/repos_group.py +++ b/rhodecode/model/repos_group.py @@ -187,20 +187,20 @@ class ReposGroupModel(BaseModel): # change properties repos_group.group_description = form_data['group_description'] repos_group.parent_group = RepoGroup.get(form_data['group_parent_id']) + repos_group.group_parent_id = form_data['group_parent_id'] repos_group.group_name = repos_group.get_new_name(form_data['group_name']) - new_path = repos_group.full_path self.sa.add(repos_group) - self.__rename_group(old_path, new_path) - # we need to get all repositories from this new group and # rename them accordingly to new group path for r in repos_group.repositories: r.repo_name = r.get_new_name(r.just_name) self.sa.add(r) + self.__rename_group(old_path, new_path) + return repos_group except: log.error(traceback.format_exc()) diff --git a/rhodecode/tests/functional/test_admin_users_groups.py b/rhodecode/tests/functional/test_admin_users_groups.py --- a/rhodecode/tests/functional/test_admin_users_groups.py +++ b/rhodecode/tests/functional/test_admin_users_groups.py @@ -1,8 +1,9 @@ from rhodecode.tests import * -from rhodecode.model.db import UsersGroup +from rhodecode.model.db import UsersGroup, UsersGroupToPerm, Permission TEST_USERS_GROUP = 'admins_test' + class TestAdminUsersGroupsController(TestController): def test_index(self): @@ -16,7 +17,7 @@ class TestAdminUsersGroupsController(Tes self.log_user() users_group_name = TEST_USERS_GROUP response = self.app.post(url('users_groups'), - {'users_group_name':users_group_name, + {'users_group_name': users_group_name, 'active':True}) response.follow() @@ -47,7 +48,6 @@ class TestAdminUsersGroupsController(Tes self.checkSessionFlash(response, 'created users group %s' % users_group_name) - gr = self.Session.query(UsersGroup)\ .filter(UsersGroup.users_group_name == users_group_name).one() @@ -60,6 +60,53 @@ class TestAdminUsersGroupsController(Tes self.assertEqual(gr, None) + def test_enable_repository_read_on_group(self): + self.log_user() + users_group_name = TEST_USERS_GROUP + 'another2' + response = self.app.post(url('users_groups'), + {'users_group_name': users_group_name, + 'active':True}) + response.follow() + + ug = UsersGroup.get_by_group_name(users_group_name) + self.checkSessionFlash(response, + 'created users group %s' % users_group_name) + + response = self.app.put(url('users_group_perm', id=ug.users_group_id), + {'create_repo_perm': True}) + + response.follow() + ug = UsersGroup.get_by_group_name(users_group_name) + p = Permission.get_by_key('hg.create.repository') + # check if user has this perm + perms = UsersGroupToPerm.query()\ + .filter(UsersGroupToPerm.users_group == ug).all() + perms = [[x.__dict__['users_group_id'], + x.__dict__['permission_id'],] for x in perms] + self.assertEqual( + perms, + [[ug.users_group_id, p.permission_id]] + ) + + # DELETE ! + ug = UsersGroup.get_by_group_name(users_group_name) + ugid = ug.users_group_id + response = self.app.delete(url('users_group', id=ug.users_group_id)) + response = response.follow() + gr = self.Session.query(UsersGroup)\ + .filter(UsersGroup.users_group_name == + users_group_name).scalar() + + self.assertEqual(gr, None) + p = Permission.get_by_key('hg.create.repository') + perms = UsersGroupToPerm.query()\ + .filter(UsersGroupToPerm.users_group_id == ugid).all() + perms = [[x.__dict__['users_group_id'], + x.__dict__['permission_id'],] for x in perms] + self.assertEqual( + perms, + [] + ) def test_delete_browser_fakeout(self): response = self.app.post(url('users_group', id=1), diff --git a/rhodecode/tests/test_models.py b/rhodecode/tests/test_models.py --- a/rhodecode/tests/test_models.py +++ b/rhodecode/tests/test_models.py @@ -23,7 +23,6 @@ def _make_group(path, desc='desc', paren return gr gr = ReposGroupModel().create(path, desc, parent_id) - Session.commit() return gr @@ -31,13 +30,19 @@ class TestReposGroups(unittest.TestCase) def setUp(self): self.g1 = _make_group('test1', skip_if_exists=True) + Session.commit() self.g2 = _make_group('test2', skip_if_exists=True) + Session.commit() self.g3 = _make_group('test3', skip_if_exists=True) + Session.commit() def tearDown(self): print 'out' def __check_path(self, *path): + """ + Checks the path for existance ! + """ path = [TESTS_TMP_PATH] + list(path) path = os.path.join(*path) return os.path.isdir(path) @@ -49,12 +54,13 @@ class TestReposGroups(unittest.TestCase) ReposGroupModel().delete(id_) def __update_group(self, id_, path, desc='desc', parent_id=None): - form_data = dict(group_name=path, - group_description=desc, - group_parent_id=parent_id, - perms_updates=[], - perms_new=[]) - + form_data = dict( + group_name=path, + group_description=desc, + group_parent_id=parent_id, + perms_updates=[], + perms_new=[] + ) gr = ReposGroupModel().update(id_, form_data) return gr @@ -150,6 +156,25 @@ class TestReposGroups(unittest.TestCase) self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name)) + def test_move_to_root(self): + g1 = _make_group('t11') + Session.commit() + g2 = _make_group('t22',parent_id=g1.group_id) + Session.commit() + + self.assertEqual(g2.full_path,'t11/t22') + self.assertTrue(self.__check_path('t11', 't22')) + + g2 = self.__update_group(g2.group_id, 'g22', parent_id=None) + Session.commit() + + self.assertEqual(g2.group_name,'g22') + # we moved out group from t1 to '' so it's full path should be 'g2' + self.assertEqual(g2.full_path,'g22') + self.assertFalse(self.__check_path('t11', 't22')) + self.assertTrue(self.__check_path('g22')) + + class TestUser(unittest.TestCase): def __init__(self, methodName='runTest'): Session.remove()