##// END OF EJS Templates
feat(branch removal trough UI): Added ability to remove branches trough UI from git and hg repositories. Fixes: RCCE-75
ilin.s -
r5428:fc536dab default
parent child Browse files
Show More
@@ -715,6 +715,7 b' class BaseReferencesView(RepoAppView):'
715 {
715 {
716 "name": _render("name", ref_name, files_url, closed),
716 "name": _render("name", ref_name, files_url, closed),
717 "name_raw": ref_name,
717 "name_raw": ref_name,
718 "closed": closed,
718 "date": _render("date", commit.date),
719 "date": _render("date", commit.date),
719 "date_raw": datetime_to_time(commit.date),
720 "date_raw": datetime_to_time(commit.date),
720 "author": _render("author", commit.author),
721 "author": _render("author", commit.author),
@@ -591,6 +591,15 b' def includeme(config):'
591 route_name='branches_home', request_method='GET',
591 route_name='branches_home', request_method='GET',
592 renderer='rhodecode:templates/branches/branches.mako')
592 renderer='rhodecode:templates/branches/branches.mako')
593
593
594 config.add_route(
595 name='branch_remove',
596 pattern='/{repo_name:.*?[^/]}/{branch_name:.*?[^/]}/remove', repo_route=True, repo_accepted_types=['hg', 'git'])
597 config.add_view(
598 RepoBranchesView,
599 attr='remove_branch',
600 route_name='branch_remove', request_method='POST'
601 )
602
594 # Bookmarks
603 # Bookmarks
595 config.add_route(
604 config.add_route(
596 name='bookmarks_home',
605 name='bookmarks_home',
@@ -19,6 +19,7 b''
19 import pytest
19 import pytest
20 from rhodecode.model.db import Repository
20 from rhodecode.model.db import Repository
21 from rhodecode.tests.routes import route_path
21 from rhodecode.tests.routes import route_path
22 from rhodecode.tests import assert_session_flash
22
23
23
24
24 @pytest.mark.usefixtures('autologin_user', 'app')
25 @pytest.mark.usefixtures('autologin_user', 'app')
@@ -33,3 +34,50 b' class TestBranchesController(object):'
33 for commit_id, obj_name in repo.scm_instance().branches.items():
34 for commit_id, obj_name in repo.scm_instance().branches.items():
34 assert commit_id in response
35 assert commit_id in response
35 assert obj_name in response
36 assert obj_name in response
37
38 def test_landing_branch_delete(self, backend, csrf_token):
39 if backend.alias == 'svn':
40 pytest.skip("Not supported yet")
41 branch_related_data_per_backend = {
42 'git': {'name': 'master'},
43 'hg': {'name': 'default'},
44 }
45 response = self.app.post(
46 route_path('branch_remove', repo_name=backend.repo_name,
47 branch_name=branch_related_data_per_backend[backend.alias]['name']),
48 params={'csrf_token': csrf_token}, status=302)
49 assert_session_flash(
50 response,
51 f"This branch {branch_related_data_per_backend[backend.alias]['name']} cannot be removed as it's currently set as landing branch"
52 )
53
54 def test_delete_branch_by_repo_owner(self, backend, csrf_token):
55 if backend.alias in ('svn', 'hg'):
56 pytest.skip("Skipping for hg and svn")
57 branch_to_be_removed = 'remove_me'
58 repo = Repository.get_by_repo_name(backend.repo_name)
59 repo.scm_instance()._create_branch(branch_to_be_removed, repo.scm_instance().commit_ids[1])
60 response = self.app.post(
61 route_path('branch_remove', repo_name=backend.repo_name,
62 branch_name=branch_to_be_removed),
63 params={'csrf_token': csrf_token}, status=302)
64 assert_session_flash(response, f"Branch {branch_to_be_removed} has been successfully deleted")
65
66 def test_delete_branch_by_not_repo_owner(self, backend, csrf_token):
67 username = 'test_regular'
68 pwd = 'test12'
69 branch_related_data_per_backend = {
70 'git': {'name': 'master', 'action': 'deleted'},
71 'hg': {'name': 'stable', 'action': 'closed'},
72 }
73 if backend.alias == 'svn':
74 pytest.skip("Not supported yet")
75 self.app.post(route_path('login'),
76 {'username': username,
77 'password': pwd})
78 selected_branch = branch_related_data_per_backend[backend.alias]['name']
79 response = self.app.post(
80 route_path('branch_remove', repo_name=backend.repo_name,
81 branch_name=selected_branch),
82 params={'csrf_token': csrf_token, 'username': username, 'password': pwd}, status=404)
83 assert response.status_code == 404
@@ -18,11 +18,15 b''
18
18
19 import logging
19 import logging
20
20
21 from pyramid.httpexceptions import HTTPFound
21
22
22 from rhodecode.apps._base import BaseReferencesView
23 from rhodecode.apps._base import BaseReferencesView
23 from rhodecode.lib import ext_json
24 from rhodecode.lib import ext_json
24 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
25 from rhodecode.lib import helpers as h
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
25 from rhodecode.model.scm import ScmModel
27 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.meta import Session
29 from rhodecode.model.db import PullRequest
26
30
27 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
28
32
@@ -33,15 +37,71 b' class RepoBranchesView(BaseReferencesVie'
33 @HasRepoPermissionAnyDecorator(
37 @HasRepoPermissionAnyDecorator(
34 'repository.read', 'repository.write', 'repository.admin')
38 'repository.read', 'repository.write', 'repository.admin')
35 def branches(self):
39 def branches(self):
40 partial_render = self.request.get_partial_renderer(
41 'rhodecode:templates/data_table/_dt_elements.mako')
42 repo_name = self.db_repo_name
36 c = self.load_default_context()
43 c = self.load_default_context()
37 self._prepare_and_set_clone_url(c)
44 self._prepare_and_set_clone_url(c)
38 c.rhodecode_repo = self.rhodecode_vcs_repo
45 c.rhodecode_repo = self.rhodecode_vcs_repo
39 c.repository_forks = ScmModel().get_forks(self.db_repo)
46 c.repository_forks = ScmModel().get_forks(self.db_repo)
40
41 ref_items = self.rhodecode_vcs_repo.branches_all.items()
47 ref_items = self.rhodecode_vcs_repo.branches_all.items()
42 data = self.load_refs_context(
48 data = self.load_refs_context(
43 ref_items=ref_items, partials_template='branches/branches_data.mako')
49 ref_items=ref_items, partials_template='branches/branches_data.mako')
44
50 data_with_actions = []
51 if self.db_repo.repo_type != 'svn':
52 for branch in data:
53 branch['action'] = partial_render(
54 f"branch_actions_{self.db_repo.repo_type}", branch['name_raw'], repo_name, closed=branch['closed']
55 )
56 data_with_actions.append(branch)
57 data = data_with_actions
45 c.has_references = bool(data)
58 c.has_references = bool(data)
46 c.data = ext_json.str_json(data)
59 c.data = ext_json.str_json(data)
47 return self._get_template_context(c)
60 return self._get_template_context(c)
61
62 @LoginRequired()
63 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
64 @CSRFRequired()
65 def remove_branch(self):
66 _ = self.request.translate
67 self.load_default_context()
68 repo = self.db_repo
69 repo_name = self.db_repo_name
70 repo_type = repo.repo_type
71 action = _('deleted') if repo_type == 'git' else _('closed')
72 redirect = HTTPFound(location=self.request.route_path('branches_home', repo_name=repo_name))
73 branch_name = self.request.matchdict.get('branch_name')
74 if repo.landing_ref_name == branch_name:
75 h.flash(
76 _("This branch {} cannot be removed as it's currently set as landing branch").format(branch_name),
77 category='error'
78 )
79 return redirect
80 if prs_related_to := Session().query(PullRequest).filter(PullRequest.target_repo_id == repo.repo_id,
81 PullRequest.status != PullRequest.STATUS_CLOSED).filter(
82 (PullRequest.source_ref.like(f'branch:{branch_name}:%')) | (
83 PullRequest.target_ref.like(f'branch:{branch_name}:%'))
84 ).all():
85 h.flash(_("Branch cannot be {} - it's used in following open Pull Request ids: {}").format(action, ','.join(
86 map(str, prs_related_to))), category='error')
87 return redirect
88
89 match repo_type:
90 case 'git':
91 self.rhodecode_vcs_repo.delete_branch(branch_name)
92 case 'hg':
93 from rhodecode.lib.vcs.backends.base import Reference
94 self.rhodecode_vcs_repo._local_close(
95 source_ref=Reference(type='branch', name=branch_name,
96 commit_id=self.rhodecode_vcs_repo.branches[branch_name]),
97 target_ref=Reference(type='branch', name='', commit_id=None),
98 user_name=self.request.user.name,
99 user_email=self.request.user.email)
100 case _:
101 raise NotImplementedError('Branch deleting functionality not yet implemented')
102 ScmModel().mark_for_invalidation(repo_name)
103 self.rhodecode_vcs_repo._invalidate_prop_cache('commit_ids')
104 self.rhodecode_vcs_repo._invalidate_prop_cache('_refs')
105 self.rhodecode_vcs_repo._invalidate_prop_cache('branches')
106 h.flash(_("Branch {} has been successfully {}").format(branch_name, action), category='success')
107 return redirect
@@ -326,6 +326,9 b' class GitRepository(BaseRepository):'
326 def _get_branches(self):
326 def _get_branches(self):
327 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
327 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
328
328
329 def delete_branch(self, branch_name):
330 return self._remote.delete_branch(branch_name)
331
329 @CachedProperty
332 @CachedProperty
330 def branches(self):
333 def branches(self):
331 return self._get_branches()
334 return self._get_branches()
@@ -4585,6 +4585,12 b' class PullRequest(Base, _PullRequestBase'
4585 else:
4585 else:
4586 return f'<DB:PullRequest at {id(self)!r}>'
4586 return f'<DB:PullRequest at {id(self)!r}>'
4587
4587
4588 def __str__(self):
4589 if self.pull_request_id:
4590 return f'#{self.pull_request_id}'
4591 else:
4592 return f'#{id(self)!r}'
4593
4588 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4594 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4589 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4595 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4590 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4596 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
@@ -92,6 +92,7 b' function registerRCRoutes() {'
92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
92 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
93 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
94 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
94 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
95 pyroutes.register('branch_remove', '/%(repo_name)s/%(branch_name)s/remove', ['repo_name', 'branch_name']);
95 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
98 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
@@ -62,13 +62,8 b''
62 };
62 };
63
63
64 var branches_data = ${c.data|n};
64 var branches_data = ${c.data|n};
65 // object list
65 var repo_type = "${c.rhodecode_db_repo.repo_type}";
66 $('#obj_list_table').DataTable({
66 var columns = [
67 data: branches_data,
68 dom: 'rtp',
69 pageLength: ${c.visual.dashboard_items},
70 order: [[ 0, "asc" ]],
71 columns: [
72 { data: {"_": "name",
67 { data: {"_": "name",
73 "sort": "name_raw"}, title: "${_('Name')}", className: "td-tags" },
68 "sort": "name_raw"}, title: "${_('Name')}", className: "td-tags" },
74 { data: {"_": "date",
69 { data: {"_": "date",
@@ -80,7 +75,22 b''
80 "type": Number}, title: "${_('Commit')}", className: "td-hash" },
75 "type": Number}, title: "${_('Commit')}", className: "td-hash" },
81 { data: {"_": "compare",
76 { data: {"_": "compare",
82 "sort": "compare"}, title: "${_('Compare')}", className: "td-compare" }
77 "sort": "compare"}, title: "${_('Compare')}", className: "td-compare" }
83 ],
78 ];
79 if (repo_type !== 'svn') {
80 columns.push({
81 data: { "_": "action", "sort": "action" },
82 title: `${_('Action')}`,
83 className: "td-action",
84 orderable: false
85 });
86 }
87
88 $('#obj_list_table').DataTable({
89 data: branches_data,
90 dom: 'rtp',
91 pageLength: ${c.visual.dashboard_items},
92 order: [[ 0, "asc" ]],
93 columns: columns,
84 language: {
94 language: {
85 paginate: DEFAULT_GRID_PAGINATION,
95 paginate: DEFAULT_GRID_PAGINATION,
86 emptyTable: _gettext("No branches available yet.")
96 emptyTable: _gettext("No branches available yet.")
@@ -277,6 +277,30 b''
277 </div>
277 </div>
278 </%def>
278 </%def>
279
279
280 <%def name="branch_actions_git(branch_name, repo_name, **kwargs)">
281 <div class="grid_delete">
282 ${h.secure_form(h.route_path('branch_remove', repo_name=repo_name, branch_name=branch_name), request=request)}
283 <input class="btn btn-link btn-danger" id="remove_branch_${branch_name}" name="remove_branch_${branch_name}"
284 onclick="submitConfirm(event, this, _gettext('Confirm to delete this branch'), _gettext('Delete'), '${branch_name}')"
285 type="submit" value="Delete"
286 >
287 ${h.end_form()}
288 </div>
289 </%def>
290
291 <%def name="branch_actions_hg(branch_name, repo_name, **kwargs)">
292 <div class="grid_delete">
293 %if not kwargs['closed']:
294 ${h.secure_form(h.route_path('branch_remove', repo_name=repo_name, branch_name=branch_name), request=request)}
295 <input class="btn btn-link btn-danger" id="remove_branch_${branch_name}" name="remove_branch_${branch_name}"
296 onclick="submitConfirm(event, this, _gettext('Confirm to close this branch'), _gettext('Close'), '${branch_name}')"
297 type="submit" value="Close"
298 >
299 ${h.end_form()}
300 %endif
301 </div>
302 </%def>
303
280 <%def name="user_group_actions(user_group_id, user_group_name)">
304 <%def name="user_group_actions(user_group_id, user_group_name)">
281 <div class="grid_edit">
305 <div class="grid_edit">
282 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
306 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
@@ -252,6 +252,7 b' def get_url_defs():'
252 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
252 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
253 "bookmarks_home": "/{repo_name}/bookmarks",
253 "bookmarks_home": "/{repo_name}/bookmarks",
254 "branches_home": "/{repo_name}/branches",
254 "branches_home": "/{repo_name}/branches",
255 "branch_remove": "/{repo_name}/{branch_name}/remove",
255 "tags_home": "/{repo_name}/tags",
256 "tags_home": "/{repo_name}/tags",
256 "repo_changelog": "/{repo_name}/changelog",
257 "repo_changelog": "/{repo_name}/changelog",
257 "repo_commits": "/{repo_name}/commits",
258 "repo_commits": "/{repo_name}/commits",
General Comments 0
You need to be logged in to leave comments. Login now