repositories: added option to archive repositories instead of deleting them.

- closes #5493
This commit is contained in:
Marcin Kuzminski 2018-09-25 17:28:11 +02:00
parent 39d181f715
commit 2910f510dc
19 changed files with 329 additions and 27 deletions

View file

@ -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'

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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', [

View file

@ -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(

View file

@ -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

View file

@ -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()

View file

@ -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': '',

View file

@ -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:

View 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

View file

@ -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

View file

@ -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:

View file

@ -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']);

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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