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
This commit is contained in:
RhodeCode Admin 2025-12-23 21:04:15 +01:00
parent ee52770fff
commit cd01bbbddd
23 changed files with 656 additions and 200 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

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

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

View file

@ -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
@ -707,6 +708,15 @@ class RepoModel(BaseModel):
nodes[safe_bytes(f"readme.{ext}")] = {"content": safe_bytes(readme_content)} 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: if nodes:
commit = ScmModel().create_nodes( commit = ScmModel().create_nodes(
user=db_user.user_id, user=db_user.user_id,

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

@ -7,63 +7,22 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<div class="label"> <div class="label">
<label for="repo_name">${_('Repository')}:</label> <label for="repo_name">${_('Repository')}:</label>
</div> </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"> <div class="input">
<div class="full-repo-name-container">
${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")} % if c.personal_repo_group:
<span class="repo-create-spacer"> / </span> <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
${h.text('repo_name', class_="medium", id="repo_name_input")} <i class="icon-repo-group tooltip"
title="${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}"></i>
% if c.personal_repo_group: </a>
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}"> % endif
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)} ${h.select('repo_group', request.GET.get('parent_group'), c.repo_groups, class_="medium")}
</a> <div class="repo-create-spacer"> / </div>
% endif <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"> <div class="repo-header-row">
<span class="help-block-repo-create"> <span class="help-block-repo-create">
@ -85,7 +44,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</div> </div>
</div> </div>
## TODO: what is this ? i don't think we use it anymore
%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
@ -118,9 +76,9 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<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}" />
@ -168,7 +126,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</div> </div>
</div> </div>
<div class="field repo-bootstrap-items"> <div class="field repo-bootstrap-items repo-bootstrap-readme">
<div class="label label-checkbox"> <div class="label label-checkbox">
<label for="repo_bootstrap_readme">${_('Add Readme')}:</label> <label for="repo_bootstrap_readme">${_('Add Readme')}:</label>
</div> </div>
@ -178,7 +136,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</div> </div>
</div> </div>
<div class="field repo-bootstrap-items"> <div class="field repo-bootstrap-items repo-bootstrap-svn" style="display: none">
<div class="label label-checkbox"> <div class="label label-checkbox">
<label for="repo_bootstrap_svn_branch_struct">${_('Add SVN structure')}:</label> <label for="repo_bootstrap_svn_branch_struct">${_('Add SVN structure')}:</label>
</div> </div>
@ -195,27 +153,26 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</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(); $('#remote_clone_toggle').hide();
$('.repo-bootstrap-items').remove(); $('.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();
} }
@ -227,29 +184,101 @@ ${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) {
$('#clone_uri').on('input paste', function(e) { let url = $(this).val().trim();
var url = $(this).val().trim();
if (url) { if (url) {
var parts = url.split('/').filter(Boolean); let parts = url.split('/').filter(Boolean);
var lastPart = parts[parts.length - 1]; let lastPart = parts[parts.length - 1];
var repoName = lastPart.replace(/\.git$/, ""); let repoName = lastPart.replace(/\.git$/, "");
$('#repo_name_input').val(repoName); $('#repo_name_input').val(repoName).trigger('input');
} else {
$('#repo_name_input').val("").trigger('input');
} }
else { });
$('#repo_name_input').val("");
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):