Merge pull request !2934 from rhodecode-enterprise-ce feature/svn-bootstrap

Changes from branch: Feature/svn bootstrap
This commit is contained in:
Andrii Verbytskyi 2025-12-29 13:30:09 +00:00
commit 543294ee86
26 changed files with 775 additions and 170 deletions

View file

@ -85,7 +85,7 @@ dogpile.cache==1.5.0
decorator==5.1.1 decorator==5.1.1
stevedore==5.1.0 stevedore==5.1.0
pbr==5.11.1 pbr==5.11.1
formencode==2.1.0 formencode==2.1.1
six==1.17.0 six==1.17.0
fsspec==2025.10.0 fsspec==2025.10.0
gunicorn==23.0.0 gunicorn==23.0.0

View file

@ -319,7 +319,7 @@ class TestCreateRepo(object):
assert repo is not None assert repo is not None
expected = { expected = {
"msg": "Created new repository `%s`" % (expected_name,), "msg": f"Created new repository `{expected_name}`",
"success": True, "success": True,
"task": None, "task": None,
} }
@ -327,3 +327,49 @@ class TestCreateRepo(object):
fixture.destroy_repo(expected_name) fixture.destroy_repo(expected_name)
if parent_group: if parent_group:
fixture.destroy_repo_group(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)

View file

@ -48,7 +48,7 @@ class TestCreateRepoGroup(object):
repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name) repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name)
assert repo_group is not None 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 expected = ret
try: try:
assert_ok(id_, expected, given=response.body) 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) repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name)
assert repo_group is not None 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 expected = ret
try: try:
assert_ok(id_, expected, given=response.body) assert_ok(id_, expected, given=response.body)
@ -115,7 +115,7 @@ class TestCreateRepoGroup(object):
owner=TEST_USER_ADMIN_LOGIN, owner=TEST_USER_ADMIN_LOGIN,
) )
response = api_call(self.app, params) 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: try:
assert_error(id_, expected, given=response.body) assert_error(id_, expected, given=response.body)
finally: finally:
@ -142,7 +142,7 @@ class TestCreateRepoGroup(object):
repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name) repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name)
assert repo_group is not None 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: try:
assert_ok(id_, expected, given=response.body) assert_ok(id_, expected, given=response.body)
finally: finally:
@ -174,7 +174,7 @@ class TestCreateRepoGroup(object):
repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name) repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name)
assert repo_group is not None assert repo_group is not None
expected = { 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(), "repo_group": repo_group.get_api_data(),
} }
try: try:
@ -206,8 +206,7 @@ class TestCreateRepoGroup(object):
response = api_call(self.app, params) response = api_call(self.app, params)
expected = { expected = {
"repo_group": "You do not have the permissions to store " "repo_group": f"You do not have the permissions to store repository groups inside repository group `{repo_group_name}`"
"repository groups inside repository group `{}`".format(repo_group_name)
} }
try: try:
assert_error(id_, expected, given=response.body) assert_error(id_, expected, given=response.body)
@ -242,13 +241,57 @@ class TestCreateRepoGroup(object):
owner=TEST_USER_ADMIN_LOGIN, owner=TEST_USER_ADMIN_LOGIN,
) )
response = api_call(self.app, params) 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) assert_error(id_, expected, given=response.body)
def test_create_group_with_extra_slashes_in_name(self, user_util): def test_create_group_with_extra_slashes_in_name(self, user_util):
existing_repo_group = user_util.create_repo_group() existing_repo_group = user_util.create_repo_group()
dirty_group_name = "//{}//group2//".format(existing_repo_group.group_name) dirty_group_name = f"//{existing_repo_group.group_name}//group2//"
cleaned_group_name = "{}/group2".format(existing_repo_group.group_name) cleaned_group_name = f"{existing_repo_group.group_name}/group2"
id_, params = build_data( id_, params = build_data(
self.apikey, self.apikey,
@ -259,7 +302,7 @@ class TestCreateRepoGroup(object):
response = api_call(self.app, params) response = api_call(self.app, params)
repo_group = RepoGroupModel.cls.get_by_group_name(cleaned_group_name) repo_group = RepoGroupModel.cls.get_by_group_name(cleaned_group_name)
expected = { 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(), "repo_group": repo_group.get_api_data(),
} }
assert_ok(id_, expected, given=response.body) assert_ok(id_, expected, given=response.body)

View file

@ -777,6 +777,8 @@ def create_repo(
enable_locking=Optional(False), enable_locking=Optional(False),
enable_downloads=Optional(False), enable_downloads=Optional(False),
copy_permissions=Optional(False), copy_permissions=Optional(False),
bootstrap_readme=Optional(False),
bootstrap_svn_branch_struct=Optional(False),
): ):
""" """
Creates a repository. Creates a repository.
@ -816,10 +818,12 @@ def create_repo(
:type enable_downloads: bool :type enable_downloads: bool
:param enable_statistics: :param enable_statistics:
:type enable_statistics: bool :type enable_statistics: bool
:param copy_permissions: Copy permission from group in which the :param copy_permissions: Copy permission from group in which the repository is being created.
repository is being created.
:type copy_permissions: bool :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: Example output:
@ -840,7 +844,7 @@ def create_repo(
id : <id_given_in_input> id : <id_given_in_input>
result : null result : null
error : { error : {
'failed to create repository `<repo_name>`' 'failed to create repository `<repo_name>`'
} }
@ -865,6 +869,11 @@ def create_repo(
if isinstance(enable_downloads, Optional): if isinstance(enable_downloads, Optional):
enable_downloads = defs.get("repo_enable_downloads") 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) landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate) ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
ref_choices = list(set(ref_choices + [landing_ref])) ref_choices = list(set(ref_choices + [landing_ref]))
@ -894,6 +903,8 @@ def create_repo(
repo_enable_statistics=enable_statistics, repo_enable_statistics=enable_statistics,
repo_enable_downloads=enable_downloads, repo_enable_downloads=enable_downloads,
repo_enable_locking=enable_locking, 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: except validation_schema.Invalid as err:
@ -915,6 +926,8 @@ def create_repo(
"enable_locking": schema_data["repo_enable_locking"], "enable_locking": schema_data["repo_enable_locking"],
"enable_downloads": schema_data["repo_enable_downloads"], "enable_downloads": schema_data["repo_enable_downloads"],
"repo_copy_permissions": schema_data["repo_copy_permissions"], "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) task = RepoModel().create(form_data=data, cur_user=owner.user_id)

View file

@ -49,6 +49,30 @@ log = logging.getLogger(__name__)
ADMIN_PREFIX: str = "/_admin" ADMIN_PREFIX: str = "/_admin"
STATIC_FILE_PREFIX: str = "/_static" 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 = { URL_NAME_REQUIREMENTS = {
# group name can have a slash in them, but they must not end with a slash # group name can have a slash in them, but they must not end with a slash
"group_name": r".*?[^/]", "group_name": r".*?[^/]",

View file

@ -267,7 +267,7 @@ class TestAdminRepos(object):
def test_create_in_group_inherit_permissions(self, autologin_user, backend, csrf_token): def test_create_in_group_inherit_permissions(self, autologin_user, backend, csrf_token):
# create GROUP # 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) gr = RepoGroupModel().create(group_name=group_name, group_description="test", owner=TEST_USER_ADMIN_LOGIN)
perm = Permission.get_by_key("repository.write") perm = Permission.get_by_key("repository.write")
RepoGroupModel().grant_user_permission(gr, TEST_USER_REGULAR_LOGIN, perm) 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") 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): def test_default_user_cannot_access_private_repo_in_a_group(self, autologin_user, user_util, backend):
group = user_util.create_repo_group() group = user_util.create_repo_group()

View file

@ -67,6 +67,7 @@ class AdminDefaultSettingsView(BaseAppView):
Session().add(setting) Session().add(setting)
Session().commit() Session().commit()
h.flash(_("Default settings updated successfully"), category="success") h.flash(_("Default settings updated successfully"), category="success")
SettingsModel().invalidate_settings_cache()
except formencode.Invalid as errors: except formencode.Invalid as errors:
data = render( data = render(

View file

@ -15,7 +15,7 @@
# This program is dual-licensed. If you wish to learn more about the # This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services, # RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/ # 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): 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_summary import RepoSummaryView
from rhodecode.apps.repository.views.repo_tags import RepoTagsView 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 # repo creating checks, special cases that aren't repo routes
config.add_route(name="repo_creating", pattern="/{repo_name:.*?[^/]}/repo_creating") config.add_route(name="repo_creating", pattern="/{repo_name:.*?[^/]}/repo_creating")
config.add_view( config.add_view(

View file

@ -20,12 +20,16 @@ import logging
from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.httpexceptions import HTTPFound, HTTPNotFound
import rhodecode
from rhodecode.apps._base import BaseAppView from rhodecode.apps._base import BaseAppView
from rhodecode.lib import helpers as h from rhodecode.lib import helpers as h
from rhodecode.lib.auth import NotAnonymous, HasRepoPermissionAny 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.permission import PermissionModel
from rhodecode.model.validation_schema.types import RepoNameType from rhodecode.model.validation_schema.types import RepoNameType
from rhodecode.model.validation_schema.schemas import repo_schema, repo_group_schema
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -35,6 +39,113 @@ class RepoChecksView(BaseAppView):
c = self._get_local_tmpl_context() c = self._get_local_tmpl_context()
return c 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() @NotAnonymous()
def repo_creating(self): def repo_creating(self):
c = self.load_default_context() c = self.load_default_context()
@ -76,9 +187,10 @@ class RepoChecksView(BaseAppView):
log.debug("celery: checking result for task:%s", task_id) log.debug("celery: checking result for task:%s", task_id)
task = celery_app.AsyncResult(task_id) task = celery_app.AsyncResult(task_id)
try: try:
task.get(timeout=10) task.get(timeout=3)
except exceptions.TimeoutError: except exceptions.TimeoutError:
task = None task = None
if task and task.failed(): if task and task.failed():
msg = self._log_creation_exception(task.result, repo_name) msg = self._log_creation_exception(task.result, repo_name)
h.flash(msg, category="error") h.flash(msg, category="error")

View file

@ -411,6 +411,8 @@ def attach_context_attributes(context, request, user_id=None, is_api=None):
context.csrf_token = csrf_token context.csrf_token = csrf_token
context.backends = list(rhodecode.BACKENDS.keys()) context.backends = list(rhodecode.BACKENDS.keys())
context.default_backend = rc_config.get("rhodecode_default_repo_type")
unread_count = 0 unread_count = 0
user_bookmark_list = [] user_bookmark_list = []
if user_id: if user_id:

View file

@ -237,7 +237,7 @@ def task_failure_signal(task_id, exception, args, kwargs, traceback, einfo, **ka
statsd = StatsdClient.statsd statsd = StatsdClient.statsd
if statsd: if statsd:
exc_type = f"{einfo.__class__.__module__}.{einfo.__class__.__name__}" 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"] closer = celery_app.conf["PYRAMID_CLOSER"]
if closer: if closer:

View file

@ -180,7 +180,7 @@ def get_mailer(transformed_email_conf: dict[str, Any], original_email_conf: dict
return Mailer(**transformed_email_conf) return Mailer(**transformed_email_conf)
@async_task(ignore_result=True, base=RequestContextAndUniqueByPayloadTask) @async_task(ignore_result=False, base=RequestContextAndUniqueByPayloadTask)
def create_repo(form_data, cur_user): def create_repo(form_data, cur_user):
from rhodecode.model.repo import RepoModel from rhodecode.model.repo import RepoModel
from rhodecode.model.user import UserModel from rhodecode.model.user import UserModel
@ -204,6 +204,9 @@ def create_repo(form_data, cur_user):
fork_of = form_data.get("fork_parent_id") fork_of = form_data.get("fork_parent_id")
state = form_data.get("repo_state", Repository.STATE_PENDING) 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 # repo creation defaults, private and repo_type are filled in form
defs = SettingsModel().get_default_repo_settings(strip_prefix=True) defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
enable_statistics = form_data.get("enable_statistics", defs.get("repo_enable_statistics")) 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_id = repo.repo_id
repo_data = repo.get_api_data() 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( audit_logger.store(
"repo.create", "repo.create",
action_data={"data": repo_data}, action_data={"data": repo_data},
@ -289,7 +300,7 @@ def create_repo(form_data, cur_user):
return True return True
@async_task(ignore_result=True, base=RequestContextAndUniqueByPayloadTask) @async_task(ignore_result=False, base=RequestContextAndUniqueByPayloadTask)
def create_repo_fork(form_data, cur_user): def create_repo_fork(form_data, cur_user):
""" """
Creates a fork of repository using internal VCS methods Creates a fork of repository using internal VCS methods

View file

@ -269,13 +269,15 @@ class DiffProcessor(object):
end += 1 end += 1
if start or end: if start or end:
def do(l): def do(_line):
last = end + len(l["line"]) last = end + len(_line["line"])
if l["action"] == Action.ADD: if _line["action"] == Action.ADD:
tag = "ins" tag = "ins"
else: else:
tag = "del" tag = "del"
l["line"] = f"{l['line'][:start]}<{tag}>{l['line'][start:last]}</{tag}>{l['line'][last:]}" _line["line"] = (
f"{_line['line'][:start]}<{tag}>{_line['line'][start:last]}</{tag}>{_line['line'][last:]}"
)
do(line) do(line)
do(next_) do(next_)
@ -1045,8 +1047,8 @@ class DiffLimitExceeded(Exception):
# NOTE(marcink): if diffs.mako change, probably this # NOTE(marcink): if diffs.mako change, probably this
# needs a bump to next version # needs a bump to the next version
CURRENT_DIFF_VERSION = "v5" CURRENT_DIFF_VERSION = "v6"
def _cleanup_cache_file(cached_diff_file): def _cleanup_cache_file(cached_diff_file):

View file

@ -284,7 +284,6 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab
repo_name = All( repo_name = All(
v.UnicodeString(strip=True, min=1, not_empty=True), v.UnicodeString(strip=True, min=1, not_empty=True),
v.SlugifyName(localizer), v.SlugifyName(localizer),
v.CannotHaveGitSuffix(localizer),
) )
repo_group = All(v.CanWriteGroup(localizer, old_data), v.OneOf(repo_groups, hideList=True)) 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")) repo_type = v.OneOf(supported_backends, required=False, if_missing=old_data.get("repo_type"))
@ -297,6 +296,9 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab
repo_enable_downloads = v.StringBoolean(if_missing=False) repo_enable_downloads = v.StringBoolean(if_missing=False)
repo_enable_locking = 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: if edit:
# this is repo owner # this is repo owner
user = All(v.UnicodeString(not_empty=True), v.ValidRepoUser(localizer, allow_disabled)) 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 enum import StrEnum, auto
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from zope.cachedescriptors.property import Lazy as LazyProperty
from rhodecode import events from rhodecode import events
from rhodecode.lib.auth import HasUserGroupPermissionAny from rhodecode.lib.auth import HasUserGroupPermissionAny
@ -47,6 +46,7 @@ from rhodecode.lib.utils2 import (
action_logger_generic, action_logger_generic,
) )
from rhodecode.lib.vcs.backends import get_backend 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.lib.vcs.nodes import NodeKind
from rhodecode.model import BaseModel from rhodecode.model import BaseModel
from rhodecode.model.db import ( from rhodecode.model.db import (
@ -69,6 +69,7 @@ from rhodecode.model.db import (
RepoGroup, RepoGroup,
RepositoryField, RepositoryField,
UserLog, UserLog,
RhodeCodeUi,
) )
from rhodecode.model.permission import PermissionModel from rhodecode.model.permission import PermissionModel
@ -677,6 +678,57 @@ class RepoModel(BaseModel):
return run_task(tasks.create_repo, form_data, cur_user) 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 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,
repo=db_repo,
message=message,
nodes=nodes,
parent_commit=commit,
author=author,
)
return commit
return None
def update_permissions( def update_permissions(
self, repo, perm_additions=None, perm_updates=None, perm_deletions=None, check_perms=True, cur_user=None self, repo, perm_additions=None, perm_updates=None, perm_deletions=None, check_perms=True, cur_user=None
): ):

View file

@ -150,7 +150,7 @@ def deferred_unique_name_validator(node, kw):
@colander.deferred @colander.deferred
def deferred_repo_group_name_validator(node, kw): 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 @colander.deferred
@ -170,7 +170,7 @@ class GroupType(colander.Mapping):
try: try:
return dict(repo_group_name=value) return dict(repo_group_name=value)
except Exception as e: 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): def deserialize(self, node, cstruct):
if cstruct is colander.null: if cstruct is colander.null:
@ -179,7 +179,7 @@ class GroupType(colander.Mapping):
appstruct = super().deserialize(node, cstruct) appstruct = super().deserialize(node, cstruct)
validated_name = appstruct["repo_group_name"] 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) (repo_group_name_without_group, parent_group_name, parent_group) = get_group_and_repo(validated_name)
appstruct["repo_group_name_with_group"] = validated_name appstruct["repo_group_name_with_group"] = validated_name

View file

@ -221,12 +221,9 @@ def deferred_unique_name_validator(node, kw):
@colander.deferred @colander.deferred
def deferred_repo_name_validator(node, kw): def deferred_repo_name_validator(node, kw):
def no_git_suffix_validator(node, value): return colander.All(
if value.endswith(".git"): validators.no_git_suffix_validator, validators.valid_name_validator, validators.forbidden_repo_name_validator
msg = _("Repository name cannot end with .git") )
raise colander.Invalid(node, msg)
return colander.All(no_git_suffix_validator, validators.valid_name_validator)
@colander.deferred @colander.deferred
@ -256,7 +253,7 @@ class GroupType(colander.Mapping):
try: try:
return dict(repo_group_name=value) return dict(repo_group_name=value)
except Exception as e: 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): def deserialize(self, node, cstruct):
if cstruct is colander.null: if cstruct is colander.null:
@ -341,6 +338,14 @@ class RepoSchema(colander.MappingSchema):
types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget() 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): def deserialize(self, cstruct):
""" """
Custom deserialize that allows to chain validation, and verify Custom deserialize that allows to chain validation, and verify

View file

@ -67,7 +67,29 @@ def valid_name_validator(node, value):
return return
msg = _("Name must start with a letter or number. Got `{}`").format(value) 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) raise colander.Invalid(node, msg)

View file

@ -49,7 +49,7 @@ from sqlalchemy.util import OrderedSet
from rhodecode.authentication import legacy_plugin_prefix, _import_legacy_plugin from rhodecode.authentication import legacy_plugin_prefix, _import_legacy_plugin
from rhodecode.authentication.base import loadplugin 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.auth import HasRepoGroupPermissionAny, HasPermissionAny
from rhodecode.lib.utils import repo_name_slug, make_db_config from rhodecode.lib.utils import repo_name_slug, make_db_config
from rhodecode.lib.utils2 import safe_int, str2bool, aslist 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): class _validator(formencode.validators.FancyValidator):
messages = { messages = {
"invalid_repo_group_name": _("Repository group name %(group_name)s is disallowed"),
"group_parent_id": _("Cannot assign this group as parent"), "group_parent_id": _("Cannot assign this group as parent"),
"group_exists": _('Group "%(group_name)s" already exists'), "group_exists": _('Group "%(group_name)s" already exists'),
"repo_exists": _('Repository with name "%(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"), "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): 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: if group_parent_id == -1:
group_parent_id = None 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")) group_obj = RepoGroup.get(old_data.get("group_id"))
parent_group_changed = False parent_group_changed = False
if edit: if edit:
@ -457,6 +467,7 @@ def ValidRepoName(localizer, edit=False, old_data=None):
# inside a group # inside a group
"repository_in_group_exists": _('Repository with name %(repo)s exists in group "%(group)s"'), "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"'), "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): 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_path = value.get("group_path")
group_name = value.get("group_name") 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) msg = M(self, "invalid_repo_name", state, repo=repo_name)
raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg}) 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 rename = old_data.get("repo_name") != repo_name_full
create = not edit create = not edit
if rename or create: if rename or create:
@ -532,25 +547,6 @@ def SlugifyName(localizer):
return _validator 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): def ValidCloneUri(localizer):
_ = localizer _ = localizer

View file

@ -3245,3 +3245,55 @@ details:not([open]) > :not(summary) {
text-overflow: ellipsis; text-overflow: ellipsis;
width: 130px; 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;
}

View file

@ -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_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_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
pyroutes.register('repo_group_list_data', '/_repo_groups', []); 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_group_new', '/_admin/repo_group/new', []);
pyroutes.register('repo_groups', '/_admin/repo_groups', []); pyroutes.register('repo_groups', '/_admin/repo_groups', []);
pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []); 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_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_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
pyroutes.register('repo_list_data', '/_repos', []); 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_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', '/%(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']); pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);

View file

@ -24,6 +24,7 @@
</%def> </%def>
<%def name="main()"> <%def name="main()">
<div class="box"> <div class="box">
${h.secure_form(h.route_path('repo_group_create'), request=request)} ${h.secure_form(h.route_path('repo_group_create'), request=request)}
<div class="form"> <div class="form">
@ -34,24 +35,40 @@
<label for="group_name">${_('Group name')}:</label> <label for="group_name">${_('Group name')}:</label>
</div> </div>
<div class="input"> <div class="input">
${h.text('group_name', class_="medium")} <div class="full-repo-name-container">
% if c.personal_repo_group:
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
<i class="icon-repo-group tooltip"
title="${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}"></i>
</a>
% endif
${h.select('group_parent_id', request.GET.get('parent_group'), c.repo_groups,class_="medium")}
<div class="repo-create-spacer"> / </div>
<div class="repo-name-input-container">
${h.text('group_name', class_="medium")}
<div class="repo-check-container check-progress" id="group_check" style="display: none"></div>
</div>
</div>
<div class="repo-header-row">
<span class="help-block-repo-create">
${_('Parent Repository Group / Repository Group Name')}.
</span>
</div>
## COPY PERMS
<div id="copy_perms">
<div id="checkbox-span">
${h.checkbox('group_copy_permissions', value="True", checked="checked")}
</div>
<div class="help-block-inline">
${_('Copy permissions from parent repository group.')}
</div>
</div>
</div> </div>
</div> </div>
<div class="field">
<div class="label">
<label for="group_parent_id">${_('Repository group')}:</label>
</div>
<div class="select">
${h.select('group_parent_id', 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
</div>
</div>
<div class="field"> <div class="field">
<div class="label"> <div class="label">
<label for="group_description">${_('Description')}:</label> <label for="group_description">${_('Description')}:</label>
@ -73,16 +90,6 @@
</div> </div>
</div> </div>
<div id="copy_perms" class="field">
<div class="label label-checkbox">
<label for="group_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('group_copy_permissions', value="True", checked="checked")}
<span class="help-block">${_('Copy permissions from parent repository group.')}</span>
</div>
</div>
<div class="buttons"> <div class="buttons">
${h.submit('save',_('Create Repository Group'),class_="btn")} ${h.submit('save',_('Create Repository Group'),class_="btn")}
</div> </div>
@ -91,31 +98,90 @@
${h.end_form()} ${h.end_form()}
</div> </div>
<script> <script>
$(document).ready(function(){ $(document).ready(function () {
var setCopyPermsOption = function(group_val){ let setCopyPermsOption = function (group_val) {
if(group_val !== "-1"){ if (group_val !== "-1") {
$('#copy_perms').show() $('#copy_perms').show()
} } else {
else{
$('#copy_perms').hide(); $('#copy_perms').hide();
} }
}; };
$("#group_parent_id").select2({ $("#group_parent_id").select2({
'containerCssClass': "drop-menu", 'containerCssClass': "drop-menu",
'dropdownCssClass': "drop-menu-dropdown", 'dropdownCssClass': "drop-menu-dropdown",
'dropdownAutoWidth': true 'dropdownAutoWidth': true
}); });
setCopyPermsOption($('#group_parent_id').val()); setCopyPermsOption($('#group_parent_id').val());
$("#group_parent_id").on("change", function(e) {
setCopyPermsOption(e.val) $("#group_parent_id").on("change", function (e) {
setCopyPermsOption(e.val);
$('#group_name').trigger('input');
}); });
$('#group_name').focus(); $('#group_name').focus();
$('#select_my_group').on('click', function(e){ $('#select_my_group').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$("#group_parent_id").val($(this).data('personalGroupId')).trigger("change"); $("#group_parent_id").val($(this).data('personalGroupId')).trigger("change");
}) });
let debounceTimer;
$('#group_name').on('input paste', function () {
let groupName = $(this).val();
if (groupName.length > 0) {
$('#group_check').text(_gettext('checking availability...'))
.show()
.removeClass('check-error check-success')
.addClass('check-progress');
} else {
$('#group_check').hide();
clearTimeout(debounceTimer);
return;
}
clearTimeout(debounceTimer); // Cancel the previous timer
debounceTimer = setTimeout(function () {
performRepoGroupLookup(groupName);
}, 200);
function performRepoGroupLookup(groupName) {
self = this;
let repoGroup = $('#group_parent_id').val();
$.ajax({
url: pyroutes.url('repo_group_name_check'),
data: {"repo_group_name": groupName, "repo_group": repoGroup},
dataType: 'json',
type: 'POST',
success: function (data) {
let result = data.result;
if (result.check_error === true) {
$('#group_check')
.text(result.errors)
.removeClass('check-progress check-success')
.addClass('check-error');
} else {
let text = "Repository group `{0}` is available".format(result.repo_group_name);
$('#group_check')
.text(text)
.removeClass('check-progress check-error')
.addClass('check-success');
}
},
error: function (jqXHR, textStatus, errorThrown) {
var prefix = "Error while checking name.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
alert(message)
}
})
}
});
}) })
</script> </script>
</%def> </%def>

View file

@ -5,16 +5,49 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<div class="label"> <div class="label">
<label for="repo_name">${_('Repository name')}:</label> <label for="repo_name">${_('Repository')}:</label>
</div> </div>
<div class="input"> <div class="input">
${h.text('repo_name', class_="medium")} <div class="full-repo-name-container">
<div class="info-block"> % if c.personal_repo_group:
<a id="remote_clone_toggle" href="#">${_('Import Existing Repository ?')}</a> <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
<i class="icon-repo-group tooltip"
title="${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}"></i>
</a>
% endif
${h.select('repo_group', request.GET.get('parent_group'), c.repo_groups, class_="medium")}
<div class="repo-create-spacer"> / </div>
<div class="repo-name-input-container">
${h.text('repo_name', class_="medium", id="repo_name_input")}
<div class="repo-check-container check-progress" id="repo_check" style="display: none"></div>
</div>
</div>
<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> </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>
%if not c.rhodecode_user.is_admin: %if not c.rhodecode_user.is_admin:
${h.hidden('user_created',True)} ${h.hidden('user_created',True)}
%endif %endif
</div> </div>
</div> </div>
<div id="remote_clone" class="field" style="display: none;"> <div id="remote_clone" class="field" style="display: none;">
@ -37,30 +70,15 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</span> </span>
</div> </div>
</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="field">
<div class="label"> <div class="label">
<label for="repo_type">${_('Type')}:</label> <label for="repo_type">${_('Type')}:</label>
</div> </div>
<div class="fields repo-type-radio"> <div class="fields repo-type-radio">
% for backend in c.backends: % for backend in c.backends:
% if loop.index == 0:
% if backend == c.default_backend:
<input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" checked="checked"/> <input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" checked="checked"/>
% else: % else:
<input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" /> <input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" />
@ -73,7 +91,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
% endfor % endfor
<span class="help-block">${_('Set the type of repository to create.')}</span> <span class="help-block">${_('Set the type of repository to create.')}</span>
</div> </div>
</div> </div>
@ -98,15 +115,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</span> </span>
</div> </div>
</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="field">
<div class="label label-checkbox"> <div class="label label-checkbox">
<label for="repo_private">${_('Private Repository')}:</label> <label for="repo_private">${_('Private Repository')}:</label>
@ -116,31 +125,54 @@ ${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> <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
</div> </div>
</div> </div>
<div class="field repo-bootstrap-items repo-bootstrap-readme">
<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 repo-bootstrap-svn" style="display: none">
<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"> <div class="buttons">
${h.submit('save',_('Create Repository'),class_="btn")} ${h.submit('save',_('Create Repository'),class_="btn")}
</div> </div>
</div> </div>
</div> </div>
<script> <script>
$(document).ready(function(){ $(document).ready(function () {
var setCopyPermsOption = function(group_val){ let setCopyPermsOption = function (group_val) {
if(group_val != "-1"){ if (group_val !== "-1") {
$('#copy_perms').show() $('#copy_perms').show()
} } else {
else{
$('#copy_perms').hide(); $('#copy_perms').hide();
} }
}; };
$('#remote_clone_toggle').on('click', function(e){ $('#remote_clone_toggle').on('click', function (e) {
$('#remote_clone').show(); $('#remote_clone').show();
e.preventDefault(); e.preventDefault();
$('#remote_clone_toggle').hide();
$('.repo-bootstrap-items').remove();
}); });
if($('#remote_clone input').hasClass('error')){ if ($('#remote_clone input').hasClass('error')) {
$('#remote_clone').show(); $('#remote_clone').show();
} }
if($('#remote_clone input').val()){ if ($('#remote_clone input').val()) {
$('#remote_clone').show(); $('#remote_clone').show();
} }
@ -152,15 +184,102 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
}); });
setCopyPermsOption($('#repo_group').val()); setCopyPermsOption($('#repo_group').val());
$("#repo_group").on("change", function(e) {
setCopyPermsOption(e.val) $("#repo_group").on("change", function (e) {
setCopyPermsOption(e.val);
$('#repo_name_input').trigger('input');
}); });
$('#repo_name').focus(); $('#repo_name_input').focus();
$('#select_my_group').on('click', function(e){ let debounceTimer;
$('#repo_name_input').on('input paste', function () {
let repoName = $(this).val();
if (repoName.length > 0) {
$('#repo_check').text(_gettext('checking availability...'))
.show()
.removeClass('check-error check-success')
.addClass('check-progress');
} else {
$('#repo_check').hide();
clearTimeout(debounceTimer);
return;
}
clearTimeout(debounceTimer); // Cancel the previous timer
debounceTimer = setTimeout(function () {
performRepoLookup(repoName);
}, 200);
function performRepoLookup(repoName) {
self = this;
let repoGroup = $('#repo_group').val();
let repoType = $('input[name="repo_type"]').val();
$.ajax({
url: pyroutes.url('repo_name_check'),
data: {"repo_name": repoName, "repo_group": repoGroup, "repo_type": repoType},
dataType: 'json',
type: 'POST',
success: function (data) {
let result = data.result;
if (result.check_error === true) {
$('#repo_check')
.text(result.errors)
.removeClass('check-progress check-success')
.addClass('check-error');
} else {
let text = _gettext("Repository `{0}` is available").format(result.repo_name);
$('#repo_check')
.text(text)
.removeClass('check-progress check-error')
.addClass('check-success');
}
},
error: function (jqXHR, textStatus, errorThrown) {
var prefix = "Error while checking name.\n"
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
alert(message)
}
})
}
});
$('#select_my_group').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$("#repo_group").val($(this).data('personalGroupId')).trigger("change"); $("#repo_group").val($(this).data('personalGroupId')).trigger("change");
});
$('#clone_uri').on('input paste', function (e) {
let url = $(this).val().trim();
if (url) {
let parts = url.split('/').filter(Boolean);
let lastPart = parts[parts.length - 1];
let repoName = lastPart.replace(/\.git$/, "");
$('#repo_name_input').val(repoName).trigger('input');
} else {
$('#repo_name_input').val("").trigger('input');
}
});
let defaultVcsBackend = "${c.default_backend}";
if (defaultVcsBackend === "svn") {
$('.repo-bootstrap-svn').show();
}
$('input[name="repo_type"]').on('change', function (e) {
let selectedVcs = $(this).val();
if (selectedVcs === "svn") {
$('.repo-bootstrap-svn').show();
} else {
$('.repo-bootstrap-svn').hide();
}
}) })
}) })

View file

@ -548,10 +548,10 @@
% endif % endif
% endif % endif
<li class="submenu-title">RhodeCode</li>
## personal group ## personal group
% if c.rhodecode_user.personal_repo_group: % if c.rhodecode_user.personal_repo_group:
<li class="submenu-title">Personal Group</li>
<li> <li>
<a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a> <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
</li> </li>
@ -559,25 +559,21 @@
<li> <li>
<a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a> <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
</li> </li>
% endif % else:
% if can_create_repos:
## Global actions
<li class="submenu-title">RhodeCode</li>
% if can_create_repos:
<li> <li>
<a href="${h.route_path('repo_new')}" >${_('New Repository')}</a> <a href="${h.route_path('repo_new')}">${_('New Repository')}</a>
</li> </li>
% endif % endif
% if can_create_repo_groups:
% if can_create_repo_groups:
<li> <li>
<a href="${h.route_path('repo_group_new')}" >${_('New Repository Group')}</a> <a href="${h.route_path('repo_group_new')}">${_('New Repository Group')}</a>
</li> </li>
% endif
% endif % endif
<li>
<li> <a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
<a href="${h.route_path('gists_new')}">${_('New Gist')}</a> </li>
</li>
</ol> </ol>

View file

@ -63,7 +63,7 @@
ico_data = [] ico_data = []
for line in source.splitlines(): for line in source.splitlines():
line = line.split(':before') line = line.split(':before')
line = map(string.strip, line) line = [x.strip() for x in line]
if len(line) in [2, 3]: if len(line) in [2, 3]:
if len(line) == 2: if len(line) == 2:
ico_cls, ico_code = line ico_cls, ico_code = line

View file

@ -200,22 +200,28 @@ def test_ValidAuth(localizer, config_stub):
pytest.raises(formencode.Invalid, validator.to_python, invalid_creds) pytest.raises(formencode.Invalid, validator.to_python, invalid_creds)
def test_ValidRepoName(localizer): def test_ValidRepoName_empty_name(localizer):
validator = v.ValidRepoName(localizer) validator = v.ValidRepoName(localizer)
pytest.raises(formencode.Invalid, validator.to_python, {"repo_name": ""}) with pytest.raises(formencode.Invalid) as excinfo:
validator.to_python({"repo_name": ""})
pytest.raises(formencode.Invalid, validator.to_python, {"repo_name": HG_REPO})
def test_ValidRepoName_repo_exists(localizer):
validator = v.ValidRepoName(localizer)
# Duplicate
with pytest.raises(formencode.Invalid) as excinfo:
validator.to_python({"repo_name": HG_REPO})
def test_ValidRepoName_conflict_with_repo_group(localizer):
validator = v.ValidRepoName(localizer)
# Duplicate repo groups
gr = RepoGroupModel().create(group_name="group_test", group_description="desc", owner=TEST_USER_ADMIN_LOGIN) gr = RepoGroupModel().create(group_name="group_test", group_description="desc", owner=TEST_USER_ADMIN_LOGIN)
pytest.raises(formencode.Invalid, validator.to_python, {"repo_name": gr.group_name}) with pytest.raises(formencode.Invalid) as excinfo:
validator.to_python({"repo_name": gr.group_name})
# TODO: write an error case for that ie. create a repo withinh a group
# pytest.raises(formencode.Invalid,
# validator.to_python, {'repo_name': 'some',
# 'repo_group': gr.group_id})
def test_ValidForkName(localizer): def test_ValidForkName(localizer):