From ee52770fff0da2981f97c9bc3edb401231fdc781 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Mon, 22 Dec 2025 14:08:02 +0100 Subject: [PATCH] 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()}