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:
parent
ee52770fff
commit
cd01bbbddd
23 changed files with 656 additions and 200 deletions
|
|
@ -85,7 +85,7 @@ dogpile.cache==1.5.0
|
|||
decorator==5.1.1
|
||||
stevedore==5.1.0
|
||||
pbr==5.11.1
|
||||
formencode==2.1.0
|
||||
formencode==2.1.1
|
||||
six==1.17.0
|
||||
fsspec==2025.10.0
|
||||
gunicorn==23.0.0
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ class TestCreateRepo(object):
|
|||
assert repo is not None
|
||||
|
||||
expected = {
|
||||
"msg": "Created new repository `%s`" % (expected_name,),
|
||||
"msg": f"Created new repository `{expected_name}`",
|
||||
"success": True,
|
||||
"task": None,
|
||||
}
|
||||
|
|
@ -327,3 +327,49 @@ class TestCreateRepo(object):
|
|||
fixture.destroy_repo(expected_name)
|
||||
if parent_group:
|
||||
fixture.destroy_repo_group(parent_group)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"given, expected_exc",
|
||||
[
|
||||
("robots.txt", True),
|
||||
("favicon.ico", True),
|
||||
],
|
||||
)
|
||||
def test_api_create_repo_forbidden_names(self, backend, given, expected_exc):
|
||||
repo_name = given
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
"create_repo",
|
||||
repo_name=repo_name,
|
||||
owner=TEST_USER_ADMIN_LOGIN,
|
||||
repo_type=backend.alias,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
expected = {"repo_name": f"Repository with name `{given}` is forbidden"}
|
||||
assert_error(id_, expected, given=response.body)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"given, expected_exc",
|
||||
[
|
||||
("_admin", True),
|
||||
("_static", True),
|
||||
("_file_store", True),
|
||||
("_repos", True),
|
||||
("_goto_data", True),
|
||||
("_file_preview", True),
|
||||
],
|
||||
)
|
||||
def test_api_create_repo_forbidden_names_and_wrong_name(self, backend, given, expected_exc):
|
||||
repo_name = given
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
"create_repo",
|
||||
repo_name=repo_name,
|
||||
owner=TEST_USER_ADMIN_LOGIN,
|
||||
repo_type=backend.alias,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
expected = {
|
||||
"repo_name": f"Name must start with a letter or number. Got `{given}`; Repository with name `{given}` is forbidden"
|
||||
}
|
||||
assert_error(id_, expected, given=response.body)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class TestCreateRepoGroup(object):
|
|||
|
||||
repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name)
|
||||
assert repo_group is not None
|
||||
ret = {"msg": "Created new repo group `%s`" % (repo_group_name,), "repo_group": repo_group.get_api_data()}
|
||||
ret = {"msg": f"Created new repo group `{repo_group_name}`", "repo_group": repo_group.get_api_data()}
|
||||
expected = ret
|
||||
try:
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
|
|
@ -75,7 +75,7 @@ class TestCreateRepoGroup(object):
|
|||
|
||||
repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name)
|
||||
assert repo_group is not None
|
||||
ret = {"msg": "Created new repo group `%s`" % (full_repo_group_name,), "repo_group": repo_group.get_api_data()}
|
||||
ret = {"msg": f"Created new repo group `{full_repo_group_name}`", "repo_group": repo_group.get_api_data()}
|
||||
expected = ret
|
||||
try:
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
|
|
@ -115,7 +115,7 @@ class TestCreateRepoGroup(object):
|
|||
owner=TEST_USER_ADMIN_LOGIN,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
expected = {"unique_repo_group_name": "Repository group with name `{}` already exists".format(repo_group_name)}
|
||||
expected = {"unique_repo_group_name": f"Repository group with name `{repo_group_name}` already exists"}
|
||||
try:
|
||||
assert_error(id_, expected, given=response.body)
|
||||
finally:
|
||||
|
|
@ -142,7 +142,7 @@ class TestCreateRepoGroup(object):
|
|||
|
||||
repo_group = RepoGroupModel.cls.get_by_group_name(repo_group_name)
|
||||
assert repo_group is not None
|
||||
expected = {"msg": "Created new repo group `%s`" % (repo_group_name,), "repo_group": repo_group.get_api_data()}
|
||||
expected = {"msg": f"Created new repo group `{repo_group_name}`", "repo_group": repo_group.get_api_data()}
|
||||
try:
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
finally:
|
||||
|
|
@ -174,7 +174,7 @@ class TestCreateRepoGroup(object):
|
|||
repo_group = RepoGroupModel.cls.get_by_group_name(full_repo_group_name)
|
||||
assert repo_group is not None
|
||||
expected = {
|
||||
"msg": "Created new repo group `{}`".format(full_repo_group_name),
|
||||
"msg": f"Created new repo group `{full_repo_group_name}`",
|
||||
"repo_group": repo_group.get_api_data(),
|
||||
}
|
||||
try:
|
||||
|
|
@ -206,8 +206,7 @@ class TestCreateRepoGroup(object):
|
|||
response = api_call(self.app, params)
|
||||
|
||||
expected = {
|
||||
"repo_group": "You do not have the permissions to store "
|
||||
"repository groups inside repository group `{}`".format(repo_group_name)
|
||||
"repo_group": f"You do not have the permissions to store repository groups inside repository group `{repo_group_name}`"
|
||||
}
|
||||
try:
|
||||
assert_error(id_, expected, given=response.body)
|
||||
|
|
@ -242,13 +241,57 @@ class TestCreateRepoGroup(object):
|
|||
owner=TEST_USER_ADMIN_LOGIN,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
expected = "failed to create repo group `%s`" % (repo_group_name,)
|
||||
expected = f"failed to create repo group `{repo_group_name}`"
|
||||
assert_error(id_, expected, given=response.body)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"given, expected_exc",
|
||||
[
|
||||
("_admin", False),
|
||||
("_static", False),
|
||||
("_file_store", False),
|
||||
("_repos", False),
|
||||
("_goto_data", False),
|
||||
("_file_preview", False),
|
||||
],
|
||||
)
|
||||
def test_api_create_repo_group_forbidden_and_wrong_names(self, backend, given, expected_exc):
|
||||
repo_group_name = given
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
"create_repo_group",
|
||||
group_name=repo_group_name,
|
||||
owner=TEST_USER_ADMIN_LOGIN,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
expected = {
|
||||
"repo_group_name": f"Name must start with a letter or number. Got `{repo_group_name}`; Repository group with name `{repo_group_name}` is forbidden"
|
||||
}
|
||||
assert_error(id_, expected, given=response.body)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"given, expected_exc",
|
||||
[
|
||||
("favicon.ico", False),
|
||||
("robots.txt", False),
|
||||
],
|
||||
)
|
||||
def test_api_create_repo_group_forbidden_names(self, backend, given, expected_exc):
|
||||
repo_group_name = given
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
"create_repo_group",
|
||||
group_name=repo_group_name,
|
||||
owner=TEST_USER_ADMIN_LOGIN,
|
||||
)
|
||||
response = api_call(self.app, params)
|
||||
expected = {"repo_group_name": f"Repository group with name `{repo_group_name}` is forbidden"}
|
||||
assert_error(id_, expected, given=response.body)
|
||||
|
||||
def test_create_group_with_extra_slashes_in_name(self, user_util):
|
||||
existing_repo_group = user_util.create_repo_group()
|
||||
dirty_group_name = "//{}//group2//".format(existing_repo_group.group_name)
|
||||
cleaned_group_name = "{}/group2".format(existing_repo_group.group_name)
|
||||
dirty_group_name = f"//{existing_repo_group.group_name}//group2//"
|
||||
cleaned_group_name = f"{existing_repo_group.group_name}/group2"
|
||||
|
||||
id_, params = build_data(
|
||||
self.apikey,
|
||||
|
|
@ -259,7 +302,7 @@ class TestCreateRepoGroup(object):
|
|||
response = api_call(self.app, params)
|
||||
repo_group = RepoGroupModel.cls.get_by_group_name(cleaned_group_name)
|
||||
expected = {
|
||||
"msg": "Created new repo group `%s`" % (cleaned_group_name,),
|
||||
"msg": f"Created new repo group `{cleaned_group_name}`",
|
||||
"repo_group": repo_group.get_api_data(),
|
||||
}
|
||||
assert_ok(id_, expected, given=response.body)
|
||||
|
|
|
|||
|
|
@ -777,6 +777,8 @@ def create_repo(
|
|||
enable_locking=Optional(False),
|
||||
enable_downloads=Optional(False),
|
||||
copy_permissions=Optional(False),
|
||||
bootstrap_readme=Optional(False),
|
||||
bootstrap_svn_branch_struct=Optional(False),
|
||||
):
|
||||
"""
|
||||
Creates a repository.
|
||||
|
|
@ -816,10 +818,12 @@ def create_repo(
|
|||
:type enable_downloads: bool
|
||||
:param enable_statistics:
|
||||
:type enable_statistics: bool
|
||||
:param copy_permissions: Copy permission from group in which the
|
||||
repository is being created.
|
||||
:param copy_permissions: Copy permission from group in which the repository is being created.
|
||||
:type copy_permissions: bool
|
||||
|
||||
:param bootstrap_readme: Add default readme for new repositories
|
||||
:type bootstrap_readme: bool
|
||||
:param bootstrap_svn_branch_struct: Add branches/tags structure for SVN repositories.
|
||||
:type bootstrap_svn_branch_struct: bool
|
||||
|
||||
Example output:
|
||||
|
||||
|
|
@ -840,7 +844,7 @@ def create_repo(
|
|||
|
||||
id : <id_given_in_input>
|
||||
result : null
|
||||
error : {
|
||||
error : {
|
||||
'failed to create repository `<repo_name>`'
|
||||
}
|
||||
|
||||
|
|
@ -865,6 +869,11 @@ def create_repo(
|
|||
if isinstance(enable_downloads, Optional):
|
||||
enable_downloads = defs.get("repo_enable_downloads")
|
||||
|
||||
if isinstance(bootstrap_readme, Optional):
|
||||
bootstrap_readme = Optional.extract(bootstrap_readme)
|
||||
if isinstance(bootstrap_svn_branch_struct, Optional):
|
||||
bootstrap_svn_branch_struct = Optional.extract(bootstrap_svn_branch_struct)
|
||||
|
||||
landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
|
||||
ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
|
||||
ref_choices = list(set(ref_choices + [landing_ref]))
|
||||
|
|
@ -894,6 +903,8 @@ def create_repo(
|
|||
repo_enable_statistics=enable_statistics,
|
||||
repo_enable_downloads=enable_downloads,
|
||||
repo_enable_locking=enable_locking,
|
||||
repo_bootstrap_readme=bootstrap_readme,
|
||||
repo_bootstrap_svn_branch_struct=bootstrap_svn_branch_struct,
|
||||
)
|
||||
)
|
||||
except validation_schema.Invalid as err:
|
||||
|
|
@ -915,6 +926,8 @@ def create_repo(
|
|||
"enable_locking": schema_data["repo_enable_locking"],
|
||||
"enable_downloads": schema_data["repo_enable_downloads"],
|
||||
"repo_copy_permissions": schema_data["repo_copy_permissions"],
|
||||
"repo_bootstrap_readme": schema_data["repo_bootstrap_readme"],
|
||||
"repo_bootstrap_svn_branch_struct": schema_data["repo_bootstrap_svn_branch_struct"],
|
||||
}
|
||||
|
||||
task = RepoModel().create(form_data=data, cur_user=owner.user_id)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,30 @@ log = logging.getLogger(__name__)
|
|||
ADMIN_PREFIX: str = "/_admin"
|
||||
STATIC_FILE_PREFIX: str = "/_static"
|
||||
|
||||
# These names shouldn't be allowed as repo names or group names
|
||||
RESERVED_NAMES: list = [
|
||||
ADMIN_PREFIX.lstrip("/"),
|
||||
STATIC_FILE_PREFIX.lstrip("/"),
|
||||
"_admin",
|
||||
"_channelstream",
|
||||
"_file_preview",
|
||||
"_file_store",
|
||||
"_goto_data",
|
||||
"_home_repo_groups",
|
||||
"_home_repos",
|
||||
"_hovercard",
|
||||
"_markup_preview",
|
||||
"_profile_user_group",
|
||||
"_profiles",
|
||||
"_repo_groups",
|
||||
"_repos",
|
||||
"_store_session_attr",
|
||||
"_user_groups",
|
||||
"_users",
|
||||
"favicon.ico",
|
||||
"robots.txt",
|
||||
]
|
||||
|
||||
URL_NAME_REQUIREMENTS = {
|
||||
# group name can have a slash in them, but they must not end with a slash
|
||||
"group_name": r".*?[^/]",
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ class TestAdminRepos(object):
|
|||
|
||||
def test_create_in_group_inherit_permissions(self, autologin_user, backend, csrf_token):
|
||||
# create GROUP
|
||||
group_name = "sometest_%s" % backend.alias
|
||||
group_name = f"sometest_{backend.alias}"
|
||||
gr = RepoGroupModel().create(group_name=group_name, group_description="test", owner=TEST_USER_ADMIN_LOGIN)
|
||||
perm = Permission.get_by_key("repository.write")
|
||||
RepoGroupModel().grant_user_permission(gr, TEST_USER_REGULAR_LOGIN, perm)
|
||||
|
|
@ -392,6 +392,22 @@ class TestAdminRepos(object):
|
|||
)
|
||||
response.mustcontain("Repository name cannot end with .git")
|
||||
|
||||
def test_create_reserved_name_admin(self, autologin_user, backend, csrf_token):
|
||||
# Test creating a repo with reserved prefix/name _admin
|
||||
repo_name = "_admin"
|
||||
description = "description for reserved name repo"
|
||||
response = self.app.post(
|
||||
route_path("repo_create"),
|
||||
fixture._get_repo_create_params(
|
||||
repo_private=False,
|
||||
repo_name=repo_name,
|
||||
repo_type=backend.alias,
|
||||
repo_description=description,
|
||||
csrf_token=csrf_token,
|
||||
),
|
||||
)
|
||||
response.mustcontain(f"Repository name {repo_name} is disallowed")
|
||||
|
||||
def test_default_user_cannot_access_private_repo_in_a_group(self, autologin_user, user_util, backend):
|
||||
group = user_util.create_repo_group()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
from rhodecode.apps._base import add_route_with_slash
|
||||
from rhodecode.apps._base import add_route_with_slash, ADMIN_PREFIX
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
|
@ -47,6 +47,23 @@ def includeme(config):
|
|||
from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
|
||||
from rhodecode.apps.repository.views.repo_tags import RepoTagsView
|
||||
|
||||
config.add_route(name="repo_name_check", pattern=ADMIN_PREFIX + "/repo_name_check")
|
||||
config.add_view(
|
||||
RepoChecksView,
|
||||
attr="repo_name_check",
|
||||
route_name="repo_name_check",
|
||||
request_method="POST",
|
||||
renderer="json_ext",
|
||||
)
|
||||
config.add_route(name="repo_group_name_check", pattern=ADMIN_PREFIX + "/repo_group_name_check")
|
||||
config.add_view(
|
||||
RepoChecksView,
|
||||
attr="repo_group_name_check",
|
||||
route_name="repo_group_name_check",
|
||||
request_method="POST",
|
||||
renderer="json_ext",
|
||||
)
|
||||
|
||||
# repo creating checks, special cases that aren't repo routes
|
||||
config.add_route(name="repo_creating", pattern="/{repo_name:.*?[^/]}/repo_creating")
|
||||
config.add_view(
|
||||
|
|
|
|||
|
|
@ -20,12 +20,16 @@ import logging
|
|||
|
||||
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
|
||||
|
||||
import rhodecode
|
||||
from rhodecode.apps._base import BaseAppView
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib.auth import NotAnonymous, HasRepoPermissionAny
|
||||
from rhodecode.model.db import Repository
|
||||
from rhodecode.lib.str_utils import safe_int
|
||||
from rhodecode.model import validation_schema
|
||||
from rhodecode.model.db import Repository, RepoGroup
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.validation_schema.types import RepoNameType
|
||||
from rhodecode.model.validation_schema.schemas import repo_schema, repo_group_schema
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -35,6 +39,113 @@ class RepoChecksView(BaseAppView):
|
|||
c = self._get_local_tmpl_context()
|
||||
return c
|
||||
|
||||
@NotAnonymous()
|
||||
def repo_name_check(self):
|
||||
post_items = dict(self.request.POST.items())
|
||||
|
||||
_ = self.request.translate
|
||||
|
||||
default_response = {
|
||||
"result": {
|
||||
"repo_name": "",
|
||||
"check_error": False,
|
||||
"errors": "",
|
||||
"exists": False,
|
||||
"slugify": False,
|
||||
}
|
||||
}
|
||||
|
||||
# determine a full repo name
|
||||
repo_name = post_items.get("repo_name")
|
||||
repo_group_id = safe_int(post_items["repo_group"])
|
||||
if repo_group_id and repo_group_id != -1:
|
||||
repo_group = RepoGroup.get(repo_group_id)
|
||||
if repo_group:
|
||||
group_path = repo_group.full_path
|
||||
repo_name = group_path + RepoGroup.url_sep() + repo_name
|
||||
|
||||
schema = repo_schema.RepoSchema().bind(
|
||||
repo_type_options=rhodecode.BACKENDS.keys(),
|
||||
)
|
||||
|
||||
try:
|
||||
schema_data = schema.deserialize(
|
||||
dict(
|
||||
repo_name=repo_name,
|
||||
repo_type=post_items.get("repo_type"),
|
||||
repo_owner=self.request.user.username,
|
||||
)
|
||||
)
|
||||
default_response["result"]["repo_name"] = schema_data["repo_name"]
|
||||
if post_items.get("repo_name") != schema_data["repo_name"]:
|
||||
default_response["result"]["slugify"] = True
|
||||
except validation_schema.Invalid as err:
|
||||
errors = err.asdict()
|
||||
if errors.get("repo_name"):
|
||||
default_response["result"]["check_error"] = True
|
||||
default_response["result"]["errors"] = errors["repo_name"]
|
||||
elif errors.get("unique_repo_name"):
|
||||
default_response["result"]["exists"] = True
|
||||
default_response["result"]["errors"] = errors["unique_repo_name"]
|
||||
default_response["result"]["check_error"] = True
|
||||
else:
|
||||
default_response["result"]["check_error"] = True
|
||||
default_response["result"]["error"] = f"Unknown error: {errors}"
|
||||
|
||||
return default_response
|
||||
|
||||
@NotAnonymous()
|
||||
def repo_group_name_check(self):
|
||||
post_items = dict(self.request.POST.items())
|
||||
|
||||
_ = self.request.translate
|
||||
|
||||
default_response = {
|
||||
"result": {
|
||||
"repo_group_name": "",
|
||||
"check_error": False,
|
||||
"errors": "",
|
||||
"exists": False,
|
||||
"slugify": False,
|
||||
}
|
||||
}
|
||||
|
||||
# determine a full repo name
|
||||
repo_group_name = post_items.get("repo_group_name")
|
||||
repo_group_id = safe_int(post_items["repo_group"])
|
||||
if repo_group_id and repo_group_id != -1:
|
||||
repo_group = RepoGroup.get(repo_group_id)
|
||||
if repo_group:
|
||||
group_path = repo_group.full_path
|
||||
repo_group_name = group_path + RepoGroup.url_sep() + repo_group_name
|
||||
|
||||
schema = repo_group_schema.RepoGroupSchema().bind()
|
||||
|
||||
try:
|
||||
schema_data = schema.deserialize(
|
||||
dict(
|
||||
repo_group_name=repo_group_name,
|
||||
repo_group_owner=self.request.user.username,
|
||||
)
|
||||
)
|
||||
default_response["result"]["repo_group_name"] = schema_data["repo_group_name"]
|
||||
if post_items.get("repo_group_name") != schema_data["repo_group_name"]:
|
||||
default_response["result"]["slugify"] = True
|
||||
except validation_schema.Invalid as err:
|
||||
errors = err.asdict()
|
||||
if errors.get("repo_group_name"):
|
||||
default_response["result"]["check_error"] = True
|
||||
default_response["result"]["errors"] = errors["repo_group_name"]
|
||||
elif errors.get("unique_repo_group_name"):
|
||||
default_response["result"]["exists"] = True
|
||||
default_response["result"]["errors"] = errors["unique_repo_group_name"]
|
||||
default_response["result"]["check_error"] = True
|
||||
else:
|
||||
default_response["result"]["check_error"] = True
|
||||
default_response["result"]["error"] = f"Unknown error: {errors}"
|
||||
|
||||
return default_response
|
||||
|
||||
@NotAnonymous()
|
||||
def repo_creating(self):
|
||||
c = self.load_default_context()
|
||||
|
|
@ -76,9 +187,10 @@ class RepoChecksView(BaseAppView):
|
|||
log.debug("celery: checking result for task:%s", task_id)
|
||||
task = celery_app.AsyncResult(task_id)
|
||||
try:
|
||||
task.get(timeout=10)
|
||||
task.get(timeout=3)
|
||||
except exceptions.TimeoutError:
|
||||
task = None
|
||||
|
||||
if task and task.failed():
|
||||
msg = self._log_creation_exception(task.result, repo_name)
|
||||
h.flash(msg, category="error")
|
||||
|
|
|
|||
|
|
@ -411,6 +411,8 @@ def attach_context_attributes(context, request, user_id=None, is_api=None):
|
|||
context.csrf_token = csrf_token
|
||||
context.backends = list(rhodecode.BACKENDS.keys())
|
||||
|
||||
context.default_backend = rc_config.get("rhodecode_default_repo_type")
|
||||
|
||||
unread_count = 0
|
||||
user_bookmark_list = []
|
||||
if user_id:
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ def task_failure_signal(task_id, exception, args, kwargs, traceback, einfo, **ka
|
|||
statsd = StatsdClient.statsd
|
||||
if statsd:
|
||||
exc_type = f"{einfo.__class__.__module__}.{einfo.__class__.__name__}"
|
||||
statsd.incr("rhodecode_exception_total", tags=["exc_source:celery", "type:{}".format(exc_type)])
|
||||
statsd.incr("rhodecode_exception_total", tags=["exc_source:celery", f"type:{exc_type}"])
|
||||
|
||||
closer = celery_app.conf["PYRAMID_CLOSER"]
|
||||
if closer:
|
||||
|
|
|
|||
|
|
@ -284,7 +284,6 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab
|
|||
repo_name = All(
|
||||
v.UnicodeString(strip=True, min=1, not_empty=True),
|
||||
v.SlugifyName(localizer),
|
||||
v.CannotHaveGitSuffix(localizer),
|
||||
)
|
||||
repo_group = All(v.CanWriteGroup(localizer, old_data), v.OneOf(repo_groups, hideList=True))
|
||||
repo_type = v.OneOf(supported_backends, required=False, if_missing=old_data.get("repo_type"))
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ from rhodecode.model.db import (
|
|||
RepoGroup,
|
||||
RepositoryField,
|
||||
UserLog,
|
||||
RhodeCodeUi,
|
||||
)
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
|
||||
|
|
@ -707,6 +708,15 @@ class RepoModel(BaseModel):
|
|||
|
||||
nodes[safe_bytes(f"readme.{ext}")] = {"content": safe_bytes(readme_content)}
|
||||
|
||||
if svn_struct and db_repo.repo_type == "svn":
|
||||
for branch in RhodeCodeUi.SVN_BRANCHES_PATTERNS:
|
||||
branch_dir = branch.lstrip("/").rstrip("*").rstrip("/")
|
||||
nodes[safe_bytes(os.path.join(branch_dir, ".dirkeep"))] = {"content": b""}
|
||||
|
||||
for tag in RhodeCodeUi.SVN_TAGS_PATTERNS:
|
||||
tag_dir = tag.lstrip("/").rstrip("*").rstrip("/")
|
||||
nodes[safe_bytes(os.path.join(tag_dir, ".dirkeep"))] = {"content": b""}
|
||||
|
||||
if nodes:
|
||||
commit = ScmModel().create_nodes(
|
||||
user=db_user.user_id,
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def deferred_unique_name_validator(node, kw):
|
|||
|
||||
@colander.deferred
|
||||
def deferred_repo_group_name_validator(node, kw):
|
||||
return validators.valid_name_validator
|
||||
return colander.All(validators.valid_name_validator, validators.forbidden_repo_group_name_validator)
|
||||
|
||||
|
||||
@colander.deferred
|
||||
|
|
@ -170,7 +170,7 @@ class GroupType(colander.Mapping):
|
|||
try:
|
||||
return dict(repo_group_name=value)
|
||||
except Exception as e:
|
||||
raise colander.Invalid(node, '"${val}" is not a mapping type: ${err}'.format(val=value, err=e))
|
||||
raise colander.Invalid(node, f'"${value}" is not a mapping type: ${e}')
|
||||
|
||||
def deserialize(self, node, cstruct):
|
||||
if cstruct is colander.null:
|
||||
|
|
@ -179,7 +179,7 @@ class GroupType(colander.Mapping):
|
|||
appstruct = super().deserialize(node, cstruct)
|
||||
validated_name = appstruct["repo_group_name"]
|
||||
|
||||
# inject group based on once deserialized data
|
||||
# inject a group based on deserialized data
|
||||
(repo_group_name_without_group, parent_group_name, parent_group) = get_group_and_repo(validated_name)
|
||||
|
||||
appstruct["repo_group_name_with_group"] = validated_name
|
||||
|
|
|
|||
|
|
@ -221,12 +221,9 @@ def deferred_unique_name_validator(node, kw):
|
|||
|
||||
@colander.deferred
|
||||
def deferred_repo_name_validator(node, kw):
|
||||
def no_git_suffix_validator(node, value):
|
||||
if value.endswith(".git"):
|
||||
msg = _("Repository name cannot end with .git")
|
||||
raise colander.Invalid(node, msg)
|
||||
|
||||
return colander.All(no_git_suffix_validator, validators.valid_name_validator)
|
||||
return colander.All(
|
||||
validators.no_git_suffix_validator, validators.valid_name_validator, validators.forbidden_repo_name_validator
|
||||
)
|
||||
|
||||
|
||||
@colander.deferred
|
||||
|
|
@ -256,7 +253,7 @@ class GroupType(colander.Mapping):
|
|||
try:
|
||||
return dict(repo_group_name=value)
|
||||
except Exception as e:
|
||||
raise colander.Invalid(node, '"${val}" is not a mapping type: ${err}'.format(val=value, err=e))
|
||||
raise colander.Invalid(node, f'"${value}" is not a mapping type: ${e}')
|
||||
|
||||
def deserialize(self, node, cstruct):
|
||||
if cstruct is colander.null:
|
||||
|
|
@ -341,6 +338,14 @@ class RepoSchema(colander.MappingSchema):
|
|||
types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget()
|
||||
)
|
||||
|
||||
repo_bootstrap_readme = colander.SchemaNode(
|
||||
types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget()
|
||||
)
|
||||
|
||||
repo_bootstrap_svn_branch_struct = colander.SchemaNode(
|
||||
types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget()
|
||||
)
|
||||
|
||||
def deserialize(self, cstruct):
|
||||
"""
|
||||
Custom deserialize that allows to chain validation, and verify
|
||||
|
|
|
|||
|
|
@ -67,7 +67,29 @@ def valid_name_validator(node, value):
|
|||
return
|
||||
|
||||
msg = _("Name must start with a letter or number. Got `{}`").format(value)
|
||||
if not re.match(r"^[a-zA-z0-9]{1,}", value):
|
||||
if not re.match(r"^[a-zA-Z0-9]+", value):
|
||||
raise colander.Invalid(node, msg)
|
||||
|
||||
|
||||
def no_git_suffix_validator(node, value):
|
||||
if value.endswith(".git"):
|
||||
msg = _("Repository name cannot end with .git")
|
||||
raise colander.Invalid(node, msg)
|
||||
|
||||
|
||||
def forbidden_repo_name_validator(node, value):
|
||||
from rhodecode.apps._base import RESERVED_NAMES
|
||||
|
||||
if value in RESERVED_NAMES:
|
||||
msg = _("Repository with name `{}` is forbidden").format(value)
|
||||
raise colander.Invalid(node, msg)
|
||||
|
||||
|
||||
def forbidden_repo_group_name_validator(node, value):
|
||||
from rhodecode.apps._base import RESERVED_NAMES
|
||||
|
||||
if value in RESERVED_NAMES:
|
||||
msg = _("Repository group with name `{}` is forbidden").format(value)
|
||||
raise colander.Invalid(node, msg)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ from sqlalchemy.util import OrderedSet
|
|||
|
||||
from rhodecode.authentication import legacy_plugin_prefix, _import_legacy_plugin
|
||||
from rhodecode.authentication.base import loadplugin
|
||||
from rhodecode.apps._base import ADMIN_PREFIX
|
||||
from rhodecode.apps._base import ADMIN_PREFIX, RESERVED_NAMES
|
||||
from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
|
||||
from rhodecode.lib.utils import repo_name_slug, make_db_config
|
||||
from rhodecode.lib.utils2 import safe_int, str2bool, aslist
|
||||
|
|
@ -280,11 +280,13 @@ def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=Fals
|
|||
|
||||
class _validator(formencode.validators.FancyValidator):
|
||||
messages = {
|
||||
"invalid_repo_group_name": _("Repository group name %(group_name)s is disallowed"),
|
||||
"group_parent_id": _("Cannot assign this group as parent"),
|
||||
"group_exists": _('Group "%(group_name)s" already exists'),
|
||||
"repo_exists": _('Repository with name "%(group_name)s" already exists'),
|
||||
"permission_denied": _("no permission to store repository groupin this location"),
|
||||
"permission_denied": _("no permission to store repository group in this location"),
|
||||
"permission_denied_root": _("no permission to store repository group in root location"),
|
||||
"has_git_suffix": _("Repository group name cannot end with .git"),
|
||||
}
|
||||
|
||||
def _convert_to_python(self, value, state):
|
||||
|
|
@ -313,6 +315,14 @@ def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=Fals
|
|||
if group_parent_id == -1:
|
||||
group_parent_id = None
|
||||
|
||||
if group_name_full in RESERVED_NAMES:
|
||||
msg = M(self, "invalid_repo_group_name", state, group_name=group_name)
|
||||
raise formencode.Invalid(msg, value, state, error_dict={"group_name": msg})
|
||||
|
||||
if group_name_full.endswith(".git"):
|
||||
msg = M(self, "has_git_suffix", state)
|
||||
raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg})
|
||||
|
||||
group_obj = RepoGroup.get(old_data.get("group_id"))
|
||||
parent_group_changed = False
|
||||
if edit:
|
||||
|
|
@ -457,6 +467,7 @@ def ValidRepoName(localizer, edit=False, old_data=None):
|
|||
# inside a group
|
||||
"repository_in_group_exists": _('Repository with name %(repo)s exists in group "%(group)s"'),
|
||||
"group_in_group_exists": _('Repository group with name "%(repo)s" exists in group "%(group)s"'),
|
||||
"has_git_suffix": _("Repository name cannot end with .git"),
|
||||
}
|
||||
|
||||
def _convert_to_python(self, value, state):
|
||||
|
|
@ -486,10 +497,14 @@ def ValidRepoName(localizer, edit=False, old_data=None):
|
|||
group_path = value.get("group_path")
|
||||
group_name = value.get("group_name")
|
||||
|
||||
if repo_name in [ADMIN_PREFIX, ""]:
|
||||
if repo_name_full in RESERVED_NAMES + [""]:
|
||||
msg = M(self, "invalid_repo_name", state, repo=repo_name)
|
||||
raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg})
|
||||
|
||||
if repo_name_full.endswith(".git"):
|
||||
msg = M(self, "has_git_suffix", state)
|
||||
raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg})
|
||||
|
||||
rename = old_data.get("repo_name") != repo_name_full
|
||||
create = not edit
|
||||
if rename or create:
|
||||
|
|
@ -532,25 +547,6 @@ def SlugifyName(localizer):
|
|||
return _validator
|
||||
|
||||
|
||||
def CannotHaveGitSuffix(localizer):
|
||||
_ = localizer
|
||||
|
||||
class _validator(formencode.validators.FancyValidator):
|
||||
messages = {
|
||||
"has_git_suffix": _("Repository name cannot end with .git"),
|
||||
}
|
||||
|
||||
def _convert_to_python(self, value, state):
|
||||
return value
|
||||
|
||||
def _validate_python(self, value, state):
|
||||
if value and value.endswith(".git"):
|
||||
msg = M(self, "has_git_suffix", state)
|
||||
raise formencode.Invalid(msg, value, state, error_dict={"repo_name": msg})
|
||||
|
||||
return _validator
|
||||
|
||||
|
||||
def ValidCloneUri(localizer):
|
||||
_ = localizer
|
||||
|
||||
|
|
|
|||
|
|
@ -3245,3 +3245,55 @@ details:not([open]) > :not(summary) {
|
|||
text-overflow: ellipsis;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.help-block-repo-create {
|
||||
color: @grey4;
|
||||
}
|
||||
|
||||
.repo-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#copy_perms {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 15px 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#checkbox-span {
|
||||
align-items: center;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.repo-create-spacer {
|
||||
margin: 0 6px 0 2px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.full-repo-name-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.repo-check-container {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.repo-check-container.check-progress {
|
||||
color: @grey4;
|
||||
}
|
||||
|
||||
.repo-check-container.check-success {
|
||||
color: @color8;
|
||||
}
|
||||
|
||||
.repo-check-container.check-error {
|
||||
color: @color5;
|
||||
}
|
||||
|
|
@ -359,6 +359,7 @@ function registerRCRoutes() {
|
|||
pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
|
||||
pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
|
||||
pyroutes.register('repo_group_list_data', '/_repo_groups', []);
|
||||
pyroutes.register('repo_group_name_check', '/_admin/repo_group_name_check', []);
|
||||
pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
|
||||
pyroutes.register('repo_groups', '/_admin/repo_groups', []);
|
||||
pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
|
||||
|
|
@ -368,6 +369,7 @@ function registerRCRoutes() {
|
|||
pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
|
||||
pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
|
||||
pyroutes.register('repo_list_data', '/_repos', []);
|
||||
pyroutes.register('repo_name_check', '/_admin/repo_name_check', []);
|
||||
pyroutes.register('repo_new', '/_admin/repos/new', []);
|
||||
pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
|
||||
pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
</%def>
|
||||
|
||||
<%def name="main()">
|
||||
|
||||
<div class="box">
|
||||
${h.secure_form(h.route_path('repo_group_create'), request=request)}
|
||||
<div class="form">
|
||||
|
|
@ -34,24 +35,40 @@
|
|||
<label for="group_name">${_('Group name')}:</label>
|
||||
</div>
|
||||
<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 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="label">
|
||||
<label for="group_description">${_('Description')}:</label>
|
||||
|
|
@ -73,16 +90,6 @@
|
|||
</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">
|
||||
${h.submit('save',_('Create Repository Group'),class_="btn")}
|
||||
</div>
|
||||
|
|
@ -91,31 +98,90 @@
|
|||
${h.end_form()}
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var setCopyPermsOption = function(group_val){
|
||||
if(group_val !== "-1"){
|
||||
$(document).ready(function () {
|
||||
let setCopyPermsOption = function (group_val) {
|
||||
if (group_val !== "-1") {
|
||||
$('#copy_perms').show()
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
$('#copy_perms').hide();
|
||||
}
|
||||
};
|
||||
|
||||
$("#group_parent_id").select2({
|
||||
'containerCssClass': "drop-menu",
|
||||
'dropdownCssClass': "drop-menu-dropdown",
|
||||
'dropdownAutoWidth': true
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
$('#select_my_group').on('click', function(e){
|
||||
$('#select_my_group').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$("#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>
|
||||
</%def>
|
||||
|
|
|
|||
|
|
@ -7,63 +7,22 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
|||
<div class="label">
|
||||
<label for="repo_name">${_('Repository')}:</label>
|
||||
</div>
|
||||
<style>
|
||||
.help-block-repo-create {
|
||||
|
||||
color: #949494;
|
||||
|
||||
}
|
||||
.repo-header-row {
|
||||
display: flex; /* Modern way to align elements inline */
|
||||
align-items: center; /* Keeps text perfectly leveled vertically */
|
||||
gap: 4px; /* Space between the label and the link */
|
||||
margin: 10px 0; /* Space below the entire row */
|
||||
|
||||
}
|
||||
|
||||
#copy_perms {
|
||||
/* Layout & Alignment */
|
||||
display: flex;
|
||||
align-items: center; /* Aligns checkbox with the first line of text */
|
||||
|
||||
/* Spacing for the whole row */
|
||||
margin: 15px 0; /* Space above and below the row */
|
||||
|
||||
/* Internal Spacing */
|
||||
gap: 6px; /* Space specifically between checkbox and text */
|
||||
}
|
||||
|
||||
#checkbox-span {
|
||||
align-items: center; /* Aligns checkbox with the first line of text */
|
||||
padding-top: 2px; /* Micro-adjustment to center checkbox with text line */
|
||||
}
|
||||
|
||||
.help-block-inline {
|
||||
font-size: 14px; /* Standard readable size */
|
||||
line-height: 1.4; /* Improves readability for multi-line text */
|
||||
color: #333; /* Professional dark grey */
|
||||
}
|
||||
|
||||
.repo-create-spacer {
|
||||
margin: 0 4px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div class="input">
|
||||
|
||||
${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
|
||||
<span class="repo-create-spacer"> / </span>
|
||||
${h.text('repo_name', class_="medium", id="repo_name_input")}
|
||||
|
||||
% if c.personal_repo_group:
|
||||
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
|
||||
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
|
||||
</a>
|
||||
% endif
|
||||
<div class="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('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">
|
||||
|
|
@ -85,7 +44,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
|||
</div>
|
||||
</div>
|
||||
|
||||
## TODO: what is this ? i don't think we use it anymore
|
||||
%if not c.rhodecode_user.is_admin:
|
||||
${h.hidden('user_created',True)}
|
||||
%endif
|
||||
|
|
@ -118,9 +76,9 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
|||
<label for="repo_type">${_('Type')}:</label>
|
||||
</div>
|
||||
<div class="fields repo-type-radio">
|
||||
|
||||
% 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"/>
|
||||
% else:
|
||||
<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 class="field repo-bootstrap-items">
|
||||
<div class="field repo-bootstrap-items repo-bootstrap-readme">
|
||||
<div class="label label-checkbox">
|
||||
<label for="repo_bootstrap_readme">${_('Add Readme')}:</label>
|
||||
</div>
|
||||
|
|
@ -178,7 +136,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
|||
</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">
|
||||
<label for="repo_bootstrap_svn_branch_struct">${_('Add SVN structure')}:</label>
|
||||
</div>
|
||||
|
|
@ -195,27 +153,26 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
|||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var setCopyPermsOption = function(group_val){
|
||||
if(group_val != "-1"){
|
||||
$(document).ready(function () {
|
||||
let setCopyPermsOption = function (group_val) {
|
||||
if (group_val !== "-1") {
|
||||
$('#copy_perms').show()
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
$('#copy_perms').hide();
|
||||
}
|
||||
};
|
||||
|
||||
$('#remote_clone_toggle').on('click', function(e){
|
||||
$('#remote_clone_toggle').on('click', function (e) {
|
||||
$('#remote_clone').show();
|
||||
e.preventDefault();
|
||||
$('#remote_clone_toggle').hide();
|
||||
$('.repo-bootstrap-items').remove();
|
||||
});
|
||||
|
||||
if($('#remote_clone input').hasClass('error')){
|
||||
if ($('#remote_clone input').hasClass('error')) {
|
||||
$('#remote_clone').show();
|
||||
}
|
||||
if($('#remote_clone input').val()){
|
||||
if ($('#remote_clone input').val()) {
|
||||
$('#remote_clone').show();
|
||||
}
|
||||
|
||||
|
|
@ -227,29 +184,101 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
|
|||
});
|
||||
|
||||
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();
|
||||
$("#repo_group").val($(this).data('personalGroupId')).trigger("change");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
$('#clone_uri').on('input paste', function(e) {
|
||||
var url = $(this).val().trim();
|
||||
$('#clone_uri').on('input paste', function (e) {
|
||||
let url = $(this).val().trim();
|
||||
if (url) {
|
||||
var parts = url.split('/').filter(Boolean);
|
||||
var lastPart = parts[parts.length - 1];
|
||||
var repoName = lastPart.replace(/\.git$/, "");
|
||||
let parts = url.split('/').filter(Boolean);
|
||||
let lastPart = parts[parts.length - 1];
|
||||
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();
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -548,10 +548,10 @@
|
|||
% endif
|
||||
% endif
|
||||
|
||||
<li class="submenu-title">RhodeCode</li>
|
||||
|
||||
## personal group
|
||||
% if c.rhodecode_user.personal_repo_group:
|
||||
<li class="submenu-title">Personal Group</li>
|
||||
|
||||
<li>
|
||||
<a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
|
||||
</li>
|
||||
|
|
@ -559,25 +559,21 @@
|
|||
<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>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
## Global actions
|
||||
<li class="submenu-title">RhodeCode</li>
|
||||
% if can_create_repos:
|
||||
% else:
|
||||
% if can_create_repos:
|
||||
<li>
|
||||
<a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
|
||||
<a href="${h.route_path('repo_new')}">${_('New Repository')}</a>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if can_create_repo_groups:
|
||||
% endif
|
||||
% if can_create_repo_groups:
|
||||
<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>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
<li>
|
||||
<a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
|
||||
</li>
|
||||
|
||||
</ol>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
ico_data = []
|
||||
for line in source.splitlines():
|
||||
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) == 2:
|
||||
ico_cls, ico_code = line
|
||||
|
|
|
|||
|
|
@ -200,22 +200,28 @@ def test_ValidAuth(localizer, config_stub):
|
|||
pytest.raises(formencode.Invalid, validator.to_python, invalid_creds)
|
||||
|
||||
|
||||
def test_ValidRepoName(localizer):
|
||||
def test_ValidRepoName_empty_name(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)
|
||||
pytest.raises(formencode.Invalid, 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})
|
||||
with pytest.raises(formencode.Invalid) as excinfo:
|
||||
validator.to_python({"repo_name": gr.group_name})
|
||||
|
||||
|
||||
def test_ValidForkName(localizer):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue