diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index 40d2886d..f6ec8bbe 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -333,7 +333,9 @@ class RepoAppView(BaseAppView): c.path_filter = self.path_filter # used by atom_feed_entry.mako - if self.rhodecode_vcs_repo is None: + is_broken = self.request.params.get("broken") # needed to prevent infinit loop + + if self.rhodecode_vcs_repo is None and is_broken is None: # unable to fetch this repo as vcs instance, report back to user log.debug("Repository was not found on filesystem, check if it exists or is not damaged") h.flash( @@ -353,8 +355,15 @@ class RepoAppView(BaseAppView): raise HTTPFound(h.route_path("repo_summary", repo_name=self.db_repo_name)) - else: # redirect if we don't show missing requirements - raise HTTPFound(h.route_path("home")) + else: # give user possibility to remove broken repo + raise HTTPFound( + h.route_path( + "edit_repo_advanced", + repo_name=self.db_repo_name, + _anchor="advanced-delete", + _query={"broken": True}, + ) + ) c.has_origin_repo_read_perm = False if self.db_repo.fork: diff --git a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py index 9d1d3124..94dbe7f8 100644 --- a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py +++ b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py @@ -115,6 +115,29 @@ class TestAdminRepoSettingsAdvanced(object): assert RepoModel().get_by_repo_name(repo_name) is None assert not repo_on_filesystem(repo_name_str) + @pytest.mark.parametrize("suffix", ["", "ąęł", "123"], ids=no_newline_id_generator) + def test_delete_broken_repo(self, autologin_user, backend, suffix, csrf_token): + repo = backend.create_repo(name_suffix=suffix, private_repo=True) + repo.private = True + Session().commit() + + repo_name = repo.repo_name + repo_name_str = safe_str(repo.repo_name) + + # delete from file system + RepoModel()._delete_filesystem_repo(repo) + assert not repo_on_filesystem(repo_name_str) + + response = self.app.post( + route_path("edit_repo_advanced_delete", repo_name=repo_name_str), params={"csrf_token": csrf_token} + ) + assert_session_flash(response, "Deleted repository `{}`".format(repo_name)) + response.follow() + + # check if repo was deleted from db + assert RepoModel().get_by_repo_name(repo_name) is None + assert not repo_on_filesystem(repo_name_str) + @patch.object(RepoSettingsAdvancedView, "_maybe_needs_password_change") @patch.object(RepoSettingsAdvancedView, "_maybe_needs_2fa_configuration") @patch.object(RepoSettingsAdvancedView, "_maybe_needs_2fa_check") @@ -128,6 +151,7 @@ class TestAdminRepoSettingsAdvanced(object): db_repo.scm_instance.return_value = scm_instance request.db_repo = db_repo + request.params = {} obj = RepoSettingsAdvancedView(MagicMock(), request) # bypass decorators diff --git a/rhodecode/apps/repository/tests/test_repo_summary.py b/rhodecode/apps/repository/tests/test_repo_summary.py index 9db750de..5123cfbd 100644 --- a/rhodecode/apps/repository/tests/test_repo_summary.py +++ b/rhodecode/apps/repository/tests/test_repo_summary.py @@ -17,6 +17,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import re +import urllib import mock import pytest @@ -232,6 +233,9 @@ class TestRepoLocation(object): f"The repository `{repo_name}` cannot be loaded in filesystem. Please check if it exist, or is not damaged." ) assert_session_flash(response, msg) + path = route_path("edit_repo_advanced", repo_name=safe_str(repo_name)) + path = urllib.parse.quote(path) + "?broken=True#advanced-delete" + assert response.location.endswith(path) @pytest.mark.parametrize("suffix", ["", "ąęł"], ids=["", "non-ascii"]) def test_missing_filesystem_repo_on_repo_check(self, autologin_user, backend, suffix, csrf_token): diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py index 3b5dc9c1..b4bc2bb3 100644 --- a/rhodecode/apps/repository/views/repo_settings_advanced.py +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -57,6 +57,17 @@ class RepoSettingsAdvancedView(RepoAppView): c = self.load_default_context() c.active = "advanced" + is_broken = self.request.params.get("broken") + if is_broken: + # Special case: the repository is missing on the filesystem, so its state is unknown. + # Only allow deletion in this mode. + c.fork_links = [] + c.pr_link = None + c.in_public_journal = None + c.ver_info_dict = {} + c.hooks_outdated = False + return self._get_template_context(c) + c.fork_links = self._resolve_repo_forks_links() c.pr_link = h.route_path("pullrequest_show_all", repo_name=self.db_repo_name) @@ -139,8 +150,6 @@ class RepoSettingsAdvancedView(RepoAppView): repo_advanced_url = h.route_path("edit_repo_advanced", repo_name=self.db_repo_name, _anchor="advanced-delete") try: - old_data = self.db_repo.get_api_data() - delete_cache = True self._invalidate_remote_cache(delete=delete_cache) @@ -159,6 +168,7 @@ class RepoSettingsAdvancedView(RepoAppView): h.flash(_("Deleted %s forks") % fork_cnt, category="success") repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name) + old_data = self.db_repo.get_api_data() audit_logger.store_web( "repo.delete", action_data={"old_data": old_data}, user=self._rhodecode_user, repo=repo ) @@ -203,8 +213,8 @@ class RepoSettingsAdvancedView(RepoAppView): # redirect to advanced for forks handle action ? raise HTTPFound(repo_advanced_url) - except Exception: - log.exception("Exception during deletion of repository") + except Exception as e: + log.exception(f"Exception during deletion of repository: {str(e)}") h.flash(_("An error occurred during deletion 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-delete")) @@ -214,7 +224,9 @@ class RepoSettingsAdvancedView(RepoAppView): def _invalidate_remote_cache(self, delete: bool): log.debug(f"Invalidating remote cache, for repository: {self.db_repo_name}") scm_repo = self.db_repo.scm_instance(cache=False) - scm_repo.vcsserver_invalidate_cache(delete=delete) + if scm_repo is not None: + # if it's None - probably repo is broken and physically absent on the disk + scm_repo.vcsserver_invalidate_cache(delete=delete) @LoginRequired() @HasRepoPermissionAnyDecorator("repository.admin")