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