feature: makes forks and PR's link to clearly show admin what will be done

This commit is contained in:
ievgenii vdovenko 2025-07-31 15:14:23 +02:00
parent 03fc3ab570
commit d392589e5c
8 changed files with 92 additions and 83 deletions

View file

@ -50,7 +50,7 @@ from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import Session, ChangesetStatus, RepositoryField, Repository, RepoGroup, ChangesetComment
from rhodecode.model.permission import PermissionModel
from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo import RepoModel, ForksAction
from rhodecode.model.scm import ScmModel, RepoList
from rhodecode.model.settings import SettingsModel, VcsSettingsModel
from rhodecode.model import validation_schema
@ -1373,7 +1373,7 @@ def delete_repo(request, apiuser, repoid, forks=Optional("")):
raise JSONRPCError(f"Cannot delete `{repo.repo_name}` it still contains attached forks")
old_data = repo.get_api_data()
RepoModel().delete(repo, forks=RepoModel.ForkAction(handle_forks))
RepoModel().delete(repo, forks_action=ForksAction(handle_forks))
repo = audit_logger.RepoWrap(repo_id=None, repo_name=repo.repo_name)

View file

@ -31,7 +31,7 @@ from rhodecode.lib.utils2 import safe_int
from rhodecode.lib.vcs import RepositoryError
from rhodecode.model.db import Session, UserFollowing, User, Repository
from rhodecode.model.permission import PermissionModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo import RepoModel, ForksAction
from rhodecode.model.scm import ScmModel
log = logging.getLogger(__name__)
@ -56,6 +56,9 @@ class RepoSettingsAdvancedView(RepoAppView):
c = self.load_default_context()
c.active = "advanced"
c.fork_links = self._resolve_repo_forks_links()
c.pr_link = h.route_path("pullrequest_show_all", repo_name=self.db_repo_name)
c.default_user_id = User.get_default_user_id()
c.in_public_journal = (
UserFollowing.query()
@ -80,6 +83,14 @@ class RepoSettingsAdvancedView(RepoAppView):
return self._get_template_context(c)
def _resolve_repo_forks_links(self):
def resolve_fork_link(repo):
for fork in repo.forks:
yield h.route_path("repo_summary", repo_name=fork.repo_name)
yield from resolve_fork_link(fork)
return list(resolve_fork_link(self.db_repo))
@LoginRequired()
@HasRepoPermissionAnyDecorator("repository.admin")
@CSRFRequired()
@ -125,8 +136,6 @@ class RepoSettingsAdvancedView(RepoAppView):
"""
_ = self.request.translate
fork_action = self._get_fork_action()
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()
@ -134,14 +143,16 @@ class RepoSettingsAdvancedView(RepoAppView):
delete_cache = True
self._invalidate_remote_cache(delete=delete_cache)
RepoModel().delete(self.db_repo, forks=fork_action)
delete_res = RepoModel().delete(self.db_repo)
_forks = self.db_repo.forks.count()
if _forks and fork_action:
if fork_action is RepoModel.ForkAction.DETACH:
h.flash(_("Detached %s forks") % _forks, category="success")
elif fork_action is RepoModel.ForkAction.DELETE:
h.flash(_("Deleted %s forks") % _forks, category="success")
fork_action = delete_res.fork_action_result.action
fork_cnt = delete_res.fork_action_result.fork_count
if fork_action is not None:
if fork_action is ForksAction.DETACH:
h.flash(_("Detached %s forks") % fork_cnt, category="success")
elif fork_action is ForksAction.DELETE:
h.flash(_("Deleted %s forks") % fork_cnt, category="success")
repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
audit_logger.store_web(
@ -196,16 +207,6 @@ class RepoSettingsAdvancedView(RepoAppView):
raise HTTPFound(h.route_path("home"))
def _get_fork_action(self):
fork_action = self.request.POST.get("forks", None)
if fork_action == "detach_forks":
return RepoModel.ForkAction.DETACH
if fork_action == "delete_forks":
return RepoModel.ForkAction.DELETE
return RepoModel.ForkAction.UNKNOWN
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)

View file

@ -2583,10 +2583,10 @@ msgstr ""
msgid "Confirm to delete this repository"
msgstr ""
#: rhodecode/templates/admin/repos/repo_edit.html:310
#: rhodecode/templates/admin/repos/repo_edit.html:208
#, python-format
msgid "this repository has %s fork"
msgid_plural "this repository has %s forks"
msgid "Following fork will be %s"
msgid_plural "Following forks will be %s"
msgstr[0] ""
msgstr[1] ""

View file

@ -7567,7 +7567,7 @@ msgstr ""
msgid "Delete this repository"
msgstr ""
#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:237
#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:242
msgid "Warning: You are about to delete the repository. This action cannot be undone!"
msgstr ""

View file

@ -57,7 +57,6 @@ from rhodecode.model import meta
from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup
from rhodecode.model.meta import Session
log = logging.getLogger(__name__)
REMOVED_REPO_PAT = re.compile(r"rm__\d{8}_\d{6}_\d{6}__.*")
@ -665,7 +664,7 @@ def repo2db_mapper(initial_repo_list, force_hooks_rebuild=False):
def repo2db_cleanup(skip_repos=None, skip_groups=None):
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo import RepoModel, ForksAction
from rhodecode.model.repo_group import RepoGroupModel
sa = meta.Session()
@ -688,7 +687,7 @@ def repo2db_cleanup(skip_repos=None, skip_groups=None):
if not instance:
log.debug("Removing non-existing repository found in db `%s`", db_repo_name)
try:
RepoModel(sa).delete(db_repo, forks=RepoModel.ForkAction.DETACH, fs_remove=False, call_events=False)
RepoModel(sa).delete(db_repo, forks_action=ForksAction.DETACH, fs_remove=False, call_events=False)
sa.commit()
removed.append(db_repo_name)
except Exception:

View file

@ -23,8 +23,8 @@ import time
import logging
import traceback
import datetime
from dataclasses import dataclass, field
from enum import StrEnum, auto
from typing import Optional
from sqlalchemy.orm import aliased
from zope.cachedescriptors.property import Lazy as LazyProperty
@ -75,18 +75,31 @@ from rhodecode.model.permission import PermissionModel
log = logging.getLogger(__name__)
class ForksAction(StrEnum):
DELETE = auto()
DETACH = auto()
UNKNOWN = auto()
@classmethod
def _missing_(cls, value):
return cls(cls.UNKNOWN)
@dataclass
class ForkActionResult:
fork_count: int = 0
action: ForksAction = None
@dataclass
class DeleteRepoResult:
is_success: bool
fork_action_result: ForkActionResult = field(default_factory=ForkActionResult)
class RepoModel(BaseModel):
cls = Repository
class ForkAction(StrEnum):
DELETE = auto()
DETACH = auto()
UNKNOWN = auto()
@classmethod
def _missing_(cls, value):
return cls(cls.UNKNOWN)
def _get_user_group(self, users_group):
return self._get_instance(UserGroup, users_group, callback=UserGroup.get_by_group_name)
@ -762,37 +775,43 @@ class RepoModel(BaseModel):
def delete(
self,
repo: Repository,
forks: ForkAction = None,
forks_action: ForksAction = None, # used only in API to enforce action
fs_remove: bool = True,
cur_user: User = None,
call_events: bool = True,
) -> bool:
) -> DeleteRepoResult:
"""
Delete given repository, forks parameter defines what do do with
attached forks. Throws AttachedForksError if deleted repo has attached
forks
Delete a given repository.
Throws AttachedForksError if a deleted repo has attached forks
:param repo:
:param forks: ForkAction 'delete' or 'detach', NOTE: left for backward compatibility, do not use it
:param forks_action:
NOTE: used only in API to enforce action;
If the repository is private, then forks will be removed.
If the repository is public, then forks will be detached.
:param fs_remove: remove(archive) repo from filesystem
"""
if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), "username", None)
repo = self._get_repo(repo)
if not repo:
return False
return DeleteRepoResult(is_success=False)
if forks is None or forks is self.ForkAction.UNKNOWN:
if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), "username", None)
if forks_action is None or forks_action is ForksAction.UNKNOWN:
is_private = repo.private
forks = self.ForkAction.DELETE if is_private else self.ForkAction.DETACH
forks_action = ForksAction.DELETE if is_private else ForksAction.DETACH
if forks is self.ForkAction.DETACH:
forks_cnt = repo.forks.count()
if forks_action is ForksAction.DETACH:
for r in repo.forks:
r.fork = None
self.sa.add(r)
elif forks is self.ForkAction.DELETE:
elif forks_action is ForksAction.DELETE:
for r in repo.forks:
self.delete(r, forks=self.ForkAction.DELETE)
self.delete(r, forks_action=forks_action)
elif repo.forks.first() is not None:
raise AttachedForksError()
@ -821,7 +840,9 @@ class RepoModel(BaseModel):
log.error(traceback.format_exc())
raise
return True
return DeleteRepoResult(
is_success=True, fork_action_result=ForkActionResult(action=forks_action, fork_count=forks_cnt)
)
def _delete_repo_related_assets(self, repo):
pr_sources = repo.pull_requests_source

View file

@ -200,21 +200,19 @@
<div class="panel-body">
${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), request=request)}
<table class="display">
<% forks = c.rhodecode_db_repo.forks.count() %>
<% forks = len(c.fork_links) %>
<% delete_forks = c.rhodecode_db_repo.private %>
% if forks:
<tr>
<td>
${_ungettext('This repository has %s fork.', 'This repository has %s forks.', forks) % forks}
<td class="alert-${'error' if delete_forks else 'warning'}" style="padding: 10px">
${_ungettext('Following fork will be %s:', 'Following forks will be %s:', forks) % ('deleted' if delete_forks else 'detached')}
</td>
<td>
%if forks:
<input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
%endif
</td>
<td>
%if forks:
<input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label>
%endif
<ul>
% for fork in c.fork_links:
<li class="list-unstyled"><a href="${fork}" target="_blank" rel="noopener noreferrer">${fork.replace("/", "")} <span aria-hidden="true" style="font-size: 0.9em;">↗️</span></a></li>
% endfor
</ul>
</td>
</tr>
% endif
@ -222,8 +220,10 @@
<% attached_prs = len(c.rhodecode_db_repo.pull_requests_source + c.rhodecode_db_repo.pull_requests_target) %>
% if attached_prs:
<tr>
<td colspan="3">
${_ungettext('This repository has %s attached pull request.', 'This repository has %s attached pull requests.', attached_prs) % attached_prs}
<td colspan="2" style="padding: 10px">
<a href="${c.pr_link}" target="_blank" rel="noopener noreferrer">
${_ungettext('This repository has %s attached pull request (open/close).', 'This repository has %s attached pull requests (open/close).', attached_prs) % attached_prs} <span aria-hidden="true" style="font-size: 0.9em;">↗️</span>
</a>
</td>
</tr>
% endif
@ -231,23 +231,11 @@
<% attached_artifacts = len(c.rhodecode_db_repo.artifacts) %>
% if attached_artifacts:
<tr>
<td colspan="3">
<td colspan="2">
${_ungettext('This repository has %s attached artifact.', 'This repository has %s attached artifacts.', attached_artifacts) % attached_artifacts}
</td>
</tr>
% endif
<tr>
<td colspan="3">
% if attached_artifacts or attached_prs or forks:
<br/>
<br/>
% endif
<span class="alert-warning" style="padding: 10px">
<strong>${_('Warning: You are about to delete the repository. This action cannot be undone!')}</strong>
</span>
</td>
</tr>
</table>
<div style="margin: 0 0 20px 0" class="fake-space"></div>

View file

@ -26,7 +26,7 @@ from rhodecode.lib.exceptions import AttachedForksError
from rhodecode.lib.utils import make_db_config
from rhodecode.model.db import Repository
from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo import RepoModel, ForksAction
from rhodecode.model.scm import ScmModel
@ -61,7 +61,7 @@ class TestRepoModel(object):
fork_of_fork = backend.create_fork()
Session().commit()
RepoModel().delete(repo=repo, forks=RepoModel.ForkAction.DELETE)
RepoModel().delete(repo=repo, forks_action=ForksAction.DELETE)
Session().commit()
assert Repository.get_by_repo_name(repo_name=repo.repo_name) is None
@ -78,7 +78,7 @@ class TestRepoModel(object):
fork_of_fork = backend.create_fork()
Session().commit()
RepoModel().delete(repo=repo, forks=RepoModel.ForkAction.DETACH)
RepoModel().delete(repo=repo, forks_action=ForksAction.DETACH)
Session().commit()
assert Repository.get_by_repo_name(repo_name=repo.repo_name) is None