repositories: added option to archive repositories instead of deleting them.
- closes #5493
This commit is contained in:
parent
39d181f715
commit
2910f510dc
19 changed files with 329 additions and 27 deletions
|
|
@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {}
|
||||||
EXTENSIONS = {}
|
EXTENSIONS = {}
|
||||||
|
|
||||||
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
|
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
|
||||||
__dbversion__ = 90 # defines current db version for migrations
|
__dbversion__ = 91 # defines current db version for migrations
|
||||||
__platform__ = platform.system()
|
__platform__ = platform.system()
|
||||||
__license__ = 'AGPLv3, and Commercial License'
|
__license__ = 'AGPLv3, and Commercial License'
|
||||||
__author__ = 'RhodeCode GmbH'
|
__author__ = 'RhodeCode GmbH'
|
||||||
|
|
|
||||||
|
|
@ -505,6 +505,36 @@ class RepoRoutePredicate(object):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class RepoForbidArchivedRoutePredicate(object):
|
||||||
|
def __init__(self, val, config):
|
||||||
|
self.val = val
|
||||||
|
|
||||||
|
def text(self):
|
||||||
|
return 'repo_forbid_archived = %s' % self.val
|
||||||
|
|
||||||
|
phash = text
|
||||||
|
|
||||||
|
def __call__(self, info, request):
|
||||||
|
_ = request.translate
|
||||||
|
rhodecode_db_repo = request.db_repo
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'%s checking if archived flag for repo for %s',
|
||||||
|
self.__class__.__name__, rhodecode_db_repo.repo_name)
|
||||||
|
|
||||||
|
if rhodecode_db_repo.archived:
|
||||||
|
log.warning('Current view is not supported for archived repo:%s',
|
||||||
|
rhodecode_db_repo.repo_name)
|
||||||
|
|
||||||
|
h.flash(
|
||||||
|
h.literal(_('Action not supported for archived repository.')),
|
||||||
|
category='warning')
|
||||||
|
summary_url = request.route_path(
|
||||||
|
'repo_summary', repo_name=rhodecode_db_repo.repo_name)
|
||||||
|
raise HTTPFound(summary_url)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class RepoTypeRoutePredicate(object):
|
class RepoTypeRoutePredicate(object):
|
||||||
def __init__(self, val, config):
|
def __init__(self, val, config):
|
||||||
self.val = val or ['hg', 'git', 'svn']
|
self.val = val or ['hg', 'git', 'svn']
|
||||||
|
|
@ -530,13 +560,6 @@ class RepoTypeRoutePredicate(object):
|
||||||
else:
|
else:
|
||||||
log.warning('Current view is not supported for repo type:%s',
|
log.warning('Current view is not supported for repo type:%s',
|
||||||
rhodecode_db_repo.repo_type)
|
rhodecode_db_repo.repo_type)
|
||||||
|
|
||||||
# h.flash(h.literal(
|
|
||||||
# _('Action not supported for %s.' % rhodecode_repo.alias)),
|
|
||||||
# category='warning')
|
|
||||||
# return redirect(
|
|
||||||
# route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -642,6 +665,8 @@ def includeme(config):
|
||||||
'repo_route', RepoRoutePredicate)
|
'repo_route', RepoRoutePredicate)
|
||||||
config.add_route_predicate(
|
config.add_route_predicate(
|
||||||
'repo_accepted_types', RepoTypeRoutePredicate)
|
'repo_accepted_types', RepoTypeRoutePredicate)
|
||||||
|
config.add_route_predicate(
|
||||||
|
'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
|
||||||
config.add_route_predicate(
|
config.add_route_predicate(
|
||||||
'repo_group_route', RepoGroupRoutePredicate)
|
'repo_group_route', RepoGroupRoutePredicate)
|
||||||
config.add_route_predicate(
|
config.add_route_predicate(
|
||||||
|
|
@ -649,4 +674,4 @@ def includeme(config):
|
||||||
config.add_route_predicate(
|
config.add_route_predicate(
|
||||||
'user_route_with_default', UserRouteWithDefaultPredicate)
|
'user_route_with_default', UserRouteWithDefaultPredicate)
|
||||||
config.add_route_predicate(
|
config.add_route_predicate(
|
||||||
'user_route', UserRoutePredicate)
|
'user_route', UserRoutePredicate)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ from rhodecode.lib.index import searcher_from_config
|
||||||
from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
|
from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
|
||||||
from rhodecode.lib.ext_json import json
|
from rhodecode.lib.ext_json import json
|
||||||
from rhodecode.model.db import (
|
from rhodecode.model.db import (
|
||||||
func, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
|
func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
|
||||||
from rhodecode.model.repo import RepoModel
|
from rhodecode.model.repo import RepoModel
|
||||||
from rhodecode.model.repo_group import RepoGroupModel
|
from rhodecode.model.repo_group import RepoGroupModel
|
||||||
from rhodecode.model.scm import RepoGroupList, RepoList
|
from rhodecode.model.scm import RepoGroupList, RepoList
|
||||||
|
|
@ -114,6 +114,7 @@ class HomeView(BaseAppView):
|
||||||
query = Repository.query()\
|
query = Repository.query()\
|
||||||
.order_by(func.length(Repository.repo_name))\
|
.order_by(func.length(Repository.repo_name))\
|
||||||
.order_by(Repository.repo_name)\
|
.order_by(Repository.repo_name)\
|
||||||
|
.filter(Repository.archived.isnot(true()))\
|
||||||
.filter(or_(
|
.filter(or_(
|
||||||
# generate multiple IN to fix limitation problems
|
# generate multiple IN to fix limitation problems
|
||||||
*in_filter_generator(Repository.repo_id, allowed_ids)
|
*in_filter_generator(Repository.repo_id, allowed_ids)
|
||||||
|
|
|
||||||
|
|
@ -231,11 +231,13 @@ def includeme(config):
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='repo_fork_new',
|
name='repo_fork_new',
|
||||||
pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
|
pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
|
||||||
|
repo_forbid_when_archived=True,
|
||||||
repo_accepted_types=['hg', 'git'])
|
repo_accepted_types=['hg', 'git'])
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='repo_fork_create',
|
name='repo_fork_create',
|
||||||
pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
|
pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
|
||||||
|
repo_forbid_when_archived=True,
|
||||||
repo_accepted_types=['hg', 'git'])
|
repo_accepted_types=['hg', 'git'])
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
|
|
@ -276,27 +278,29 @@ def includeme(config):
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='pullrequest_new',
|
name='pullrequest_new',
|
||||||
pattern='/{repo_name:.*?[^/]}/pull-request/new',
|
pattern='/{repo_name:.*?[^/]}/pull-request/new',
|
||||||
repo_route=True, repo_accepted_types=['hg', 'git'])
|
repo_route=True, repo_accepted_types=['hg', 'git'],
|
||||||
|
repo_forbid_when_archived=True)
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='pullrequest_create',
|
name='pullrequest_create',
|
||||||
pattern='/{repo_name:.*?[^/]}/pull-request/create',
|
pattern='/{repo_name:.*?[^/]}/pull-request/create',
|
||||||
repo_route=True, repo_accepted_types=['hg', 'git'])
|
repo_route=True, repo_accepted_types=['hg', 'git'],
|
||||||
|
repo_forbid_when_archived=True)
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='pullrequest_update',
|
name='pullrequest_update',
|
||||||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
|
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
|
||||||
repo_route=True)
|
repo_route=True, repo_forbid_when_archived=True)
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='pullrequest_merge',
|
name='pullrequest_merge',
|
||||||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
|
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
|
||||||
repo_route=True)
|
repo_route=True, repo_forbid_when_archived=True)
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='pullrequest_delete',
|
name='pullrequest_delete',
|
||||||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
|
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
|
||||||
repo_route=True)
|
repo_route=True, repo_forbid_when_archived=True)
|
||||||
|
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='pullrequest_comment_create',
|
name='pullrequest_comment_create',
|
||||||
|
|
@ -318,6 +322,9 @@ def includeme(config):
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='edit_repo_advanced',
|
name='edit_repo_advanced',
|
||||||
pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
|
pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
|
||||||
|
config.add_route(
|
||||||
|
name='edit_repo_advanced_archive',
|
||||||
|
pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
|
||||||
config.add_route(
|
config.add_route(
|
||||||
name='edit_repo_advanced_delete',
|
name='edit_repo_advanced_delete',
|
||||||
pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
|
pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,27 @@ class TestRepoForkViewTests(TestController):
|
||||||
assert response.json == {u'data': [], u'draw': None,
|
assert response.json == {u'data': [], u'draw': None,
|
||||||
u'recordsFiltered': 0, u'recordsTotal': 0}
|
u'recordsFiltered': 0, u'recordsTotal': 0}
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('url_type', [
|
||||||
|
'repo_fork_new',
|
||||||
|
'repo_fork_create'
|
||||||
|
])
|
||||||
|
def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
|
||||||
|
user = user_util.create_user(password='qweqwe')
|
||||||
|
self.log_user(user.username, 'qweqwe')
|
||||||
|
|
||||||
|
# create a temporary repo
|
||||||
|
source = user_util.create_repo(repo_type=backend.alias)
|
||||||
|
repo_name = source.repo_name
|
||||||
|
repo = Repository.get_by_repo_name(repo_name)
|
||||||
|
repo.archived = True
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
response = self.app.get(
|
||||||
|
route_path(url_type, repo_name=repo_name), status=302)
|
||||||
|
|
||||||
|
msg = 'Action not supported for archived repository.'
|
||||||
|
assert_session_flash(response, msg)
|
||||||
|
|
||||||
|
|
||||||
class TestSVNFork(TestController):
|
class TestSVNFork(TestController):
|
||||||
@pytest.mark.parametrize('route_name', [
|
@pytest.mark.parametrize('route_name', [
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from rhodecode.lib.vcs.nodes import FileNode
|
||||||
from rhodecode.lib import helpers as h
|
from rhodecode.lib import helpers as h
|
||||||
from rhodecode.model.changeset_status import ChangesetStatusModel
|
from rhodecode.model.changeset_status import ChangesetStatusModel
|
||||||
from rhodecode.model.db import (
|
from rhodecode.model.db import (
|
||||||
PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
|
PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
|
||||||
from rhodecode.model.meta import Session
|
from rhodecode.model.meta import Session
|
||||||
from rhodecode.model.pull_request import PullRequestModel
|
from rhodecode.model.pull_request import PullRequestModel
|
||||||
from rhodecode.model.user import UserModel
|
from rhodecode.model.user import UserModel
|
||||||
|
|
@ -1191,6 +1191,28 @@ class TestPullrequestsControllerDelete(object):
|
||||||
)
|
)
|
||||||
assert response.body == 'true'
|
assert response.body == 'true'
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('url_type', [
|
||||||
|
'pullrequest_new',
|
||||||
|
'pullrequest_create',
|
||||||
|
'pullrequest_update',
|
||||||
|
'pullrequest_merge',
|
||||||
|
])
|
||||||
|
def test_pull_request_is_forbidden_on_archived_repo(
|
||||||
|
self, autologin_user, backend, xhr_header, user_util, url_type):
|
||||||
|
|
||||||
|
# create a temporary repo
|
||||||
|
source = user_util.create_repo(repo_type=backend.alias)
|
||||||
|
repo_name = source.repo_name
|
||||||
|
repo = Repository.get_by_repo_name(repo_name)
|
||||||
|
repo.archived = True
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
response = self.app.get(
|
||||||
|
route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
|
||||||
|
|
||||||
|
msg = 'Action not supported for archived repository.'
|
||||||
|
assert_session_flash(response, msg)
|
||||||
|
|
||||||
|
|
||||||
def assert_pull_request_status(pull_request, expected_status):
|
def assert_pull_request_status(pull_request, expected_status):
|
||||||
status = ChangesetStatusModel().calculated_review_status(
|
status = ChangesetStatusModel().calculated_review_status(
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ def route_path(name, params=None, **kwargs):
|
||||||
'repo_summary': '/{repo_name}',
|
'repo_summary': '/{repo_name}',
|
||||||
'edit_repo_advanced': '/{repo_name}/settings/advanced',
|
'edit_repo_advanced': '/{repo_name}/settings/advanced',
|
||||||
'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
|
'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
|
||||||
|
'edit_repo_advanced_archive': '/{repo_name}/settings/advanced/archive',
|
||||||
'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
|
'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
|
||||||
'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
|
'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
|
||||||
'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
|
'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
|
||||||
|
|
@ -133,7 +134,7 @@ class TestAdminRepoSettingsAdvanced(object):
|
||||||
"suffix",
|
"suffix",
|
||||||
['', u'ąęł' , '123'],
|
['', u'ąęł' , '123'],
|
||||||
ids=no_newline_id_generator)
|
ids=no_newline_id_generator)
|
||||||
def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
|
def test_advanced_repo_delete(self, autologin_user, backend, suffix, csrf_token):
|
||||||
repo = backend.create_repo(name_suffix=suffix)
|
repo = backend.create_repo(name_suffix=suffix)
|
||||||
repo_name = repo.repo_name
|
repo_name = repo.repo_name
|
||||||
repo_name_str = safe_str(repo.repo_name)
|
repo_name_str = safe_str(repo.repo_name)
|
||||||
|
|
@ -148,3 +149,25 @@ class TestAdminRepoSettingsAdvanced(object):
|
||||||
# check if repo was deleted from db
|
# check if repo was deleted from db
|
||||||
assert RepoModel().get_by_repo_name(repo_name) is None
|
assert RepoModel().get_by_repo_name(repo_name) is None
|
||||||
assert not repo_on_filesystem(repo_name_str)
|
assert not repo_on_filesystem(repo_name_str)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"suffix",
|
||||||
|
['', u'ąęł' , '123'],
|
||||||
|
ids=no_newline_id_generator)
|
||||||
|
def test_advanced_repo_archive(self, autologin_user, backend, suffix, csrf_token):
|
||||||
|
repo = backend.create_repo(name_suffix=suffix)
|
||||||
|
repo_name = repo.repo_name
|
||||||
|
repo_name_str = safe_str(repo.repo_name)
|
||||||
|
|
||||||
|
response = self.app.post(
|
||||||
|
route_path('edit_repo_advanced_archive', repo_name=repo_name_str),
|
||||||
|
params={'csrf_token': csrf_token})
|
||||||
|
|
||||||
|
assert_session_flash(response,
|
||||||
|
u'Archived repository `{}`'.format(repo_name))
|
||||||
|
|
||||||
|
response = self.app.get(route_path('repo_summary', repo_name=repo_name_str))
|
||||||
|
response.mustcontain('This repository has been archived. It is now read-only.')
|
||||||
|
|
||||||
|
# check if repo was deleted from db
|
||||||
|
assert RepoModel().get_by_repo_name(repo_name).archived is True
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import logging
|
||||||
from pyramid.view import view_config
|
from pyramid.view import view_config
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
|
from rhodecode import events
|
||||||
from rhodecode.apps._base import RepoAppView
|
from rhodecode.apps._base import RepoAppView
|
||||||
from rhodecode.lib import helpers as h
|
from rhodecode.lib import helpers as h
|
||||||
from rhodecode.lib import audit_logger
|
from rhodecode.lib import audit_logger
|
||||||
|
|
@ -45,6 +46,13 @@ class RepoSettingsView(RepoAppView):
|
||||||
c = self._get_local_tmpl_context()
|
c = self._get_local_tmpl_context()
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
def _get_users_with_permissions(self):
|
||||||
|
user_permissions = {}
|
||||||
|
for perm in self.db_repo.permissions():
|
||||||
|
user_permissions[perm.user_id] = perm
|
||||||
|
|
||||||
|
return user_permissions
|
||||||
|
|
||||||
@LoginRequired()
|
@LoginRequired()
|
||||||
@HasRepoPermissionAnyDecorator('repository.admin')
|
@HasRepoPermissionAnyDecorator('repository.admin')
|
||||||
@view_config(
|
@view_config(
|
||||||
|
|
@ -67,6 +75,49 @@ class RepoSettingsView(RepoAppView):
|
||||||
|
|
||||||
return self._get_template_context(c)
|
return self._get_template_context(c)
|
||||||
|
|
||||||
|
@LoginRequired()
|
||||||
|
@HasRepoPermissionAnyDecorator('repository.admin')
|
||||||
|
@CSRFRequired()
|
||||||
|
@view_config(
|
||||||
|
route_name='edit_repo_advanced_archive', request_method='POST',
|
||||||
|
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
|
||||||
|
def edit_advanced_archive(self):
|
||||||
|
"""
|
||||||
|
Archives the repository. It will become read-only, and not visible in search
|
||||||
|
or other queries. But still visible for super-admins.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ = self.request.translate
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_data = self.db_repo.get_api_data()
|
||||||
|
RepoModel().archive(self.db_repo)
|
||||||
|
|
||||||
|
repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
|
||||||
|
audit_logger.store_web(
|
||||||
|
'repo.archive', action_data={'old_data': old_data},
|
||||||
|
user=self._rhodecode_user, repo=repo)
|
||||||
|
|
||||||
|
ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
|
||||||
|
h.flash(
|
||||||
|
_('Archived repository `%s`') % self.db_repo_name,
|
||||||
|
category='success')
|
||||||
|
Session().commit()
|
||||||
|
except Exception:
|
||||||
|
log.exception("Exception during archiving of repository")
|
||||||
|
h.flash(_('An error occurred during archiving of `%s`')
|
||||||
|
% self.db_repo_name, category='error')
|
||||||
|
# redirect to advanced for more deletion options
|
||||||
|
raise HTTPFound(
|
||||||
|
h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
|
||||||
|
_anchor='advanced-archive'))
|
||||||
|
|
||||||
|
# flush permissions for all users defined in permissions
|
||||||
|
affected_user_ids = self._get_users_with_permissions().keys()
|
||||||
|
events.trigger(events.UserPermissionsChange(affected_user_ids))
|
||||||
|
|
||||||
|
raise HTTPFound(h.route_path('home'))
|
||||||
|
|
||||||
@LoginRequired()
|
@LoginRequired()
|
||||||
@HasRepoPermissionAnyDecorator('repository.admin')
|
@HasRepoPermissionAnyDecorator('repository.admin')
|
||||||
@CSRFRequired()
|
@CSRFRequired()
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ ACTIONS_V1 = {
|
||||||
'repo.edit': {'old_data': {}},
|
'repo.edit': {'old_data': {}},
|
||||||
'repo.edit.permissions': {},
|
'repo.edit.permissions': {},
|
||||||
'repo.edit.permissions.branch': {},
|
'repo.edit.permissions.branch': {},
|
||||||
|
'repo.archive': {'old_data': {}},
|
||||||
'repo.delete': {'old_data': {}},
|
'repo.delete': {'old_data': {}},
|
||||||
|
|
||||||
'repo.archive.download': {'user_agent': '', 'archive_name': '',
|
'repo.archive.download': {'user_agent': '', 'archive_name': '',
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ def _cached_perms_data(user_id, scope, user_is_admin,
|
||||||
|
|
||||||
class PermOrigin(object):
|
class PermOrigin(object):
|
||||||
SUPER_ADMIN = 'superadmin'
|
SUPER_ADMIN = 'superadmin'
|
||||||
|
ARCHIVED = 'archived'
|
||||||
|
|
||||||
REPO_USER = 'user:%s'
|
REPO_USER = 'user:%s'
|
||||||
REPO_USERGROUP = 'usergroup:%s'
|
REPO_USERGROUP = 'usergroup:%s'
|
||||||
|
|
@ -463,8 +464,14 @@ class PermissionCalculator(object):
|
||||||
# repositories
|
# repositories
|
||||||
for perm in self.default_repo_perms:
|
for perm in self.default_repo_perms:
|
||||||
r_k = perm.UserRepoToPerm.repository.repo_name
|
r_k = perm.UserRepoToPerm.repository.repo_name
|
||||||
|
archived = perm.UserRepoToPerm.repository.archived
|
||||||
p = 'repository.admin'
|
p = 'repository.admin'
|
||||||
self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
|
self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
|
||||||
|
# special case for archived repositories, which we block still even for
|
||||||
|
# super admins
|
||||||
|
if archived:
|
||||||
|
p = 'repository.read'
|
||||||
|
self.permissions_repositories[r_k] = p, PermOrigin.ARCHIVED
|
||||||
|
|
||||||
# repository groups
|
# repository groups
|
||||||
for perm in self.default_repo_groups_perms:
|
for perm in self.default_repo_groups_perms:
|
||||||
|
|
@ -572,6 +579,7 @@ class PermissionCalculator(object):
|
||||||
def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
|
def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
|
||||||
for perm in self.default_repo_perms:
|
for perm in self.default_repo_perms:
|
||||||
r_k = perm.UserRepoToPerm.repository.repo_name
|
r_k = perm.UserRepoToPerm.repository.repo_name
|
||||||
|
archived = perm.UserRepoToPerm.repository.archived
|
||||||
p = perm.Permission.permission_name
|
p = perm.Permission.permission_name
|
||||||
o = PermOrigin.REPO_DEFAULT
|
o = PermOrigin.REPO_DEFAULT
|
||||||
self.permissions_repositories[r_k] = p, o
|
self.permissions_repositories[r_k] = p, o
|
||||||
|
|
@ -602,6 +610,15 @@ class PermissionCalculator(object):
|
||||||
o = PermOrigin.SUPER_ADMIN
|
o = PermOrigin.SUPER_ADMIN
|
||||||
self.permissions_repositories[r_k] = p, o
|
self.permissions_repositories[r_k] = p, o
|
||||||
|
|
||||||
|
# finally in case of archived repositories, we downgrade higher
|
||||||
|
# permissions to read
|
||||||
|
if archived:
|
||||||
|
current_perm = self.permissions_repositories[r_k]
|
||||||
|
if current_perm in ['repository.write', 'repository.admin']:
|
||||||
|
p = 'repository.read'
|
||||||
|
o = PermOrigin.ARCHIVED
|
||||||
|
self.permissions_repositories[r_k] = p, o
|
||||||
|
|
||||||
def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
|
def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
|
||||||
for perm in self.default_branch_repo_perms:
|
for perm in self.default_branch_repo_perms:
|
||||||
|
|
||||||
|
|
|
||||||
36
rhodecode/lib/dbmigrate/versions/091_version_4_14_0.py
Normal file
36
rhodecode/lib/dbmigrate/versions/091_version_4_14_0.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import *
|
||||||
|
|
||||||
|
from rhodecode.model import meta
|
||||||
|
from rhodecode.lib.dbmigrate.versions import _reset_base, notify
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
"""
|
||||||
|
Upgrade operations go here.
|
||||||
|
Don't create your own engine; bind migrate_engine to your metadata
|
||||||
|
"""
|
||||||
|
_reset_base(migrate_engine)
|
||||||
|
from rhodecode.lib.dbmigrate.schema import db_4_13_0_0 as db
|
||||||
|
|
||||||
|
repository_table = db.Repository.__table__
|
||||||
|
|
||||||
|
archived = Column('archived', Boolean(), nullable=True)
|
||||||
|
archived.create(table=repository_table)
|
||||||
|
|
||||||
|
# issue fixups
|
||||||
|
fixups(db, meta.Session)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
meta = MetaData()
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
|
||||||
|
def fixups(models, _SESSION):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1585,6 +1585,8 @@ class Repository(Base, BaseModel):
|
||||||
unique=False, default=None)
|
unique=False, default=None)
|
||||||
private = Column(
|
private = Column(
|
||||||
"private", Boolean(), nullable=True, unique=None, default=None)
|
"private", Boolean(), nullable=True, unique=None, default=None)
|
||||||
|
archived = Column(
|
||||||
|
"archived", Boolean(), nullable=True, unique=None, default=None)
|
||||||
enable_statistics = Column(
|
enable_statistics = Column(
|
||||||
"statistics", Boolean(), nullable=True, unique=None, default=True)
|
"statistics", Boolean(), nullable=True, unique=None, default=True)
|
||||||
enable_downloads = Column(
|
enable_downloads = Column(
|
||||||
|
|
@ -1783,9 +1785,12 @@ class Repository(Base, BaseModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
|
def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
|
||||||
case_insensitive=True):
|
case_insensitive=True, archived=False):
|
||||||
q = Repository.query()
|
q = Repository.query()
|
||||||
|
|
||||||
|
if not archived:
|
||||||
|
q = q.filter(Repository.archived.isnot(true()))
|
||||||
|
|
||||||
if not isinstance(user_id, Optional):
|
if not isinstance(user_id, Optional):
|
||||||
q = q.filter(Repository.user_id == user_id)
|
q = q.filter(Repository.user_id == user_id)
|
||||||
|
|
||||||
|
|
@ -1796,6 +1801,7 @@ class Repository(Base, BaseModel):
|
||||||
q = q.order_by(func.lower(Repository.repo_name))
|
q = q.order_by(func.lower(Repository.repo_name))
|
||||||
else:
|
else:
|
||||||
q = q.order_by(Repository.repo_name)
|
q = q.order_by(Repository.repo_name)
|
||||||
|
|
||||||
return q.all()
|
return q.all()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,8 @@ class RepoModel(BaseModel):
|
||||||
def quick_menu(repo_name):
|
def quick_menu(repo_name):
|
||||||
return _render('quick_menu', repo_name)
|
return _render('quick_menu', repo_name)
|
||||||
|
|
||||||
def repo_lnk(name, rtype, rstate, private, fork_of):
|
def repo_lnk(name, rtype, rstate, private, archived, fork_of):
|
||||||
return _render('repo_name', name, rtype, rstate, private, fork_of,
|
return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
|
||||||
short_name=not admin, admin=False)
|
short_name=not admin, admin=False)
|
||||||
|
|
||||||
def last_change(last_change):
|
def last_change(last_change):
|
||||||
|
|
@ -246,8 +246,8 @@ class RepoModel(BaseModel):
|
||||||
row = {
|
row = {
|
||||||
"menu": quick_menu(repo.repo_name),
|
"menu": quick_menu(repo.repo_name),
|
||||||
|
|
||||||
"name": repo_lnk(repo.repo_name, repo.repo_type,
|
"name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
|
||||||
repo.repo_state, repo.private, repo.fork),
|
repo.private, repo.archived, repo.fork),
|
||||||
"name_raw": repo.repo_name.lower(),
|
"name_raw": repo.repo_name.lower(),
|
||||||
|
|
||||||
"last_change": last_change(repo.last_db_change),
|
"last_change": last_change(repo.last_db_change),
|
||||||
|
|
@ -427,6 +427,7 @@ class RepoModel(BaseModel):
|
||||||
new_repo.group = repo_group
|
new_repo.group = repo_group
|
||||||
new_repo.description = description or repo_name
|
new_repo.description = description or repo_name
|
||||||
new_repo.private = private
|
new_repo.private = private
|
||||||
|
new_repo.archived = False
|
||||||
new_repo.clone_uri = clone_uri
|
new_repo.clone_uri = clone_uri
|
||||||
new_repo.landing_rev = landing_rev
|
new_repo.landing_rev = landing_rev
|
||||||
|
|
||||||
|
|
@ -608,6 +609,23 @@ class RepoModel(BaseModel):
|
||||||
from rhodecode.lib.celerylib import tasks, run_task
|
from rhodecode.lib.celerylib import tasks, run_task
|
||||||
return run_task(tasks.create_repo_fork, form_data, cur_user)
|
return run_task(tasks.create_repo_fork, form_data, cur_user)
|
||||||
|
|
||||||
|
def archive(self, repo):
|
||||||
|
"""
|
||||||
|
Archive given repository. Set archive flag.
|
||||||
|
|
||||||
|
:param repo:
|
||||||
|
"""
|
||||||
|
repo = self._get_repo(repo)
|
||||||
|
if repo:
|
||||||
|
|
||||||
|
try:
|
||||||
|
repo.archived = True
|
||||||
|
self.sa.add(repo)
|
||||||
|
self.sa.commit()
|
||||||
|
except Exception:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
raise
|
||||||
|
|
||||||
def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
|
def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
|
||||||
"""
|
"""
|
||||||
Delete given repository, forks parameter defines what do do with
|
Delete given repository, forks parameter defines what do do with
|
||||||
|
|
@ -616,6 +634,7 @@ class RepoModel(BaseModel):
|
||||||
|
|
||||||
:param repo:
|
:param repo:
|
||||||
:param forks: str 'delete' or 'detach'
|
:param forks: str 'delete' or 'detach'
|
||||||
|
:param pull_requests: str 'delete' or None
|
||||||
:param fs_remove: remove(archive) repo from filesystem
|
:param fs_remove: remove(archive) repo from filesystem
|
||||||
"""
|
"""
|
||||||
if not cur_user:
|
if not cur_user:
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,7 @@ function registerRCRoutes() {
|
||||||
pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
|
pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
|
||||||
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
|
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
|
||||||
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
|
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
|
||||||
|
pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
|
||||||
pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
|
pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
|
||||||
pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
|
pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
|
||||||
pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
|
pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
<button class="btn btn-small" type="submit"
|
<button class="btn btn-small" type="submit"
|
||||||
onclick="return confirm('${_('Confirm to lock repository.')}');">
|
onclick="return confirm('${_('Confirm to lock repository.')}');">
|
||||||
<i class="icon-lock"></i>
|
<i class="icon-lock"></i>
|
||||||
${_('Lock Repository')}
|
${_('Lock repository')}
|
||||||
</button>
|
</button>
|
||||||
%endif
|
%endif
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -111,6 +111,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel panel-warning">
|
||||||
|
<div class="panel-heading" id="advanced-archive">
|
||||||
|
<h3 class="panel-title">${_('Archive repository')} <a class="permalink" href="#advanced-archive"> ¶</a></h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
${h.secure_form(h.route_path('edit_repo_advanced_archive', repo_name=c.repo_name), request=request)}
|
||||||
|
|
||||||
|
<div style="margin: 0 0 20px 0" class="fake-space"></div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button class="btn btn-small btn-danger" type="submit"
|
||||||
|
onclick="return confirm('${_('Confirm to archive this repository: %s') % c.repo_name}');">
|
||||||
|
<i class="icon-remove-sign"></i>
|
||||||
|
${_('Archive this repository')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="help-block">
|
||||||
|
${_('Archiving the repository will make it entirely read-only. The repository cannot be committed to.'
|
||||||
|
'It is hidden from the search results and dashboard. ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading" id="advanced-delete">
|
<div class="panel-heading" id="advanced-delete">
|
||||||
<h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"> ¶</a></h3>
|
<h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"> ¶</a></h3>
|
||||||
|
|
@ -152,7 +181,7 @@
|
||||||
<button class="btn btn-small btn-danger" type="submit"
|
<button class="btn btn-small btn-danger" type="submit"
|
||||||
onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
|
onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
|
||||||
<i class="icon-remove-sign"></i>
|
<i class="icon-remove-sign"></i>
|
||||||
${_('Delete This Repository')}
|
${_('Delete this repository')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="clear"></div>
|
<div class="clear"></div>
|
||||||
</div>
|
</div>
|
||||||
|
% if c.rhodecode_db_repo.archived:
|
||||||
|
<div class="alert alert-warning text-center">
|
||||||
|
<strong>${_('This repository has been archived. It is now read-only.')}</strong>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<!--- END CONTEXT BAR -->
|
<!--- END CONTEXT BAR -->
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
|
<%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
|
||||||
<%
|
<%
|
||||||
def get_name(name,short_name=short_name):
|
def get_name(name,short_name=short_name):
|
||||||
if short_name:
|
if short_name:
|
||||||
|
|
@ -105,6 +105,7 @@
|
||||||
(${_('creating...')})
|
(${_('creating...')})
|
||||||
</span>
|
</span>
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -561,7 +561,7 @@ def test_get_repo_by_id(test, expected):
|
||||||
|
|
||||||
|
|
||||||
def test_invalidation_context(baseapp):
|
def test_invalidation_context(baseapp):
|
||||||
repo_id = 999
|
repo_id = 9999
|
||||||
|
|
||||||
cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
|
cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
|
||||||
repo_id, CacheKey.CACHE_TYPE_README)
|
repo_id, CacheKey.CACHE_TYPE_README)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,13 @@ import pytest
|
||||||
from rhodecode.lib.vcs.backends.git.repository import GitRepository
|
from rhodecode.lib.vcs.backends.git.repository import GitRepository
|
||||||
from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
|
from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
|
||||||
from rhodecode.lib.vcs.nodes import FileNode
|
from rhodecode.lib.vcs.nodes import FileNode
|
||||||
|
from rhodecode.model.db import Repository
|
||||||
from rhodecode.model.meta import Session
|
from rhodecode.model.meta import Session
|
||||||
|
from rhodecode.tests import GIT_REPO, HG_REPO
|
||||||
|
|
||||||
from rhodecode.tests.vcs_operations import (
|
from rhodecode.tests.vcs_operations import (
|
||||||
Command, _check_proper_clone, _check_proper_git_push, _check_proper_hg_push)
|
Command, _check_proper_clone, _check_proper_git_push, _check_proper_hg_push,
|
||||||
|
_add_files_and_push)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("disable_locking")
|
@pytest.mark.usefixtures("disable_locking")
|
||||||
|
|
@ -242,3 +245,37 @@ class TestVCSOperationsSpecial(object):
|
||||||
assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout
|
assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout
|
||||||
assert 'remote: RhodeCode: push completed' in stdout
|
assert 'remote: RhodeCode: push completed' in stdout
|
||||||
assert 'exporting bookmark feature2' in stdout
|
assert 'exporting bookmark feature2' in stdout
|
||||||
|
|
||||||
|
def test_push_is_forbidden_on_archived_repo_hg(self, backend_hg, rc_web_server, tmpdir):
|
||||||
|
empty_repo = backend_hg.create_repo()
|
||||||
|
repo_name = empty_repo.repo_name
|
||||||
|
|
||||||
|
repo = Repository.get_by_repo_name(repo_name)
|
||||||
|
repo.archived = True
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
clone_url = rc_web_server.repo_clone_url(repo_name)
|
||||||
|
stdout, stderr = Command('/tmp').execute(
|
||||||
|
'hg clone', clone_url, tmpdir.strpath)
|
||||||
|
|
||||||
|
stdout, stderr = _add_files_and_push(
|
||||||
|
'hg', tmpdir.strpath, clone_url=clone_url)
|
||||||
|
|
||||||
|
assert 'abort: HTTP Error 403: Forbidden' in stderr
|
||||||
|
|
||||||
|
def test_push_is_forbidden_on_archived_repo_git(self, backend_git, rc_web_server, tmpdir):
|
||||||
|
empty_repo = backend_git.create_repo()
|
||||||
|
repo_name = empty_repo.repo_name
|
||||||
|
|
||||||
|
repo = Repository.get_by_repo_name(repo_name)
|
||||||
|
repo.archived = True
|
||||||
|
Session().commit()
|
||||||
|
|
||||||
|
clone_url = rc_web_server.repo_clone_url(repo_name)
|
||||||
|
stdout, stderr = Command('/tmp').execute(
|
||||||
|
'git clone', clone_url, tmpdir.strpath)
|
||||||
|
|
||||||
|
stdout, stderr = _add_files_and_push(
|
||||||
|
'git', tmpdir.strpath, clone_url=clone_url)
|
||||||
|
|
||||||
|
assert "The requested URL returned error: 403" in stderr
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue