feature(repo): added ability to bootstrap repositories on creation

This commit is contained in:
RhodeCode Admin 2025-12-22 14:08:02 +01:00
parent 6f780dccb1
commit ee52770fff
4 changed files with 176 additions and 30 deletions

View file

@ -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},

View file

@ -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))

View file

@ -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
):

View file

@ -5,16 +5,91 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<div class="fields">
<div class="field">
<div class="label">
<label for="repo_name">${_('Repository name')}:</label>
<label for="repo_name">${_('Repository')}:</label>
</div>
<style>
.help-block-repo-create {
color: #949494;
}
.repo-header-row {
display: flex; /* Modern way to align elements inline */
align-items: center; /* Keeps text perfectly leveled vertically */
gap: 4px; /* Space between the label and the link */
margin: 10px 0; /* Space below the entire row */
}
#copy_perms {
/* Layout & Alignment */
display: flex;
align-items: center; /* Aligns checkbox with the first line of text */
/* Spacing for the whole row */
margin: 15px 0; /* Space above and below the row */
/* Internal Spacing */
gap: 6px; /* Space specifically between checkbox and text */
}
#checkbox-span {
align-items: center; /* Aligns checkbox with the first line of text */
padding-top: 2px; /* Micro-adjustment to center checkbox with text line */
}
.help-block-inline {
font-size: 14px; /* Standard readable size */
line-height: 1.4; /* Improves readability for multi-line text */
color: #333; /* Professional dark grey */
}
.repo-create-spacer {
margin: 0 4px;
font-weight: bold;
font-size: 16px;
}
</style>
<div class="input">
${h.text('repo_name', class_="medium")}
<div class="info-block">
<a id="remote_clone_toggle" href="#">${_('Import Existing Repository ?')}</a>
${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
<span class="repo-create-spacer"> / </span>
${h.text('repo_name', class_="medium", id="repo_name_input")}
% if c.personal_repo_group:
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
</a>
% endif
<div class="repo-header-row">
<span class="help-block-repo-create">
${_('Repository Group / Repository Name')}.
</span>
<div class="import-repo">
<a id="remote_clone_toggle" href="#">${_('Import an existing repository.')}</a>
</div>
</div>
## COPY PERMS
<div id="copy_perms">
<div id="checkbox-span">
${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
</div>
<div class="help-block-inline">
${_('Copy permissions from parent repository group.')}
</div>
</div>
## 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
</div>
</div>
<div id="remote_clone" class="field" style="display: none;">
@ -37,20 +112,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</span>
</div>
</div>
<div class="field">
<div class="label">
<label for="repo_group">${_('Repository group')}:</label>
</div>
<div class="select">
${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
% if c.personal_repo_group:
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
</a>
% endif
<span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
</div>
</div>
<div class="field">
<div class="label">
@ -58,7 +119,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</div>
<div class="fields repo-type-radio">
% for backend in c.backends:
% if loop.index == 0:
<input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" checked="checked"/>
@ -73,7 +133,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
% endfor
<span class="help-block">${_('Set the type of repository to create.')}</span>
</div>
</div>
@ -98,15 +157,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</span>
</div>
</div>
<div id="copy_perms" class="field">
<div class="label label-checkbox">
<label for="repo_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
<span class="help-block">${_('Copy permissions from parent repository group.')}</span>
</div>
</div>
<div class="field">
<div class="label label-checkbox">
<label for="repo_private">${_('Private Repository')}:</label>
@ -116,6 +167,28 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
</div>
</div>
<div class="field repo-bootstrap-items">
<div class="label label-checkbox">
<label for="repo_bootstrap_readme">${_('Add Readme')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('repo_bootstrap_readme',value="True")}
<span class="help-block">${_('Initialize repository with a README. Allows you to immediately clone this project’s repository.')}</span>
</div>
</div>
<div class="field repo-bootstrap-items">
<div class="label label-checkbox">
<label for="repo_bootstrap_svn_branch_struct">${_('Add SVN structure')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('repo_bootstrap_svn_branch_struct',value="True")}
<span class="help-block">${_('Initialize repository with a branches/tags DIRS')}</span>
</div>
</div>
<div class="buttons">
${h.submit('save',_('Create Repository'),class_="btn")}
</div>
@ -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("");
}
})
})
</script>
${h.end_form()}