Show More
@@ -715,6 +715,7 b' class BaseReferencesView(RepoAppView):' | |||
|
715 | 715 | { |
|
716 | 716 | "name": _render("name", ref_name, files_url, closed), |
|
717 | 717 | "name_raw": ref_name, |
|
718 | "closed": closed, | |
|
718 | 719 | "date": _render("date", commit.date), |
|
719 | 720 | "date_raw": datetime_to_time(commit.date), |
|
720 | 721 | "author": _render("author", commit.author), |
@@ -591,6 +591,15 b' def includeme(config):' | |||
|
591 | 591 | route_name='branches_home', request_method='GET', |
|
592 | 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 | 603 | # Bookmarks |
|
595 | 604 | config.add_route( |
|
596 | 605 | name='bookmarks_home', |
@@ -19,6 +19,7 b'' | |||
|
19 | 19 | import pytest |
|
20 | 20 | from rhodecode.model.db import Repository |
|
21 | 21 | from rhodecode.tests.routes import route_path |
|
22 | from rhodecode.tests import assert_session_flash | |
|
22 | 23 | |
|
23 | 24 | |
|
24 | 25 | @pytest.mark.usefixtures('autologin_user', 'app') |
@@ -33,3 +34,50 b' class TestBranchesController(object):' | |||
|
33 | 34 | for commit_id, obj_name in repo.scm_instance().branches.items(): |
|
34 | 35 | assert commit_id in response |
|
35 | 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 | 19 | import logging |
|
20 | 20 | |
|
21 | from pyramid.httpexceptions import HTTPFound | |
|
21 | 22 | |
|
22 | 23 | from rhodecode.apps._base import BaseReferencesView |
|
23 | 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 | 27 | from rhodecode.model.scm import ScmModel |
|
28 | from rhodecode.model.meta import Session | |
|
29 | from rhodecode.model.db import PullRequest | |
|
26 | 30 | |
|
27 | 31 | log = logging.getLogger(__name__) |
|
28 | 32 | |
@@ -33,15 +37,71 b' class RepoBranchesView(BaseReferencesVie' | |||
|
33 | 37 | @HasRepoPermissionAnyDecorator( |
|
34 | 38 | 'repository.read', 'repository.write', 'repository.admin') |
|
35 | 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 | 43 | c = self.load_default_context() |
|
37 | 44 | self._prepare_and_set_clone_url(c) |
|
38 | 45 | c.rhodecode_repo = self.rhodecode_vcs_repo |
|
39 | 46 | c.repository_forks = ScmModel().get_forks(self.db_repo) |
|
40 | ||
|
41 | 47 | ref_items = self.rhodecode_vcs_repo.branches_all.items() |
|
42 | 48 | data = self.load_refs_context( |
|
43 | 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 | 58 | c.has_references = bool(data) |
|
46 | 59 | c.data = ext_json.str_json(data) |
|
47 | 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 | 326 | def _get_branches(self): |
|
327 | 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 | 332 | @CachedProperty |
|
330 | 333 | def branches(self): |
|
331 | 334 | return self._get_branches() |
@@ -4585,6 +4585,12 b' class PullRequest(Base, _PullRequestBase' | |||
|
4585 | 4585 | else: |
|
4586 | 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 | 4594 | reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request') |
|
4589 | 4595 | statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request') |
|
4590 | 4596 | comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request') |
@@ -92,6 +92,7 b' function registerRCRoutes() {' | |||
|
92 | 92 | pyroutes.register('auth_home', '/_admin/auth*traverse', []); |
|
93 | 93 | pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']); |
|
94 | 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 | 96 | pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []); |
|
96 | 97 | pyroutes.register('channelstream_proxy', '/_channelstream', []); |
|
97 | 98 | pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []); |
@@ -62,13 +62,8 b'' | |||
|
62 | 62 | }; |
|
63 | 63 | |
|
64 | 64 | var branches_data = ${c.data|n}; |
|
65 | // object list | |
|
66 | $('#obj_list_table').DataTable({ | |
|
67 | data: branches_data, | |
|
68 | dom: 'rtp', | |
|
69 | pageLength: ${c.visual.dashboard_items}, | |
|
70 | order: [[ 0, "asc" ]], | |
|
71 | columns: [ | |
|
65 | var repo_type = "${c.rhodecode_db_repo.repo_type}"; | |
|
66 | var columns = [ | |
|
72 | 67 | { data: {"_": "name", |
|
73 | 68 | "sort": "name_raw"}, title: "${_('Name')}", className: "td-tags" }, |
|
74 | 69 | { data: {"_": "date", |
@@ -80,7 +75,22 b'' | |||
|
80 | 75 | "type": Number}, title: "${_('Commit')}", className: "td-hash" }, |
|
81 | 76 | { data: {"_": "compare", |
|
82 | 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 | 94 | language: { |
|
85 | 95 | paginate: DEFAULT_GRID_PAGINATION, |
|
86 | 96 | emptyTable: _gettext("No branches available yet.") |
@@ -277,6 +277,30 b'' | |||
|
277 | 277 | </div> |
|
278 | 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 | 304 | <%def name="user_group_actions(user_group_id, user_group_name)"> |
|
281 | 305 | <div class="grid_edit"> |
|
282 | 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 | 252 | "pullrequest_show_all_data": "/{repo_name}/pull-request-data", |
|
253 | 253 | "bookmarks_home": "/{repo_name}/bookmarks", |
|
254 | 254 | "branches_home": "/{repo_name}/branches", |
|
255 | "branch_remove": "/{repo_name}/{branch_name}/remove", | |
|
255 | 256 | "tags_home": "/{repo_name}/tags", |
|
256 | 257 | "repo_changelog": "/{repo_name}/changelog", |
|
257 | 258 | "repo_commits": "/{repo_name}/commits", |
General Comments 0
You need to be logged in to leave comments.
Login now