From 6f780dccb1b1ef8bb4d1d75b955b7567095a0dc2 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 23 Dec 2025 20:10:49 +0100 Subject: [PATCH 1/5] fix(diffs): bumped diffs cache to invalidate previous logic on single branch --- rhodecode/lib/diffs.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py index 6a756811..4b3b9241 100644 --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -269,13 +269,15 @@ class DiffProcessor(object): end += 1 if start or end: - def do(l): - last = end + len(l["line"]) - if l["action"] == Action.ADD: + def do(_line): + last = end + len(_line["line"]) + if _line["action"] == Action.ADD: tag = "ins" else: tag = "del" - l["line"] = f"{l['line'][:start]}<{tag}>{l['line'][start:last]}{l['line'][last:]}" + _line["line"] = ( + f"{_line['line'][:start]}<{tag}>{_line['line'][start:last]}{_line['line'][last:]}" + ) do(line) do(next_) @@ -1045,8 +1047,8 @@ class DiffLimitExceeded(Exception): # NOTE(marcink): if diffs.mako change, probably this -# needs a bump to next version -CURRENT_DIFF_VERSION = "v5" +# needs a bump to the next version +CURRENT_DIFF_VERSION = "v6" def _cleanup_cache_file(cached_diff_file): From ee52770fff0da2981f97c9bc3edb401231fdc781 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 22 Dec 2025 14:08:02 +0100 Subject: [PATCH 2/5] feature(repo): added ability to bootstrap repositories on creation --- rhodecode/lib/celerylib/tasks.py | 11 ++ rhodecode/model/forms.py | 3 + rhodecode/model/repo.py | 44 +++++- .../templates/admin/repos/repo_add_base.mako | 148 ++++++++++++++---- 4 files changed, 176 insertions(+), 30 deletions(-) diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py index 1bfa2b41..bd4a71e5 100644 --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -204,6 +204,9 @@ def create_repo(form_data, cur_user): fork_of = form_data.get("fork_parent_id") state = form_data.get("repo_state", Repository.STATE_PENDING) + repo_bootstrap_readme = form_data.get("repo_bootstrap_readme") + repo_bootstrap_svn_branch_struct = form_data.get("repo_bootstrap_svn_branch_struct") + # repo creation defaults, private and repo_type are filled in form defs = SettingsModel().get_default_repo_settings(strip_prefix=True) enable_statistics = form_data.get("enable_statistics", defs.get("repo_enable_statistics")) @@ -261,6 +264,14 @@ def create_repo(form_data, cur_user): repo_id = repo.repo_id repo_data = repo.get_api_data() + commit = RepoModel().bootstrap( + db_repo=repo, + db_user=cur_user, + readme=repo_bootstrap_readme, + svn_struct=repo_bootstrap_svn_branch_struct, + ) + log.debug("Bootstrap commit: %s", commit) + audit_logger.store( "repo.create", action_data={"data": repo_data}, diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index afcb55ba..fa2abceb 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -297,6 +297,9 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab repo_enable_downloads = v.StringBoolean(if_missing=False) repo_enable_locking = v.StringBoolean(if_missing=False) + repo_bootstrap_readme = v.StringBoolean(if_missing=False) + repo_bootstrap_svn_branch_struct = v.StringBoolean(if_missing=False) + if edit: # this is repo owner user = All(v.UnicodeString(not_empty=True), v.ValidRepoUser(localizer, allow_disabled)) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index ce7813f1..6c59e897 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -27,7 +27,6 @@ from dataclasses import dataclass, field from enum import StrEnum, auto from sqlalchemy.orm import aliased -from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode import events from rhodecode.lib.auth import HasUserGroupPermissionAny @@ -47,6 +46,7 @@ from rhodecode.lib.utils2 import ( action_logger_generic, ) from rhodecode.lib.vcs.backends import get_backend +from rhodecode.lib.vcs.backends.base import EmptyCommit from rhodecode.lib.vcs.nodes import NodeKind from rhodecode.model import BaseModel from rhodecode.model.db import ( @@ -677,6 +677,48 @@ class RepoModel(BaseModel): return run_task(tasks.create_repo, form_data, cur_user) + def bootstrap(self, db_repo, db_user, readme=None, svn_struct=None): + """ + Bootstrap new created repo with some files/structure + """ + from rhodecode.model.scm import ScmModel + from rhodecode.model.settings import SettingsModel + + _branch_name, _sha_commit_id, is_head = db_repo.landing_ref_name, "", True + author = db_user.full_contact + repo_name = db_repo.repo_name + message = "Bootstrap: created initial project files" + commit = EmptyCommit(alias=db_repo.repo_type, branch=_branch_name) + + rc_config = SettingsModel().get_all_settings(cache=True, from_request=False) + default_renderer = rc_config.get("rhodecode_markup_renderer", "rst") + + nodes = {} + if readme: + if default_renderer == "markdown": + ext = "md" + readme_content = f"# {repo_name}\n" + elif default_renderer == "rst": + ext = "rst" + readme_content = f"{repo_name}\n" + "-" * len(repo_name) + "\n" + else: + ext = ".txt" + readme_content = f"{repo_name}\n" + + nodes[safe_bytes(f"readme.{ext}")] = {"content": safe_bytes(readme_content)} + + if nodes: + commit = ScmModel().create_nodes( + user=db_user.user_id, + repo=db_repo, + message=message, + nodes=nodes, + parent_commit=commit, + author=author, + ) + return commit + return None + def update_permissions( self, repo, perm_additions=None, perm_updates=None, perm_deletions=None, check_perms=True, cur_user=None ): diff --git a/rhodecode/templates/admin/repos/repo_add_base.mako b/rhodecode/templates/admin/repos/repo_add_base.mako index d1a5c1ca..fefa90c6 100644 --- a/rhodecode/templates/admin/repos/repo_add_base.mako +++ b/rhodecode/templates/admin/repos/repo_add_base.mako @@ -5,16 +5,91 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
- +
+ + +
- ${h.text('repo_name', class_="medium")} -
- ${_('Import Existing Repository ?')} + + ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")} + / + ${h.text('repo_name', class_="medium", id="repo_name_input")} + + % if c.personal_repo_group: + + ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)} + + % endif + +
+ + ${_('Repository Group / Repository Name')}. + +
+ + ## COPY PERMS +
+
+ ${h.checkbox('repo_copy_permissions', value="True", checked="checked")} +
+ +
+ ${_('Copy permissions from parent repository group.')} +
+
+ + ## TODO: what is this ? i don't think we use it anymore %if not c.rhodecode_user.is_admin: ${h.hidden('user_created',True)} %endif +
-
-
- -
-
- ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")} - % if c.personal_repo_group: - - ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)} - - % endif - ${_('Optionally select a group to put this repository into.')} -
-
@@ -58,7 +119,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
- % for backend in c.backends: % if loop.index == 0: @@ -73,7 +133,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)} % endfor - ${_('Set the type of repository to create.')}
@@ -98,15 +157,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
-
-
- -
-
- ${h.checkbox('repo_copy_permissions', value="True", checked="checked")} - ${_('Copy permissions from parent repository group.')} -
-
+
@@ -116,6 +167,28 @@ ${h.secure_form(h.route_path('repo_create'), request=request)} ${_('Private repositories are only visible to people explicitly added as collaborators.')}
+ +
+
+ +
+
+ ${h.checkbox('repo_bootstrap_readme',value="True")} + ${_('Initialize repository with a README. Allows you to immediately clone this project’s repository.')} +
+
+ +
+
+ +
+
+ ${h.checkbox('repo_bootstrap_svn_branch_struct',value="True")} + ${_('Initialize repository with a branches/tags DIRS')} +
+
+ +
${h.submit('save',_('Create Repository'),class_="btn")}
@@ -135,6 +208,8 @@ ${h.secure_form(h.route_path('repo_create'), request=request)} $('#remote_clone_toggle').on('click', function(e){ $('#remote_clone').show(); e.preventDefault(); + $('#remote_clone_toggle').hide(); + $('.repo-bootstrap-items').remove(); }); if($('#remote_clone input').hasClass('error')){ @@ -163,6 +238,21 @@ ${h.secure_form(h.route_path('repo_create'), request=request)} $("#repo_group").val($(this).data('personalGroupId')).trigger("change"); }) + + $('#clone_uri').on('input paste', function(e) { + var url = $(this).val().trim(); + if (url) { + var parts = url.split('/').filter(Boolean); + var lastPart = parts[parts.length - 1]; + var repoName = lastPart.replace(/\.git$/, ""); + + $('#repo_name_input').val(repoName); + } + else { + $('#repo_name_input').val(""); + } + }) + }) ${h.end_form()} From cd01bbbddd8144faf9d44837a9264f73fe336e27 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Tue, 23 Dec 2025 21:04:15 +0100 Subject: [PATCH 3/5] feat(repo-creation): added svn bootstrap, and improved repo creation validation Also added refined UI, ability to pre-validate repo names fold: repo create fold repo creation foldz feat(repo-creation): added svn bootstrap, and improved repo creation validation Also added refined UI, ability to pre-validate repo names fold: repo create fold repo creation foldz fold --- requirements.txt | 2 +- rhodecode/api/tests/test_create_repo.py | 48 ++++- rhodecode/api/tests/test_create_repo_group.py | 65 +++++- rhodecode/api/views/repo_api.py | 21 +- rhodecode/apps/_base/__init__.py | 24 +++ .../apps/admin/tests/test_admin_repos.py | 18 +- rhodecode/apps/repository/__init__.py | 19 +- .../apps/repository/views/repo_checks.py | 116 ++++++++++- rhodecode/lib/base.py | 2 + rhodecode/lib/celerylib/loader.py | 2 +- rhodecode/model/forms.py | 1 - rhodecode/model/repo.py | 10 + .../schemas/repo_group_schema.py | 6 +- .../validation_schema/schemas/repo_schema.py | 19 +- .../model/validation_schema/validators.py | 24 ++- rhodecode/model/validators.py | 40 ++-- rhodecode/public/css/main.less | 52 +++++ rhodecode/public/js/rhodecode/routes.js | 2 + .../admin/repo_groups/repo_group_add.mako | 134 +++++++++--- .../templates/admin/repos/repo_add_base.mako | 193 ++++++++++-------- rhodecode/templates/base/base.mako | 28 ++- rhodecode/templates/debug_style/icons.html | 2 +- rhodecode/tests/other/test_validators.py | 28 ++- 23 files changed, 656 insertions(+), 200 deletions(-) diff --git a/requirements.txt b/requirements.txt index b8ec9dcb..506a2665 100644 --- a/requirements.txt +++ b/requirements.txt @@ -85,7 +85,7 @@ dogpile.cache==1.5.0 decorator==5.1.1 stevedore==5.1.0 pbr==5.11.1 -formencode==2.1.0 +formencode==2.1.1 six==1.17.0 fsspec==2025.10.0 gunicorn==23.0.0 diff --git a/rhodecode/api/tests/test_create_repo.py b/rhodecode/api/tests/test_create_repo.py index efd28955..8eca846e 100644 --- a/rhodecode/api/tests/test_create_repo.py +++ b/rhodecode/api/tests/test_create_repo.py @@ -319,7 +319,7 @@ class TestCreateRepo(object): assert repo is not None expected = { - "msg": "Created new repository `%s`" % (expected_name,), + "msg": f"Created new repository `{expected_name}`", "success": True, "task": None, } @@ -327,3 +327,49 @@ class TestCreateRepo(object): fixture.destroy_repo(expected_name) if parent_group: fixture.destroy_repo_group(parent_group) + + @pytest.mark.parametrize( + "given, expected_exc", + [ + ("robots.txt", True), + ("favicon.ico", True), + ], + ) + def test_api_create_repo_forbidden_names(self, backend, given, expected_exc): + repo_name = given + id_, params = build_data( + self.apikey, + "create_repo", + repo_name=repo_name, + owner=TEST_USER_ADMIN_LOGIN, + repo_type=backend.alias, + ) + response = api_call(self.app, params) + expected = {"repo_name": f"Repository with name `{given}` is forbidden"} + assert_error(id_, expected, given=response.body) + + @pytest.mark.parametrize( + "given, expected_exc", + [ + ("_admin", True), + ("_static", True), + ("_file_store", True), + ("_repos", True), + ("_goto_data", True), + ("_file_preview", True), + ], + ) + def test_api_create_repo_forbidden_names_and_wrong_name(self, backend, given, expected_exc): + repo_name = given + id_, params = build_data( + self.apikey, + "create_repo", + repo_name=repo_name, + owner=TEST_USER_ADMIN_LOGIN, + repo_type=backend.alias, + ) + response = api_call(self.app, params) + expected = { + "repo_name": f"Name must start with a letter or number. Got `{given}`; Repository with name `{given}` is forbidden" + } + assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/tests/test_create_repo_group.py b/rhodecode/api/tests/test_create_repo_group.py index 043d0f70..5aef3db6 100644 --- a/rhodecode/api/tests/test_create_repo_group.py +++ b/rhodecode/api/tests/test_create_repo_group.py @@ -48,7 +48,7 @@ class TestCreateRepoGroup(object): repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name) assert repo_group is not None - ret = {"msg": "Created new repo group `%s`" % (repo_group_name,), "repo_group": repo_group.get_api_data()} + ret = {"msg": f"Created new repo group `{repo_group_name}`", "repo_group": repo_group.get_api_data()} expected = ret try: assert_ok(id_, expected, given=response.body) @@ -75,7 +75,7 @@ class TestCreateRepoGroup(object): repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name) assert repo_group is not None - ret = {"msg": "Created new repo group `%s`" % (full_repo_group_name,), "repo_group": repo_group.get_api_data()} + ret = {"msg": f"Created new repo group `{full_repo_group_name}`", "repo_group": repo_group.get_api_data()} expected = ret try: assert_ok(id_, expected, given=response.body) @@ -115,7 +115,7 @@ class TestCreateRepoGroup(object): owner=TEST_USER_ADMIN_LOGIN, ) response = api_call(self.app, params) - expected = {"unique_repo_group_name": "Repository group with name `{}` already exists".format(repo_group_name)} + expected = {"unique_repo_group_name": f"Repository group with name `{repo_group_name}` already exists"} try: assert_error(id_, expected, given=response.body) finally: @@ -142,7 +142,7 @@ class TestCreateRepoGroup(object): repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name) assert repo_group is not None - expected = {"msg": "Created new repo group `%s`" % (repo_group_name,), "repo_group": repo_group.get_api_data()} + expected = {"msg": f"Created new repo group `{repo_group_name}`", "repo_group": repo_group.get_api_data()} try: assert_ok(id_, expected, given=response.body) finally: @@ -174,7 +174,7 @@ class TestCreateRepoGroup(object): repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name) assert repo_group is not None expected = { - "msg": "Created new repo group `{}`".format(full_repo_group_name), + "msg": f"Created new repo group `{full_repo_group_name}`", "repo_group": repo_group.get_api_data(), } try: @@ -206,8 +206,7 @@ class TestCreateRepoGroup(object): response = api_call(self.app, params) expected = { - "repo_group": "You do not have the permissions to store " - "repository groups inside repository group `{}`".format(repo_group_name) + "repo_group": f"You do not have the permissions to store repository groups inside repository group `{repo_group_name}`" } try: assert_error(id_, expected, given=response.body) @@ -242,13 +241,57 @@ class TestCreateRepoGroup(object): owner=TEST_USER_ADMIN_LOGIN, ) response = api_call(self.app, params) - expected = "failed to create repo group `%s`" % (repo_group_name,) + expected = f"failed to create repo group `{repo_group_name}`" + assert_error(id_, expected, given=response.body) + + @pytest.mark.parametrize( + "given, expected_exc", + [ + ("_admin", False), + ("_static", False), + ("_file_store", False), + ("_repos", False), + ("_goto_data", False), + ("_file_preview", False), + ], + ) + def test_api_create_repo_group_forbidden_and_wrong_names(self, backend, given, expected_exc): + repo_group_name = given + id_, params = build_data( + self.apikey, + "create_repo_group", + group_name=repo_group_name, + owner=TEST_USER_ADMIN_LOGIN, + ) + response = api_call(self.app, params) + expected = { + "repo_group_name": f"Name must start with a letter or number. Got `{repo_group_name}`; Repository group with name `{repo_group_name}` is forbidden" + } + assert_error(id_, expected, given=response.body) + + @pytest.mark.parametrize( + "given, expected_exc", + [ + ("favicon.ico", False), + ("robots.txt", False), + ], + ) + def test_api_create_repo_group_forbidden_names(self, backend, given, expected_exc): + repo_group_name = given + id_, params = build_data( + self.apikey, + "create_repo_group", + group_name=repo_group_name, + owner=TEST_USER_ADMIN_LOGIN, + ) + response = api_call(self.app, params) + expected = {"repo_group_name": f"Repository group with name `{repo_group_name}` is forbidden"} assert_error(id_, expected, given=response.body) def test_create_group_with_extra_slashes_in_name(self, user_util): existing_repo_group = user_util.create_repo_group() - dirty_group_name = "//{}//group2//".format(existing_repo_group.group_name) - cleaned_group_name = "{}/group2".format(existing_repo_group.group_name) + dirty_group_name = f"//{existing_repo_group.group_name}//group2//" + cleaned_group_name = f"{existing_repo_group.group_name}/group2" id_, params = build_data( self.apikey, @@ -259,7 +302,7 @@ class TestCreateRepoGroup(object): response = api_call(self.app, params) repo_group = RepoGroupModel.cls.get_by_group_name(cleaned_group_name) expected = { - "msg": "Created new repo group `%s`" % (cleaned_group_name,), + "msg": f"Created new repo group `{cleaned_group_name}`", "repo_group": repo_group.get_api_data(), } assert_ok(id_, expected, given=response.body) diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py index 31d946b3..9301a301 100644 --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -777,6 +777,8 @@ def create_repo( enable_locking=Optional(False), enable_downloads=Optional(False), copy_permissions=Optional(False), + bootstrap_readme=Optional(False), + bootstrap_svn_branch_struct=Optional(False), ): """ Creates a repository. @@ -816,10 +818,12 @@ def create_repo( :type enable_downloads: bool :param enable_statistics: :type enable_statistics: bool - :param copy_permissions: Copy permission from group in which the - repository is being created. + :param copy_permissions: Copy permission from group in which the repository is being created. :type copy_permissions: bool - + :param bootstrap_readme: Add default readme for new repositories + :type bootstrap_readme: bool + :param bootstrap_svn_branch_struct: Add branches/tags structure for SVN repositories. + :type bootstrap_svn_branch_struct: bool Example output: @@ -840,7 +844,7 @@ def create_repo( id : result : null - error : { + error : { 'failed to create repository ``' } @@ -865,6 +869,11 @@ def create_repo( if isinstance(enable_downloads, Optional): enable_downloads = defs.get("repo_enable_downloads") + if isinstance(bootstrap_readme, Optional): + bootstrap_readme = Optional.extract(bootstrap_readme) + if isinstance(bootstrap_svn_branch_struct, Optional): + bootstrap_svn_branch_struct = Optional.extract(bootstrap_svn_branch_struct) + landing_ref, _label = ScmModel.backend_landing_ref(repo_type) ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate) ref_choices = list(set(ref_choices + [landing_ref])) @@ -894,6 +903,8 @@ def create_repo( repo_enable_statistics=enable_statistics, repo_enable_downloads=enable_downloads, repo_enable_locking=enable_locking, + repo_bootstrap_readme=bootstrap_readme, + repo_bootstrap_svn_branch_struct=bootstrap_svn_branch_struct, ) ) except validation_schema.Invalid as err: @@ -915,6 +926,8 @@ def create_repo( "enable_locking": schema_data["repo_enable_locking"], "enable_downloads": schema_data["repo_enable_downloads"], "repo_copy_permissions": schema_data["repo_copy_permissions"], + "repo_bootstrap_readme": schema_data["repo_bootstrap_readme"], + "repo_bootstrap_svn_branch_struct": schema_data["repo_bootstrap_svn_branch_struct"], } task = RepoModel().create(form_data=data, cur_user=owner.user_id) diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index ffdf7a61..40d2886d 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -49,6 +49,30 @@ log = logging.getLogger(__name__) ADMIN_PREFIX: str = "/_admin" STATIC_FILE_PREFIX: str = "/_static" +# These names shouldn't be allowed as repo names or group names +RESERVED_NAMES: list = [ + ADMIN_PREFIX.lstrip("/"), + STATIC_FILE_PREFIX.lstrip("/"), + "_admin", + "_channelstream", + "_file_preview", + "_file_store", + "_goto_data", + "_home_repo_groups", + "_home_repos", + "_hovercard", + "_markup_preview", + "_profile_user_group", + "_profiles", + "_repo_groups", + "_repos", + "_store_session_attr", + "_user_groups", + "_users", + "favicon.ico", + "robots.txt", +] + URL_NAME_REQUIREMENTS = { # group name can have a slash in them, but they must not end with a slash "group_name": r".*?[^/]", diff --git a/rhodecode/apps/admin/tests/test_admin_repos.py b/rhodecode/apps/admin/tests/test_admin_repos.py index 6b51a2e8..dca359ac 100644 --- a/rhodecode/apps/admin/tests/test_admin_repos.py +++ b/rhodecode/apps/admin/tests/test_admin_repos.py @@ -267,7 +267,7 @@ class TestAdminRepos(object): def test_create_in_group_inherit_permissions(self, autologin_user, backend, csrf_token): # create GROUP - group_name = "sometest_%s" % backend.alias + group_name = f"sometest_{backend.alias}" gr = RepoGroupModel().create(group_name=group_name, group_description="test", owner=TEST_USER_ADMIN_LOGIN) perm = Permission.get_by_key("repository.write") RepoGroupModel().grant_user_permission(gr, TEST_USER_REGULAR_LOGIN, perm) @@ -392,6 +392,22 @@ class TestAdminRepos(object): ) response.mustcontain("Repository name cannot end with .git") + def test_create_reserved_name_admin(self, autologin_user, backend, csrf_token): + # Test creating a repo with reserved prefix/name _admin + repo_name = "_admin" + description = "description for reserved name repo" + response = self.app.post( + route_path("repo_create"), + fixture._get_repo_create_params( + repo_private=False, + repo_name=repo_name, + repo_type=backend.alias, + repo_description=description, + csrf_token=csrf_token, + ), + ) + response.mustcontain(f"Repository name {repo_name} is disallowed") + def test_default_user_cannot_access_private_repo_in_a_group(self, autologin_user, user_util, backend): group = user_util.create_repo_group() diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py index 5bb15c71..3c06d6b8 100644 --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -15,7 +15,7 @@ # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -from rhodecode.apps._base import add_route_with_slash +from rhodecode.apps._base import add_route_with_slash, ADMIN_PREFIX def includeme(config): @@ -47,6 +47,23 @@ def includeme(config): from rhodecode.apps.repository.views.repo_summary import RepoSummaryView from rhodecode.apps.repository.views.repo_tags import RepoTagsView + config.add_route(name="repo_name_check", pattern=ADMIN_PREFIX + "/repo_name_check") + config.add_view( + RepoChecksView, + attr="repo_name_check", + route_name="repo_name_check", + request_method="POST", + renderer="json_ext", + ) + config.add_route(name="repo_group_name_check", pattern=ADMIN_PREFIX + "/repo_group_name_check") + config.add_view( + RepoChecksView, + attr="repo_group_name_check", + route_name="repo_group_name_check", + request_method="POST", + renderer="json_ext", + ) + # repo creating checks, special cases that aren't repo routes config.add_route(name="repo_creating", pattern="/{repo_name:.*?[^/]}/repo_creating") config.add_view( diff --git a/rhodecode/apps/repository/views/repo_checks.py b/rhodecode/apps/repository/views/repo_checks.py index 8a551fbd..07466e90 100644 --- a/rhodecode/apps/repository/views/repo_checks.py +++ b/rhodecode/apps/repository/views/repo_checks.py @@ -20,12 +20,16 @@ import logging from pyramid.httpexceptions import HTTPFound, HTTPNotFound +import rhodecode from rhodecode.apps._base import BaseAppView from rhodecode.lib import helpers as h from rhodecode.lib.auth import NotAnonymous, HasRepoPermissionAny -from rhodecode.model.db import Repository +from rhodecode.lib.str_utils import safe_int +from rhodecode.model import validation_schema +from rhodecode.model.db import Repository, RepoGroup from rhodecode.model.permission import PermissionModel from rhodecode.model.validation_schema.types import RepoNameType +from rhodecode.model.validation_schema.schemas import repo_schema, repo_group_schema log = logging.getLogger(__name__) @@ -35,6 +39,113 @@ class RepoChecksView(BaseAppView): c = self._get_local_tmpl_context() return c + @NotAnonymous() + def repo_name_check(self): + post_items = dict(self.request.POST.items()) + + _ = self.request.translate + + default_response = { + "result": { + "repo_name": "", + "check_error": False, + "errors": "", + "exists": False, + "slugify": False, + } + } + + # determine a full repo name + repo_name = post_items.get("repo_name") + repo_group_id = safe_int(post_items["repo_group"]) + if repo_group_id and repo_group_id != -1: + repo_group = RepoGroup.get(repo_group_id) + if repo_group: + group_path = repo_group.full_path + repo_name = group_path + RepoGroup.url_sep() + repo_name + + schema = repo_schema.RepoSchema().bind( + repo_type_options=rhodecode.BACKENDS.keys(), + ) + + try: + schema_data = schema.deserialize( + dict( + repo_name=repo_name, + repo_type=post_items.get("repo_type"), + repo_owner=self.request.user.username, + ) + ) + default_response["result"]["repo_name"] = schema_data["repo_name"] + if post_items.get("repo_name") != schema_data["repo_name"]: + default_response["result"]["slugify"] = True + except validation_schema.Invalid as err: + errors = err.asdict() + if errors.get("repo_name"): + default_response["result"]["check_error"] = True + default_response["result"]["errors"] = errors["repo_name"] + elif errors.get("unique_repo_name"): + default_response["result"]["exists"] = True + default_response["result"]["errors"] = errors["unique_repo_name"] + default_response["result"]["check_error"] = True + else: + default_response["result"]["check_error"] = True + default_response["result"]["error"] = f"Unknown error: {errors}" + + return default_response + + @NotAnonymous() + def repo_group_name_check(self): + post_items = dict(self.request.POST.items()) + + _ = self.request.translate + + default_response = { + "result": { + "repo_group_name": "", + "check_error": False, + "errors": "", + "exists": False, + "slugify": False, + } + } + + # determine a full repo name + repo_group_name = post_items.get("repo_group_name") + repo_group_id = safe_int(post_items["repo_group"]) + if repo_group_id and repo_group_id != -1: + repo_group = RepoGroup.get(repo_group_id) + if repo_group: + group_path = repo_group.full_path + repo_group_name = group_path + RepoGroup.url_sep() + repo_group_name + + schema = repo_group_schema.RepoGroupSchema().bind() + + try: + schema_data = schema.deserialize( + dict( + repo_group_name=repo_group_name, + repo_group_owner=self.request.user.username, + ) + ) + default_response["result"]["repo_group_name"] = schema_data["repo_group_name"] + if post_items.get("repo_group_name") != schema_data["repo_group_name"]: + default_response["result"]["slugify"] = True + except validation_schema.Invalid as err: + errors = err.asdict() + if errors.get("repo_group_name"): + default_response["result"]["check_error"] = True + default_response["result"]["errors"] = errors["repo_group_name"] + elif errors.get("unique_repo_group_name"): + default_response["result"]["exists"] = True + default_response["result"]["errors"] = errors["unique_repo_group_name"] + default_response["result"]["check_error"] = True + else: + default_response["result"]["check_error"] = True + default_response["result"]["error"] = f"Unknown error: {errors}" + + return default_response + @NotAnonymous() def repo_creating(self): c = self.load_default_context() @@ -76,9 +187,10 @@ class RepoChecksView(BaseAppView): log.debug("celery: checking result for task:%s", task_id) task = celery_app.AsyncResult(task_id) try: - task.get(timeout=10) + task.get(timeout=3) except exceptions.TimeoutError: task = None + if task and task.failed(): msg = self._log_creation_exception(task.result, repo_name) h.flash(msg, category="error") diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py index 839b22fb..58416111 100644 --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -411,6 +411,8 @@ def attach_context_attributes(context, request, user_id=None, is_api=None): context.csrf_token = csrf_token context.backends = list(rhodecode.BACKENDS.keys()) + context.default_backend = rc_config.get("rhodecode_default_repo_type") + unread_count = 0 user_bookmark_list = [] if user_id: diff --git a/rhodecode/lib/celerylib/loader.py b/rhodecode/lib/celerylib/loader.py index a1289581..27237397 100644 --- a/rhodecode/lib/celerylib/loader.py +++ b/rhodecode/lib/celerylib/loader.py @@ -237,7 +237,7 @@ def task_failure_signal(task_id, exception, args, kwargs, traceback, einfo, **ka statsd = StatsdClient.statsd if statsd: exc_type = f"{einfo.__class__.__module__}.{einfo.__class__.__name__}" - statsd.incr("rhodecode_exception_total", tags=["exc_source:celery", "type:{}".format(exc_type)]) + statsd.incr("rhodecode_exception_total", tags=["exc_source:celery", f"type:{exc_type}"]) closer = celery_app.conf["PYRAMID_CLOSER"] if closer: diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index fa2abceb..d1d88bff 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -284,7 +284,6 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab repo_name = All( v.UnicodeString(strip=True, min=1, not_empty=True), v.SlugifyName(localizer), - v.CannotHaveGitSuffix(localizer), ) repo_group = All(v.CanWriteGroup(localizer, old_data), v.OneOf(repo_groups, hideList=True)) repo_type = v.OneOf(supported_backends, required=False, if_missing=old_data.get("repo_type")) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index 6c59e897..5fb7084c 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -69,6 +69,7 @@ from rhodecode.model.db import ( RepoGroup, RepositoryField, UserLog, + RhodeCodeUi, ) from rhodecode.model.permission import PermissionModel @@ -707,6 +708,15 @@ class RepoModel(BaseModel): nodes[safe_bytes(f"readme.{ext}")] = {"content": safe_bytes(readme_content)} + if svn_struct and db_repo.repo_type == "svn": + for branch in RhodeCodeUi.SVN_BRANCHES_PATTERNS: + branch_dir = branch.lstrip("/").rstrip("*").rstrip("/") + nodes[safe_bytes(os.path.join(branch_dir, ".dirkeep"))] = {"content": b""} + + for tag in RhodeCodeUi.SVN_TAGS_PATTERNS: + tag_dir = tag.lstrip("/").rstrip("*").rstrip("/") + nodes[safe_bytes(os.path.join(tag_dir, ".dirkeep"))] = {"content": b""} + if nodes: commit = ScmModel().create_nodes( user=db_user.user_id, diff --git a/rhodecode/model/validation_schema/schemas/repo_group_schema.py b/rhodecode/model/validation_schema/schemas/repo_group_schema.py index 4440b790..d849fdf5 100644 --- a/rhodecode/model/validation_schema/schemas/repo_group_schema.py +++ b/rhodecode/model/validation_schema/schemas/repo_group_schema.py @@ -150,7 +150,7 @@ def deferred_unique_name_validator(node, kw): @colander.deferred def deferred_repo_group_name_validator(node, kw): - return validators.valid_name_validator + return colander.All(validators.valid_name_validator, validators.forbidden_repo_group_name_validator) @colander.deferred @@ -170,7 +170,7 @@ class GroupType(colander.Mapping): try: return dict(repo_group_name=value) except Exception as e: - raise colander.Invalid(node, '"${val}" is not a mapping type: ${err}'.format(val=value, err=e)) + raise colander.Invalid(node, f'"${value}" is not a mapping type: ${e}') def deserialize(self, node, cstruct): if cstruct is colander.null: @@ -179,7 +179,7 @@ class GroupType(colander.Mapping): appstruct = super().deserialize(node, cstruct) validated_name = appstruct["repo_group_name"] - # inject group based on once deserialized data + # inject a group based on deserialized data (repo_group_name_without_group, parent_group_name, parent_group) = get_group_and_repo(validated_name) appstruct["repo_group_name_with_group"] = validated_name diff --git a/rhodecode/model/validation_schema/schemas/repo_schema.py b/rhodecode/model/validation_schema/schemas/repo_schema.py index 00edd9ee..8d5cbb6d 100644 --- a/rhodecode/model/validation_schema/schemas/repo_schema.py +++ b/rhodecode/model/validation_schema/schemas/repo_schema.py @@ -221,12 +221,9 @@ def deferred_unique_name_validator(node, kw): @colander.deferred def deferred_repo_name_validator(node, kw): - def no_git_suffix_validator(node, value): - if value.endswith(".git"): - msg = _("Repository name cannot end with .git") - raise colander.Invalid(node, msg) - - return colander.All(no_git_suffix_validator, validators.valid_name_validator) + return colander.All( + validators.no_git_suffix_validator, validators.valid_name_validator, validators.forbidden_repo_name_validator + ) @colander.deferred @@ -256,7 +253,7 @@ class GroupType(colander.Mapping): try: return dict(repo_group_name=value) except Exception as e: - raise colander.Invalid(node, '"${val}" is not a mapping type: ${err}'.format(val=value, err=e)) + raise colander.Invalid(node, f'"${value}" is not a mapping type: ${e}') def deserialize(self, node, cstruct): if cstruct is colander.null: @@ -341,6 +338,14 @@ class RepoSchema(colander.MappingSchema): types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget() ) + repo_bootstrap_readme = colander.SchemaNode( + types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget() + ) + + repo_bootstrap_svn_branch_struct = colander.SchemaNode( + types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget() + ) + def deserialize(self, cstruct): """ Custom deserialize that allows to chain validation, and verify diff --git a/rhodecode/model/validation_schema/validators.py b/rhodecode/model/validation_schema/validators.py index 421775ba..52084156 100644 --- a/rhodecode/model/validation_schema/validators.py +++ b/rhodecode/model/validation_schema/validators.py @@ -67,7 +67,29 @@ def valid_name_validator(node, value): return msg = _("Name must start with a letter or number. Got `{}`").format(value) - if not re.match(r"^[a-zA-z0-9]{1,}", value): + if not re.match(r"^[a-zA-Z0-9]+", value): + raise colander.Invalid(node, msg) + + +def no_git_suffix_validator(node, value): + if value.endswith(".git"): + msg = _("Repository name cannot end with .git") + raise colander.Invalid(node, msg) + + +def forbidden_repo_name_validator(node, value): + from rhodecode.apps._base import RESERVED_NAMES + + if value in RESERVED_NAMES: + msg = _("Repository with name `{}` is forbidden").format(value) + raise colander.Invalid(node, msg) + + +def forbidden_repo_group_name_validator(node, value): + from rhodecode.apps._base import RESERVED_NAMES + + if value in RESERVED_NAMES: + msg = _("Repository group with name `{}` is forbidden").format(value) raise colander.Invalid(node, msg) diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py index 97fddabb..fe029f81 100644 --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -49,7 +49,7 @@ from sqlalchemy.util import OrderedSet from rhodecode.authentication import legacy_plugin_prefix, _import_legacy_plugin from rhodecode.authentication.base import loadplugin -from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.apps._base import ADMIN_PREFIX, RESERVED_NAMES from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny from rhodecode.lib.utils import repo_name_slug, make_db_config from rhodecode.lib.utils2 import safe_int, str2bool, aslist @@ -280,11 +280,13 @@ def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=Fals class _validator(formencode.validators.FancyValidator): messages = { + "invalid_repo_group_name": _("Repository group name %(group_name)s is disallowed"), "group_parent_id": _("Cannot assign this group as parent"), "group_exists": _('Group "%(group_name)s" already exists'), "repo_exists": _('Repository with name "%(group_name)s" already exists'), - "permission_denied": _("no permission to store repository groupin this location"), + "permission_denied": _("no permission to store repository group in this location"), "permission_denied_root": _("no permission to store repository group in root location"), + "has_git_suffix": _("Repository group name cannot end with .git"), } def _convert_to_python(self, value, state): @@ -313,6 +315,14 @@ def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=Fals if group_parent_id == -1: group_parent_id = None + if group_name_full in RESERVED_NAMES: + msg = M(self, "invalid_repo_group_name", state, group_name=group_name) + raise formencode.Invalid(msg, value, state, error_dict={"group_name": msg}) + + if group_name_full.endswith(".git"): + msg = M(self, "has_git_suffix", state) + raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg}) + group_obj = RepoGroup.get(old_data.get("group_id")) parent_group_changed = False if edit: @@ -457,6 +467,7 @@ def ValidRepoName(localizer, edit=False, old_data=None): # inside a group "repository_in_group_exists": _('Repository with name %(repo)s exists in group "%(group)s"'), "group_in_group_exists": _('Repository group with name "%(repo)s" exists in group "%(group)s"'), + "has_git_suffix": _("Repository name cannot end with .git"), } def _convert_to_python(self, value, state): @@ -486,10 +497,14 @@ def ValidRepoName(localizer, edit=False, old_data=None): group_path = value.get("group_path") group_name = value.get("group_name") - if repo_name in [ADMIN_PREFIX, ""]: + if repo_name_full in RESERVED_NAMES + [""]: msg = M(self, "invalid_repo_name", state, repo=repo_name) raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg}) + if repo_name_full.endswith(".git"): + msg = M(self, "has_git_suffix", state) + raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg}) + rename = old_data.get("repo_name") != repo_name_full create = not edit if rename or create: @@ -532,25 +547,6 @@ def SlugifyName(localizer): return _validator -def CannotHaveGitSuffix(localizer): - _ = localizer - - class _validator(formencode.validators.FancyValidator): - messages = { - "has_git_suffix": _("Repository name cannot end with .git"), - } - - def _convert_to_python(self, value, state): - return value - - def _validate_python(self, value, state): - if value and value.endswith(".git"): - msg = M(self, "has_git_suffix", state) - raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg}) - - return _validator - - def ValidCloneUri(localizer): _ = localizer diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less index 5a3225a7..5957269a 100644 --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -3245,3 +3245,55 @@ details:not([open]) > :not(summary) { text-overflow: ellipsis; width: 130px; } + +.help-block-repo-create { + color: @grey4; +} + +.repo-header-row { + display: flex; + align-items: center; + gap: 4px; + margin: 10px 0; +} + +#copy_perms { + display: flex; + align-items: center; + margin: 15px 0; + gap: 6px; +} + +#checkbox-span { + align-items: center; + padding-top: 2px; +} + +.repo-create-spacer { + margin: 0 6px 0 2px; + font-weight: bold; + font-size: 16px; +} + +.full-repo-name-container { + display: flex; + align-items: baseline; +} + +.repo-check-container { + height: auto; + overflow: hidden; + padding-top: 5px; +} + +.repo-check-container.check-progress { + color: @grey4; +} + +.repo-check-container.check-success { + color: @color8; +} + +.repo-check-container.check-error { + color: @color5; +} \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 519bfcf1..263946e0 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -359,6 +359,7 @@ function registerRCRoutes() { pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']); pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']); pyroutes.register('repo_group_list_data', '/_repo_groups', []); + pyroutes.register('repo_group_name_check', '/_admin/repo_group_name_check', []); pyroutes.register('repo_group_new', '/_admin/repo_group/new', []); pyroutes.register('repo_groups', '/_admin/repo_groups', []); pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []); @@ -368,6 +369,7 @@ function registerRCRoutes() { pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']); pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']); pyroutes.register('repo_list_data', '/_repos', []); + pyroutes.register('repo_name_check', '/_admin/repo_name_check', []); pyroutes.register('repo_new', '/_admin/repos/new', []); pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']); pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']); diff --git a/rhodecode/templates/admin/repo_groups/repo_group_add.mako b/rhodecode/templates/admin/repo_groups/repo_group_add.mako index ff953af5..066b10ca 100644 --- a/rhodecode/templates/admin/repo_groups/repo_group_add.mako +++ b/rhodecode/templates/admin/repo_groups/repo_group_add.mako @@ -24,6 +24,7 @@ <%def name="main()"> +
${h.secure_form(h.route_path('repo_group_create'), request=request)}
@@ -34,24 +35,40 @@
- ${h.text('group_name', class_="medium")} +
+ % if c.personal_repo_group: + + + + % endif + ${h.select('group_parent_id', request.GET.get('parent_group'), c.repo_groups,class_="medium")} +
/
+
+ ${h.text('group_name', class_="medium")} + +
+
+
+ + ${_('Parent Repository Group / Repository Group Name')}. + +
+ + ## COPY PERMS +
+
+ ${h.checkbox('group_copy_permissions', value="True", checked="checked")} +
+ +
+ ${_('Copy permissions from parent repository group.')} +
+
+
-
-
- -
-
- ${h.select('group_parent_id', request.GET.get('parent_group'),c.repo_groups,class_="medium")} - % if c.personal_repo_group: - - ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)} - - % endif -
-
-
@@ -73,16 +90,6 @@
-
-
- -
-
- ${h.checkbox('group_copy_permissions', value="True", checked="checked")} - ${_('Copy permissions from parent repository group.')} -
-
-
${h.submit('save',_('Create Repository Group'),class_="btn")}
@@ -91,31 +98,90 @@ ${h.end_form()} diff --git a/rhodecode/templates/admin/repos/repo_add_base.mako b/rhodecode/templates/admin/repos/repo_add_base.mako index fefa90c6..7b46e8cd 100644 --- a/rhodecode/templates/admin/repos/repo_add_base.mako +++ b/rhodecode/templates/admin/repos/repo_add_base.mako @@ -7,63 +7,22 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
- -
- - ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")} - / - ${h.text('repo_name', class_="medium", id="repo_name_input")} - - % if c.personal_repo_group: - - ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)} - - % endif +
+ % if c.personal_repo_group: + + + + % endif + ${h.select('repo_group', request.GET.get('parent_group'), c.repo_groups, class_="medium")} +
/
+
+ ${h.text('repo_name', class_="medium", id="repo_name_input")} + +
+
@@ -85,7 +44,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
- ## TODO: what is this ? i don't think we use it anymore %if not c.rhodecode_user.is_admin: ${h.hidden('user_created',True)} %endif @@ -118,9 +76,9 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
- % for backend in c.backends: - % if loop.index == 0: + + % if backend == c.default_backend: % else: @@ -168,7 +126,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
-
+
@@ -178,7 +136,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
-
+