feat(branch removal trough UI): Added ability to remove branches trough UI from git and hg repositories. Fixes: RCCE-75

This commit is contained in:
Serhii Ilin 2024-06-07 12:28:04 +03:00
parent 3d0e81968b
commit 59e99e4368
10 changed files with 174 additions and 11 deletions

View file

@ -715,6 +715,7 @@ class BaseReferencesView(RepoAppView):
{
"name": _render("name", ref_name, files_url, closed),
"name_raw": ref_name,
"closed": closed,
"date": _render("date", commit.date),
"date_raw": datetime_to_time(commit.date),
"author": _render("author", commit.author),

View file

@ -591,6 +591,15 @@ def includeme(config):
route_name='branches_home', request_method='GET',
renderer='rhodecode:templates/branches/branches.mako')
config.add_route(
name='branch_remove',
pattern='/{repo_name:.*?[^/]}/{branch_name:.*?[^/]}/remove', repo_route=True, repo_accepted_types=['hg', 'git'])
config.add_view(
RepoBranchesView,
attr='remove_branch',
route_name='branch_remove', request_method='POST'
)
# Bookmarks
config.add_route(
name='bookmarks_home',

View file

@ -19,6 +19,7 @@
import pytest
from rhodecode.model.db import Repository
from rhodecode.tests.routes import route_path
from rhodecode.tests import assert_session_flash
@pytest.mark.usefixtures('autologin_user', 'app')
@ -33,3 +34,50 @@ class TestBranchesController(object):
for commit_id, obj_name in repo.scm_instance().branches.items():
assert commit_id in response
assert obj_name in response
def test_landing_branch_delete(self, backend, csrf_token):
if backend.alias == 'svn':
pytest.skip("Not supported yet")
branch_related_data_per_backend = {
'git': {'name': 'master'},
'hg': {'name': 'default'},
}
response = self.app.post(
route_path('branch_remove', repo_name=backend.repo_name,
branch_name=branch_related_data_per_backend[backend.alias]['name']),
params={'csrf_token': csrf_token}, status=302)
assert_session_flash(
response,
f"This branch {branch_related_data_per_backend[backend.alias]['name']} cannot be removed as it's currently set as landing branch"
)
def test_delete_branch_by_repo_owner(self, backend, csrf_token):
if backend.alias in ('svn', 'hg'):
pytest.skip("Skipping for hg and svn")
branch_to_be_removed = 'remove_me'
repo = Repository.get_by_repo_name(backend.repo_name)
repo.scm_instance()._create_branch(branch_to_be_removed, repo.scm_instance().commit_ids[1])
response = self.app.post(
route_path('branch_remove', repo_name=backend.repo_name,
branch_name=branch_to_be_removed),
params={'csrf_token': csrf_token}, status=302)
assert_session_flash(response, f"Branch {branch_to_be_removed} has been successfully deleted")
def test_delete_branch_by_not_repo_owner(self, backend, csrf_token):
username = 'test_regular'
pwd = 'test12'
branch_related_data_per_backend = {
'git': {'name': 'master', 'action': 'deleted'},
'hg': {'name': 'stable', 'action': 'closed'},
}
if backend.alias == 'svn':
pytest.skip("Not supported yet")
self.app.post(route_path('login'),
{'username': username,
'password': pwd})
selected_branch = branch_related_data_per_backend[backend.alias]['name']
response = self.app.post(
route_path('branch_remove', repo_name=backend.repo_name,
branch_name=selected_branch),
params={'csrf_token': csrf_token, 'username': username, 'password': pwd}, status=404)
assert response.status_code == 404

View file

@ -18,11 +18,15 @@
import logging
from pyramid.httpexceptions import HTTPFound
from rhodecode.apps._base import BaseReferencesView
from rhodecode.lib import ext_json
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
from rhodecode.model.scm import ScmModel
from rhodecode.model.meta import Session
from rhodecode.model.db import PullRequest
log = logging.getLogger(__name__)
@ -33,15 +37,71 @@ class RepoBranchesView(BaseReferencesView):
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def branches(self):
partial_render = self.request.get_partial_renderer(
'rhodecode:templates/data_table/_dt_elements.mako')
repo_name = self.db_repo_name
c = self.load_default_context()
self._prepare_and_set_clone_url(c)
c.rhodecode_repo = self.rhodecode_vcs_repo
c.repository_forks = ScmModel().get_forks(self.db_repo)
ref_items = self.rhodecode_vcs_repo.branches_all.items()
data = self.load_refs_context(
ref_items=ref_items, partials_template='branches/branches_data.mako')
data_with_actions = []
if self.db_repo.repo_type != 'svn':
for branch in data:
branch['action'] = partial_render(
f"branch_actions_{self.db_repo.repo_type}", branch['name_raw'], repo_name, closed=branch['closed']
)
data_with_actions.append(branch)
data = data_with_actions
c.has_references = bool(data)
c.data = ext_json.str_json(data)
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
@CSRFRequired()
def remove_branch(self):
_ = self.request.translate
self.load_default_context()
repo = self.db_repo
repo_name = self.db_repo_name
repo_type = repo.repo_type
action = _('deleted') if repo_type == 'git' else _('closed')
redirect = HTTPFound(location=self.request.route_path('branches_home', repo_name=repo_name))
branch_name = self.request.matchdict.get('branch_name')
if repo.landing_ref_name == branch_name:
h.flash(
_("This branch {} cannot be removed as it's currently set as landing branch").format(branch_name),
category='error'
)
return redirect
if prs_related_to := Session().query(PullRequest).filter(PullRequest.target_repo_id == repo.repo_id,
PullRequest.status != PullRequest.STATUS_CLOSED).filter(
(PullRequest.source_ref.like(f'branch:{branch_name}:%')) | (
PullRequest.target_ref.like(f'branch:{branch_name}:%'))
).all():
h.flash(_("Branch cannot be {} - it's used in following open Pull Request ids: {}").format(action, ','.join(
map(str, prs_related_to))), category='error')
return redirect
match repo_type:
case 'git':
self.rhodecode_vcs_repo.delete_branch(branch_name)
case 'hg':
from rhodecode.lib.vcs.backends.base import Reference
self.rhodecode_vcs_repo._local_close(
source_ref=Reference(type='branch', name=branch_name,
commit_id=self.rhodecode_vcs_repo.branches[branch_name]),
target_ref=Reference(type='branch', name='', commit_id=None),
user_name=self.request.user.name,
user_email=self.request.user.email)
case _:
raise NotImplementedError('Branch deleting functionality not yet implemented')
ScmModel().mark_for_invalidation(repo_name)
self.rhodecode_vcs_repo._invalidate_prop_cache('commit_ids')
self.rhodecode_vcs_repo._invalidate_prop_cache('_refs')
self.rhodecode_vcs_repo._invalidate_prop_cache('branches')
h.flash(_("Branch {} has been successfully {}").format(branch_name, action), category='success')
return redirect

View file

@ -326,6 +326,9 @@ class GitRepository(BaseRepository):
def _get_branches(self):
return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
def delete_branch(self, branch_name):
return self._remote.delete_branch(branch_name)
@CachedProperty
def branches(self):
return self._get_branches()

View file

@ -4585,6 +4585,12 @@ class PullRequest(Base, _PullRequestBase):
else:
return f'<DB:PullRequest at {id(self)!r}>'
def __str__(self):
if self.pull_request_id:
return f'#{self.pull_request_id}'
else:
return f'#{id(self)!r}'
reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')

View file

@ -92,6 +92,7 @@ function registerRCRoutes() {
pyroutes.register('auth_home', '/_admin/auth*traverse', []);
pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
pyroutes.register('branch_remove', '/%(repo_name)s/%(branch_name)s/remove', ['repo_name', 'branch_name']);
pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
pyroutes.register('channelstream_proxy', '/_channelstream', []);
pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);

View file

@ -62,13 +62,8 @@ $(document).ready(function() {
};
var branches_data = ${c.data|n};
// object list
$('#obj_list_table').DataTable({
data: branches_data,
dom: 'rtp',
pageLength: ${c.visual.dashboard_items},
order: [[ 0, "asc" ]],
columns: [
var repo_type = "${c.rhodecode_db_repo.repo_type}";
var columns = [
{ data: {"_": "name",
"sort": "name_raw"}, title: "${_('Name')}", className: "td-tags" },
{ data: {"_": "date",
@ -80,7 +75,22 @@ $(document).ready(function() {
"type": Number}, title: "${_('Commit')}", className: "td-hash" },
{ data: {"_": "compare",
"sort": "compare"}, title: "${_('Compare')}", className: "td-compare" }
],
];
if (repo_type !== 'svn') {
columns.push({
data: { "_": "action", "sort": "action" },
title: `${_('Action')}`,
className: "td-action",
orderable: false
});
}
$('#obj_list_table').DataTable({
data: branches_data,
dom: 'rtp',
pageLength: ${c.visual.dashboard_items},
order: [[ 0, "asc" ]],
columns: columns,
language: {
paginate: DEFAULT_GRID_PAGINATION,
emptyTable: _gettext("No branches available yet.")

View file

@ -277,6 +277,30 @@ ${h.style_metatag(tag_type, tag)|n,trim}
</div>
</%def>
<%def name="branch_actions_git(branch_name, repo_name, **kwargs)">
<div class="grid_delete">
${h.secure_form(h.route_path('branch_remove', repo_name=repo_name, branch_name=branch_name), request=request)}
<input class="btn btn-link btn-danger" id="remove_branch_${branch_name}" name="remove_branch_${branch_name}"
onclick="submitConfirm(event, this, _gettext('Confirm to delete this branch'), _gettext('Delete'), '${branch_name}')"
type="submit" value="Delete"
>
${h.end_form()}
</div>
</%def>
<%def name="branch_actions_hg(branch_name, repo_name, **kwargs)">
<div class="grid_delete">
%if not kwargs['closed']:
${h.secure_form(h.route_path('branch_remove', repo_name=repo_name, branch_name=branch_name), request=request)}
<input class="btn btn-link btn-danger" id="remove_branch_${branch_name}" name="remove_branch_${branch_name}"
onclick="submitConfirm(event, this, _gettext('Confirm to close this branch'), _gettext('Close'), '${branch_name}')"
type="submit" value="Close"
>
${h.end_form()}
%endif
</div>
</%def>
<%def name="user_group_actions(user_group_id, user_group_name)">
<div class="grid_edit">
<a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>

View file

@ -252,6 +252,7 @@ def get_url_defs():
"pullrequest_show_all_data": "/{repo_name}/pull-request-data",
"bookmarks_home": "/{repo_name}/bookmarks",
"branches_home": "/{repo_name}/branches",
"branch_remove": "/{repo_name}/{branch_name}/remove",
"tags_home": "/{repo_name}/tags",
"repo_changelog": "/{repo_name}/changelog",
"repo_commits": "/{repo_name}/commits",