##// END OF EJS Templates
repositories: added option to archive repositories instead of deleting them....
marcink -
r3090:bdd9dc16 default
parent child
Show More
@@ -0,0 +1,36
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_13_0_0 as db
18
19 repository_table = db.Repository.__table__
20
21 archived = Column('archived', Boolean(), nullable=True)
22 archived.create(table=repository_table)
23
24 # issue fixups
25 fixups(db, meta.Session)
26
27
28 def downgrade(migrate_engine):
29 meta = MetaData()
30 meta.bind = migrate_engine
31
32
33 def fixups(models, _SESSION):
34 pass
35
36
@@ -51,7 +51,7 PYRAMID_SETTINGS = {}
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 90 # defines current db version for migrations
54 __dbversion__ = 91 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
@@ -505,6 +505,36 class RepoRoutePredicate(object):
505 return False
505 return False
506
506
507
507
508 class RepoForbidArchivedRoutePredicate(object):
509 def __init__(self, val, config):
510 self.val = val
511
512 def text(self):
513 return 'repo_forbid_archived = %s' % self.val
514
515 phash = text
516
517 def __call__(self, info, request):
518 _ = request.translate
519 rhodecode_db_repo = request.db_repo
520
521 log.debug(
522 '%s checking if archived flag for repo for %s',
523 self.__class__.__name__, rhodecode_db_repo.repo_name)
524
525 if rhodecode_db_repo.archived:
526 log.warning('Current view is not supported for archived repo:%s',
527 rhodecode_db_repo.repo_name)
528
529 h.flash(
530 h.literal(_('Action not supported for archived repository.')),
531 category='warning')
532 summary_url = request.route_path(
533 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
534 raise HTTPFound(summary_url)
535 return True
536
537
508 class RepoTypeRoutePredicate(object):
538 class RepoTypeRoutePredicate(object):
509 def __init__(self, val, config):
539 def __init__(self, val, config):
510 self.val = val or ['hg', 'git', 'svn']
540 self.val = val or ['hg', 'git', 'svn']
@@ -530,13 +560,6 class RepoTypeRoutePredicate(object):
530 else:
560 else:
531 log.warning('Current view is not supported for repo type:%s',
561 log.warning('Current view is not supported for repo type:%s',
532 rhodecode_db_repo.repo_type)
562 rhodecode_db_repo.repo_type)
533
534 # h.flash(h.literal(
535 # _('Action not supported for %s.' % rhodecode_repo.alias)),
536 # category='warning')
537 # return redirect(
538 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
539
540 return False
563 return False
541
564
542
565
@@ -643,10 +666,12 def includeme(config):
643 config.add_route_predicate(
666 config.add_route_predicate(
644 'repo_accepted_types', RepoTypeRoutePredicate)
667 'repo_accepted_types', RepoTypeRoutePredicate)
645 config.add_route_predicate(
668 config.add_route_predicate(
669 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
670 config.add_route_predicate(
646 'repo_group_route', RepoGroupRoutePredicate)
671 'repo_group_route', RepoGroupRoutePredicate)
647 config.add_route_predicate(
672 config.add_route_predicate(
648 'user_group_route', UserGroupRoutePredicate)
673 'user_group_route', UserGroupRoutePredicate)
649 config.add_route_predicate(
674 config.add_route_predicate(
650 'user_route_with_default', UserRouteWithDefaultPredicate)
675 'user_route_with_default', UserRouteWithDefaultPredicate)
651 config.add_route_predicate(
676 config.add_route_predicate(
652 'user_route', UserRoutePredicate) No newline at end of file
677 'user_route', UserRoutePredicate)
@@ -33,7 +33,7 from rhodecode.lib.index import searcher
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.model.db import (
35 from rhodecode.model.db import (
36 func, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
36 func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
37 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.repo_group import RepoGroupModel
38 from rhodecode.model.repo_group import RepoGroupModel
39 from rhodecode.model.scm import RepoGroupList, RepoList
39 from rhodecode.model.scm import RepoGroupList, RepoList
@@ -114,6 +114,7 class HomeView(BaseAppView):
114 query = Repository.query()\
114 query = Repository.query()\
115 .order_by(func.length(Repository.repo_name))\
115 .order_by(func.length(Repository.repo_name))\
116 .order_by(Repository.repo_name)\
116 .order_by(Repository.repo_name)\
117 .filter(Repository.archived.isnot(true()))\
117 .filter(or_(
118 .filter(or_(
118 # generate multiple IN to fix limitation problems
119 # generate multiple IN to fix limitation problems
119 *in_filter_generator(Repository.repo_id, allowed_ids)
120 *in_filter_generator(Repository.repo_id, allowed_ids)
@@ -231,11 +231,13 def includeme(config):
231 config.add_route(
231 config.add_route(
232 name='repo_fork_new',
232 name='repo_fork_new',
233 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
233 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
234 repo_forbid_when_archived=True,
234 repo_accepted_types=['hg', 'git'])
235 repo_accepted_types=['hg', 'git'])
235
236
236 config.add_route(
237 config.add_route(
237 name='repo_fork_create',
238 name='repo_fork_create',
238 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
239 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
240 repo_forbid_when_archived=True,
239 repo_accepted_types=['hg', 'git'])
241 repo_accepted_types=['hg', 'git'])
240
242
241 config.add_route(
243 config.add_route(
@@ -276,27 +278,29 def includeme(config):
276 config.add_route(
278 config.add_route(
277 name='pullrequest_new',
279 name='pullrequest_new',
278 pattern='/{repo_name:.*?[^/]}/pull-request/new',
280 pattern='/{repo_name:.*?[^/]}/pull-request/new',
279 repo_route=True, repo_accepted_types=['hg', 'git'])
281 repo_route=True, repo_accepted_types=['hg', 'git'],
282 repo_forbid_when_archived=True)
280
283
281 config.add_route(
284 config.add_route(
282 name='pullrequest_create',
285 name='pullrequest_create',
283 pattern='/{repo_name:.*?[^/]}/pull-request/create',
286 pattern='/{repo_name:.*?[^/]}/pull-request/create',
284 repo_route=True, repo_accepted_types=['hg', 'git'])
287 repo_route=True, repo_accepted_types=['hg', 'git'],
288 repo_forbid_when_archived=True)
285
289
286 config.add_route(
290 config.add_route(
287 name='pullrequest_update',
291 name='pullrequest_update',
288 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
292 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
289 repo_route=True)
293 repo_route=True, repo_forbid_when_archived=True)
290
294
291 config.add_route(
295 config.add_route(
292 name='pullrequest_merge',
296 name='pullrequest_merge',
293 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
297 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
294 repo_route=True)
298 repo_route=True, repo_forbid_when_archived=True)
295
299
296 config.add_route(
300 config.add_route(
297 name='pullrequest_delete',
301 name='pullrequest_delete',
298 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
302 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
299 repo_route=True)
303 repo_route=True, repo_forbid_when_archived=True)
300
304
301 config.add_route(
305 config.add_route(
302 name='pullrequest_comment_create',
306 name='pullrequest_comment_create',
@@ -319,6 +323,9 def includeme(config):
319 name='edit_repo_advanced',
323 name='edit_repo_advanced',
320 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
324 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
321 config.add_route(
325 config.add_route(
326 name='edit_repo_advanced_archive',
327 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
328 config.add_route(
322 name='edit_repo_advanced_delete',
329 name='edit_repo_advanced_delete',
323 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
330 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
324 config.add_route(
331 config.add_route(
@@ -303,6 +303,27 class TestRepoForkViewTests(TestControll
303 assert response.json == {u'data': [], u'draw': None,
303 assert response.json == {u'data': [], u'draw': None,
304 u'recordsFiltered': 0, u'recordsTotal': 0}
304 u'recordsFiltered': 0, u'recordsTotal': 0}
305
305
306 @pytest.mark.parametrize('url_type', [
307 'repo_fork_new',
308 'repo_fork_create'
309 ])
310 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
311 user = user_util.create_user(password='qweqwe')
312 self.log_user(user.username, 'qweqwe')
313
314 # create a temporary repo
315 source = user_util.create_repo(repo_type=backend.alias)
316 repo_name = source.repo_name
317 repo = Repository.get_by_repo_name(repo_name)
318 repo.archived = True
319 Session().commit()
320
321 response = self.app.get(
322 route_path(url_type, repo_name=repo_name), status=302)
323
324 msg = 'Action not supported for archived repository.'
325 assert_session_flash(response, msg)
326
306
327
307 class TestSVNFork(TestController):
328 class TestSVNFork(TestController):
308 @pytest.mark.parametrize('route_name', [
329 @pytest.mark.parametrize('route_name', [
@@ -26,7 +26,7 from rhodecode.lib.vcs.nodes import File
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
@@ -1191,6 +1191,28 class TestPullrequestsControllerDelete(o
1191 )
1191 )
1192 assert response.body == 'true'
1192 assert response.body == 'true'
1193
1193
1194 @pytest.mark.parametrize('url_type', [
1195 'pullrequest_new',
1196 'pullrequest_create',
1197 'pullrequest_update',
1198 'pullrequest_merge',
1199 ])
1200 def test_pull_request_is_forbidden_on_archived_repo(
1201 self, autologin_user, backend, xhr_header, user_util, url_type):
1202
1203 # create a temporary repo
1204 source = user_util.create_repo(repo_type=backend.alias)
1205 repo_name = source.repo_name
1206 repo = Repository.get_by_repo_name(repo_name)
1207 repo.archived = True
1208 Session().commit()
1209
1210 response = self.app.get(
1211 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1212
1213 msg = 'Action not supported for archived repository.'
1214 assert_session_flash(response, msg)
1215
1194
1216
1195 def assert_pull_request_status(pull_request, expected_status):
1217 def assert_pull_request_status(pull_request, expected_status):
1196 status = ChangesetStatusModel().calculated_review_status(
1218 status = ChangesetStatusModel().calculated_review_status(
@@ -39,6 +39,7 def route_path(name, params=None, **kwar
39 'repo_summary': '/{repo_name}',
39 'repo_summary': '/{repo_name}',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
41 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
42 'edit_repo_advanced_archive': '/{repo_name}/settings/advanced/archive',
42 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
43 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
43 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
44 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
44 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
45 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
@@ -133,7 +134,7 class TestAdminRepoSettingsAdvanced(obje
133 "suffix",
134 "suffix",
134 ['', u'ąęł' , '123'],
135 ['', u'ąęł' , '123'],
135 ids=no_newline_id_generator)
136 ids=no_newline_id_generator)
136 def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
137 def test_advanced_repo_delete(self, autologin_user, backend, suffix, csrf_token):
137 repo = backend.create_repo(name_suffix=suffix)
138 repo = backend.create_repo(name_suffix=suffix)
138 repo_name = repo.repo_name
139 repo_name = repo.repo_name
139 repo_name_str = safe_str(repo.repo_name)
140 repo_name_str = safe_str(repo.repo_name)
@@ -148,3 +149,25 class TestAdminRepoSettingsAdvanced(obje
148 # check if repo was deleted from db
149 # check if repo was deleted from db
149 assert RepoModel().get_by_repo_name(repo_name) is None
150 assert RepoModel().get_by_repo_name(repo_name) is None
150 assert not repo_on_filesystem(repo_name_str)
151 assert not repo_on_filesystem(repo_name_str)
152
153 @pytest.mark.parametrize(
154 "suffix",
155 ['', u'ąęł' , '123'],
156 ids=no_newline_id_generator)
157 def test_advanced_repo_archive(self, autologin_user, backend, suffix, csrf_token):
158 repo = backend.create_repo(name_suffix=suffix)
159 repo_name = repo.repo_name
160 repo_name_str = safe_str(repo.repo_name)
161
162 response = self.app.post(
163 route_path('edit_repo_advanced_archive', repo_name=repo_name_str),
164 params={'csrf_token': csrf_token})
165
166 assert_session_flash(response,
167 u'Archived repository `{}`'.format(repo_name))
168
169 response = self.app.get(route_path('repo_summary', repo_name=repo_name_str))
170 response.mustcontain('This repository has been archived. It is now read-only.')
171
172 # check if repo was deleted from db
173 assert RepoModel().get_by_repo_name(repo_name).archived is True
@@ -23,6 +23,7 import logging
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode import events
26 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
29 from rhodecode.lib import audit_logger
@@ -45,6 +46,13 class RepoSettingsView(RepoAppView):
45 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
46 return c
47 return c
47
48
49 def _get_users_with_permissions(self):
50 user_permissions = {}
51 for perm in self.db_repo.permissions():
52 user_permissions[perm.user_id] = perm
53
54 return user_permissions
55
48 @LoginRequired()
56 @LoginRequired()
49 @HasRepoPermissionAnyDecorator('repository.admin')
57 @HasRepoPermissionAnyDecorator('repository.admin')
50 @view_config(
58 @view_config(
@@ -71,6 +79,49 class RepoSettingsView(RepoAppView):
71 @HasRepoPermissionAnyDecorator('repository.admin')
79 @HasRepoPermissionAnyDecorator('repository.admin')
72 @CSRFRequired()
80 @CSRFRequired()
73 @view_config(
81 @view_config(
82 route_name='edit_repo_advanced_archive', request_method='POST',
83 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
84 def edit_advanced_archive(self):
85 """
86 Archives the repository. It will become read-only, and not visible in search
87 or other queries. But still visible for super-admins.
88 """
89
90 _ = self.request.translate
91
92 try:
93 old_data = self.db_repo.get_api_data()
94 RepoModel().archive(self.db_repo)
95
96 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
97 audit_logger.store_web(
98 'repo.archive', action_data={'old_data': old_data},
99 user=self._rhodecode_user, repo=repo)
100
101 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
102 h.flash(
103 _('Archived repository `%s`') % self.db_repo_name,
104 category='success')
105 Session().commit()
106 except Exception:
107 log.exception("Exception during archiving of repository")
108 h.flash(_('An error occurred during archiving of `%s`')
109 % self.db_repo_name, category='error')
110 # redirect to advanced for more deletion options
111 raise HTTPFound(
112 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
113 _anchor='advanced-archive'))
114
115 # flush permissions for all users defined in permissions
116 affected_user_ids = self._get_users_with_permissions().keys()
117 events.trigger(events.UserPermissionsChange(affected_user_ids))
118
119 raise HTTPFound(h.route_path('home'))
120
121 @LoginRequired()
122 @HasRepoPermissionAnyDecorator('repository.admin')
123 @CSRFRequired()
124 @view_config(
74 route_name='edit_repo_advanced_delete', request_method='POST',
125 route_name='edit_repo_advanced_delete', request_method='POST',
75 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
126 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
76 def edit_advanced_delete(self):
127 def edit_advanced_delete(self):
@@ -65,6 +65,7 ACTIONS_V1 = {
65 'repo.edit': {'old_data': {}},
65 'repo.edit': {'old_data': {}},
66 'repo.edit.permissions': {},
66 'repo.edit.permissions': {},
67 'repo.edit.permissions.branch': {},
67 'repo.edit.permissions.branch': {},
68 'repo.archive': {'old_data': {}},
68 'repo.delete': {'old_data': {}},
69 'repo.delete': {'old_data': {}},
69
70
70 'repo.archive.download': {'user_agent': '', 'archive_name': '',
71 'repo.archive.download': {'user_agent': '', 'archive_name': '',
@@ -322,6 +322,7 def _cached_perms_data(user_id, scope, u
322
322
323 class PermOrigin(object):
323 class PermOrigin(object):
324 SUPER_ADMIN = 'superadmin'
324 SUPER_ADMIN = 'superadmin'
325 ARCHIVED = 'archived'
325
326
326 REPO_USER = 'user:%s'
327 REPO_USER = 'user:%s'
327 REPO_USERGROUP = 'usergroup:%s'
328 REPO_USERGROUP = 'usergroup:%s'
@@ -463,8 +464,14 class PermissionCalculator(object):
463 # repositories
464 # repositories
464 for perm in self.default_repo_perms:
465 for perm in self.default_repo_perms:
465 r_k = perm.UserRepoToPerm.repository.repo_name
466 r_k = perm.UserRepoToPerm.repository.repo_name
467 archived = perm.UserRepoToPerm.repository.archived
466 p = 'repository.admin'
468 p = 'repository.admin'
467 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
469 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
470 # special case for archived repositories, which we block still even for
471 # super admins
472 if archived:
473 p = 'repository.read'
474 self.permissions_repositories[r_k] = p, PermOrigin.ARCHIVED
468
475
469 # repository groups
476 # repository groups
470 for perm in self.default_repo_groups_perms:
477 for perm in self.default_repo_groups_perms:
@@ -572,6 +579,7 class PermissionCalculator(object):
572 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
579 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
573 for perm in self.default_repo_perms:
580 for perm in self.default_repo_perms:
574 r_k = perm.UserRepoToPerm.repository.repo_name
581 r_k = perm.UserRepoToPerm.repository.repo_name
582 archived = perm.UserRepoToPerm.repository.archived
575 p = perm.Permission.permission_name
583 p = perm.Permission.permission_name
576 o = PermOrigin.REPO_DEFAULT
584 o = PermOrigin.REPO_DEFAULT
577 self.permissions_repositories[r_k] = p, o
585 self.permissions_repositories[r_k] = p, o
@@ -602,6 +610,15 class PermissionCalculator(object):
602 o = PermOrigin.SUPER_ADMIN
610 o = PermOrigin.SUPER_ADMIN
603 self.permissions_repositories[r_k] = p, o
611 self.permissions_repositories[r_k] = p, o
604
612
613 # finally in case of archived repositories, we downgrade higher
614 # permissions to read
615 if archived:
616 current_perm = self.permissions_repositories[r_k]
617 if current_perm in ['repository.write', 'repository.admin']:
618 p = 'repository.read'
619 o = PermOrigin.ARCHIVED
620 self.permissions_repositories[r_k] = p, o
621
605 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
622 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
606 for perm in self.default_branch_repo_perms:
623 for perm in self.default_branch_repo_perms:
607
624
@@ -1585,6 +1585,8 class Repository(Base, BaseModel):
1585 unique=False, default=None)
1585 unique=False, default=None)
1586 private = Column(
1586 private = Column(
1587 "private", Boolean(), nullable=True, unique=None, default=None)
1587 "private", Boolean(), nullable=True, unique=None, default=None)
1588 archived = Column(
1589 "archived", Boolean(), nullable=True, unique=None, default=None)
1588 enable_statistics = Column(
1590 enable_statistics = Column(
1589 "statistics", Boolean(), nullable=True, unique=None, default=True)
1591 "statistics", Boolean(), nullable=True, unique=None, default=True)
1590 enable_downloads = Column(
1592 enable_downloads = Column(
@@ -1783,9 +1785,12 class Repository(Base, BaseModel):
1783
1785
1784 @classmethod
1786 @classmethod
1785 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1787 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1786 case_insensitive=True):
1788 case_insensitive=True, archived=False):
1787 q = Repository.query()
1789 q = Repository.query()
1788
1790
1791 if not archived:
1792 q = q.filter(Repository.archived.isnot(true()))
1793
1789 if not isinstance(user_id, Optional):
1794 if not isinstance(user_id, Optional):
1790 q = q.filter(Repository.user_id == user_id)
1795 q = q.filter(Repository.user_id == user_id)
1791
1796
@@ -1796,6 +1801,7 class Repository(Base, BaseModel):
1796 q = q.order_by(func.lower(Repository.repo_name))
1801 q = q.order_by(func.lower(Repository.repo_name))
1797 else:
1802 else:
1798 q = q.order_by(Repository.repo_name)
1803 q = q.order_by(Repository.repo_name)
1804
1799 return q.all()
1805 return q.all()
1800
1806
1801 @property
1807 @property
@@ -207,8 +207,8 class RepoModel(BaseModel):
207 def quick_menu(repo_name):
207 def quick_menu(repo_name):
208 return _render('quick_menu', repo_name)
208 return _render('quick_menu', repo_name)
209
209
210 def repo_lnk(name, rtype, rstate, private, fork_of):
210 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
211 return _render('repo_name', name, rtype, rstate, private, fork_of,
211 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
212 short_name=not admin, admin=False)
212 short_name=not admin, admin=False)
213
213
214 def last_change(last_change):
214 def last_change(last_change):
@@ -246,8 +246,8 class RepoModel(BaseModel):
246 row = {
246 row = {
247 "menu": quick_menu(repo.repo_name),
247 "menu": quick_menu(repo.repo_name),
248
248
249 "name": repo_lnk(repo.repo_name, repo.repo_type,
249 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
250 repo.repo_state, repo.private, repo.fork),
250 repo.private, repo.archived, repo.fork),
251 "name_raw": repo.repo_name.lower(),
251 "name_raw": repo.repo_name.lower(),
252
252
253 "last_change": last_change(repo.last_db_change),
253 "last_change": last_change(repo.last_db_change),
@@ -427,6 +427,7 class RepoModel(BaseModel):
427 new_repo.group = repo_group
427 new_repo.group = repo_group
428 new_repo.description = description or repo_name
428 new_repo.description = description or repo_name
429 new_repo.private = private
429 new_repo.private = private
430 new_repo.archived = False
430 new_repo.clone_uri = clone_uri
431 new_repo.clone_uri = clone_uri
431 new_repo.landing_rev = landing_rev
432 new_repo.landing_rev = landing_rev
432
433
@@ -608,6 +609,23 class RepoModel(BaseModel):
608 from rhodecode.lib.celerylib import tasks, run_task
609 from rhodecode.lib.celerylib import tasks, run_task
609 return run_task(tasks.create_repo_fork, form_data, cur_user)
610 return run_task(tasks.create_repo_fork, form_data, cur_user)
610
611
612 def archive(self, repo):
613 """
614 Archive given repository. Set archive flag.
615
616 :param repo:
617 """
618 repo = self._get_repo(repo)
619 if repo:
620
621 try:
622 repo.archived = True
623 self.sa.add(repo)
624 self.sa.commit()
625 except Exception:
626 log.error(traceback.format_exc())
627 raise
628
611 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
629 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
612 """
630 """
613 Delete given repository, forks parameter defines what do do with
631 Delete given repository, forks parameter defines what do do with
@@ -616,6 +634,7 class RepoModel(BaseModel):
616
634
617 :param repo:
635 :param repo:
618 :param forks: str 'delete' or 'detach'
636 :param forks: str 'delete' or 'detach'
637 :param pull_requests: str 'delete' or None
619 :param fs_remove: remove(archive) repo from filesystem
638 :param fs_remove: remove(archive) repo from filesystem
620 """
639 """
621 if not cur_user:
640 if not cur_user:
@@ -228,6 +228,7 function registerRCRoutes() {
228 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']);
228 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']);
229 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
229 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
230 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
230 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
231 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
231 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
232 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
232 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
233 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
233 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
234 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
@@ -98,7 +98,7
98 <button class="btn btn-small" type="submit"
98 <button class="btn btn-small" type="submit"
99 onclick="return confirm('${_('Confirm to lock repository.')}');">
99 onclick="return confirm('${_('Confirm to lock repository.')}');">
100 <i class="icon-lock"></i>
100 <i class="icon-lock"></i>
101 ${_('Lock Repository')}
101 ${_('Lock repository')}
102 </button>
102 </button>
103 %endif
103 %endif
104 </div>
104 </div>
@@ -111,6 +111,35
111 </div>
111 </div>
112 </div>
112 </div>
113
113
114
115 <div class="panel panel-warning">
116 <div class="panel-heading" id="advanced-archive">
117 <h3 class="panel-title">${_('Archive repository')} <a class="permalink" href="#advanced-archive"></a></h3>
118 </div>
119 <div class="panel-body">
120 ${h.secure_form(h.route_path('edit_repo_advanced_archive', repo_name=c.repo_name), request=request)}
121
122 <div style="margin: 0 0 20px 0" class="fake-space"></div>
123
124 <div class="field">
125 <button class="btn btn-small btn-danger" type="submit"
126 onclick="return confirm('${_('Confirm to archive this repository: %s') % c.repo_name}');">
127 <i class="icon-remove-sign"></i>
128 ${_('Archive this repository')}
129 </button>
130 </div>
131 <div class="field">
132 <span class="help-block">
133 ${_('Archiving the repository will make it entirely read-only. The repository cannot be committed to.'
134 'It is hidden from the search results and dashboard. ')}
135 </span>
136 </div>
137
138 ${h.end_form()}
139 </div>
140 </div>
141
142
114 <div class="panel panel-danger">
143 <div class="panel panel-danger">
115 <div class="panel-heading" id="advanced-delete">
144 <div class="panel-heading" id="advanced-delete">
116 <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"></a></h3>
145 <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"></a></h3>
@@ -152,7 +181,7
152 <button class="btn btn-small btn-danger" type="submit"
181 <button class="btn btn-small btn-danger" type="submit"
153 onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
182 onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
154 <i class="icon-remove-sign"></i>
183 <i class="icon-remove-sign"></i>
155 ${_('Delete This Repository')}
184 ${_('Delete this repository')}
156 </button>
185 </button>
157 </div>
186 </div>
158 <div class="field">
187 <div class="field">
@@ -287,6 +287,11
287 </div>
287 </div>
288 <div class="clear"></div>
288 <div class="clear"></div>
289 </div>
289 </div>
290 % if c.rhodecode_db_repo.archived:
291 <div class="alert alert-warning text-center">
292 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
293 </div>
294 % endif
290 <!--- END CONTEXT BAR -->
295 <!--- END CONTEXT BAR -->
291
296
292 </%def>
297 </%def>
@@ -66,7 +66,7
66 </div>
66 </div>
67 </%def>
67 </%def>
68
68
69 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
69 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
70 <%
70 <%
71 def get_name(name,short_name=short_name):
71 def get_name(name,short_name=short_name):
72 if short_name:
72 if short_name:
@@ -105,6 +105,7
105 (${_('creating...')})
105 (${_('creating...')})
106 </span>
106 </span>
107 %endif
107 %endif
108
108 </div>
109 </div>
109 </%def>
110 </%def>
110
111
@@ -561,7 +561,7 def test_get_repo_by_id(test, expected):
561
561
562
562
563 def test_invalidation_context(baseapp):
563 def test_invalidation_context(baseapp):
564 repo_id = 999
564 repo_id = 9999
565
565
566 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
566 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
567 repo_id, CacheKey.CACHE_TYPE_README)
567 repo_id, CacheKey.CACHE_TYPE_README)
@@ -25,10 +25,13 import pytest
25 from rhodecode.lib.vcs.backends.git.repository import GitRepository
25 from rhodecode.lib.vcs.backends.git.repository import GitRepository
26 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
26 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
27 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.lib.vcs.nodes import FileNode
28 from rhodecode.model.db import Repository
28 from rhodecode.model.meta import Session
29 from rhodecode.model.meta import Session
30 from rhodecode.tests import GIT_REPO, HG_REPO
29
31
30 from rhodecode.tests.vcs_operations import (
32 from rhodecode.tests.vcs_operations import (
31 Command, _check_proper_clone, _check_proper_git_push, _check_proper_hg_push)
33 Command, _check_proper_clone, _check_proper_git_push, _check_proper_hg_push,
34 _add_files_and_push)
32
35
33
36
34 @pytest.mark.usefixtures("disable_locking")
37 @pytest.mark.usefixtures("disable_locking")
@@ -242,3 +245,37 class TestVCSOperationsSpecial(object):
242 assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout
245 assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout
243 assert 'remote: RhodeCode: push completed' in stdout
246 assert 'remote: RhodeCode: push completed' in stdout
244 assert 'exporting bookmark feature2' in stdout
247 assert 'exporting bookmark feature2' in stdout
248
249 def test_push_is_forbidden_on_archived_repo_hg(self, backend_hg, rc_web_server, tmpdir):
250 empty_repo = backend_hg.create_repo()
251 repo_name = empty_repo.repo_name
252
253 repo = Repository.get_by_repo_name(repo_name)
254 repo.archived = True
255 Session().commit()
256
257 clone_url = rc_web_server.repo_clone_url(repo_name)
258 stdout, stderr = Command('/tmp').execute(
259 'hg clone', clone_url, tmpdir.strpath)
260
261 stdout, stderr = _add_files_and_push(
262 'hg', tmpdir.strpath, clone_url=clone_url)
263
264 assert 'abort: HTTP Error 403: Forbidden' in stderr
265
266 def test_push_is_forbidden_on_archived_repo_git(self, backend_git, rc_web_server, tmpdir):
267 empty_repo = backend_git.create_repo()
268 repo_name = empty_repo.repo_name
269
270 repo = Repository.get_by_repo_name(repo_name)
271 repo.archived = True
272 Session().commit()
273
274 clone_url = rc_web_server.repo_clone_url(repo_name)
275 stdout, stderr = Command('/tmp').execute(
276 'git clone', clone_url, tmpdir.strpath)