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

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

View file

@ -85,7 +85,7 @@ dogpile.cache==1.5.0
decorator==5.1.1
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

View file

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

View file

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

View file

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

View file

@ -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".*?[^/]",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -180,7 +180,7 @@ def get_mailer(transformed_email_conf: dict[str, Any], original_email_conf: dict
return Mailer(**transformed_email_conf)
@async_task(ignore_result=True, base=RequestContextAndUniqueByPayloadTask)
@async_task(ignore_result=False, base=RequestContextAndUniqueByPayloadTask)
def create_repo(form_data, cur_user):
from rhodecode.model.repo import RepoModel
from rhodecode.model.user import UserModel
@ -204,6 +204,9 @@ def create_repo(form_data, cur_user):
fork_of = form_data.get("fork_parent_id")
state = form_data.get("repo_state", Repository.STATE_PENDING)
repo_bootstrap_readme = form_data.get("repo_bootstrap_readme")
repo_bootstrap_svn_branch_struct = form_data.get("repo_bootstrap_svn_branch_struct")
# repo creation defaults, private and repo_type are filled in form
defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
enable_statistics = form_data.get("enable_statistics", defs.get("repo_enable_statistics"))
@ -261,6 +264,14 @@ def create_repo(form_data, cur_user):
repo_id = repo.repo_id
repo_data = repo.get_api_data()
commit = RepoModel().bootstrap(
db_repo=repo,
db_user=cur_user,
readme=repo_bootstrap_readme,
svn_struct=repo_bootstrap_svn_branch_struct,
)
log.debug("Bootstrap commit: %s", commit)
audit_logger.store(
"repo.create",
action_data={"data": repo_data},
@ -289,7 +300,7 @@ def create_repo(form_data, cur_user):
return True
@async_task(ignore_result=True, base=RequestContextAndUniqueByPayloadTask)
@async_task(ignore_result=False, base=RequestContextAndUniqueByPayloadTask)
def create_repo_fork(form_data, cur_user):
"""
Creates a fork of repository using internal VCS methods

View file

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

View file

@ -284,7 +284,6 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab
repo_name = All(
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"))
@ -297,6 +296,9 @@ def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disab
repo_enable_downloads = v.StringBoolean(if_missing=False)
repo_enable_locking = v.StringBoolean(if_missing=False)
repo_bootstrap_readme = v.StringBoolean(if_missing=False)
repo_bootstrap_svn_branch_struct = v.StringBoolean(if_missing=False)
if edit:
# this is repo owner
user = All(v.UnicodeString(not_empty=True), v.ValidRepoUser(localizer, allow_disabled))

View file

@ -27,7 +27,6 @@ from dataclasses import dataclass, field
from enum import StrEnum, auto
from sqlalchemy.orm import aliased
from zope.cachedescriptors.property import Lazy as LazyProperty
from rhodecode import events
from rhodecode.lib.auth import HasUserGroupPermissionAny
@ -47,6 +46,7 @@ from rhodecode.lib.utils2 import (
action_logger_generic,
)
from rhodecode.lib.vcs.backends import get_backend
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.nodes import NodeKind
from rhodecode.model import BaseModel
from rhodecode.model.db import (
@ -69,6 +69,7 @@ from rhodecode.model.db import (
RepoGroup,
RepositoryField,
UserLog,
RhodeCodeUi,
)
from rhodecode.model.permission import PermissionModel
@ -677,6 +678,57 @@ class RepoModel(BaseModel):
return run_task(tasks.create_repo, form_data, cur_user)
def bootstrap(self, db_repo, db_user, readme=None, svn_struct=None):
"""
Bootstrap new created repo with some files/structure
"""
from rhodecode.model.scm import ScmModel
from rhodecode.model.settings import SettingsModel
_branch_name, _sha_commit_id, is_head = db_repo.landing_ref_name, "", True
author = db_user.full_contact
repo_name = db_repo.repo_name
message = "Bootstrap: created initial project files"
commit = EmptyCommit(alias=db_repo.repo_type, branch=_branch_name)
rc_config = SettingsModel().get_all_settings(cache=True, from_request=False)
default_renderer = rc_config.get("rhodecode_markup_renderer", "rst")
nodes = {}
if readme:
if default_renderer == "markdown":
ext = "md"
readme_content = f"# {repo_name}\n"
elif default_renderer == "rst":
ext = "rst"
readme_content = f"{repo_name}\n" + "-" * len(repo_name) + "\n"
else:
ext = ".txt"
readme_content = f"{repo_name}\n"
nodes[safe_bytes(f"readme.{ext}")] = {"content": safe_bytes(readme_content)}
if svn_struct and db_repo.repo_type == "svn":
for branch in RhodeCodeUi.SVN_BRANCHES_PATTERNS:
branch_dir = branch.lstrip("/").rstrip("*").rstrip("/")
nodes[safe_bytes(os.path.join(branch_dir, ".dirkeep"))] = {"content": b""}
for tag in RhodeCodeUi.SVN_TAGS_PATTERNS:
tag_dir = tag.lstrip("/").rstrip("*").rstrip("/")
nodes[safe_bytes(os.path.join(tag_dir, ".dirkeep"))] = {"content": b""}
if nodes:
commit = ScmModel().create_nodes(
user=db_user.user_id,
repo=db_repo,
message=message,
nodes=nodes,
parent_commit=commit,
author=author,
)
return commit
return None
def update_permissions(
self, repo, perm_additions=None, perm_updates=None, perm_deletions=None, check_perms=True, cur_user=None
):

View file

@ -150,7 +150,7 @@ def deferred_unique_name_validator(node, kw):
@colander.deferred
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

View file

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

View file

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

View file

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

View file

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

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_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']);

View file

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

View file

@ -5,16 +5,49 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<div class="fields">
<div class="field">
<div class="label">
<label for="repo_name">${_('Repository name')}:</label>
<label for="repo_name">${_('Repository')}:</label>
</div>
<div class="input">
${h.text('repo_name', class_="medium")}
<div class="info-block">
<a id="remote_clone_toggle" href="#">${_('Import Existing Repository ?')}</a>
<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">
${_('Repository Group / Repository Name')}.
</span>
<div class="import-repo">
<a id="remote_clone_toggle" href="#">${_('Import an existing repository.')}</a>
</div>
</div>
## COPY PERMS
<div id="copy_perms">
<div id="checkbox-span">
${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
</div>
<div class="help-block-inline">
${_('Copy permissions from parent repository group.')}
</div>
</div>
%if not c.rhodecode_user.is_admin:
${h.hidden('user_created',True)}
%endif
</div>
</div>
<div id="remote_clone" class="field" style="display: none;">
@ -37,30 +70,15 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</span>
</div>
</div>
<div class="field">
<div class="label">
<label for="repo_group">${_('Repository group')}:</label>
</div>
<div class="select">
${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
% if c.personal_repo_group:
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
</a>
% endif
<span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
</div>
</div>
<div class="field">
<div class="label">
<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}" />
@ -73,7 +91,6 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
% endfor
<span class="help-block">${_('Set the type of repository to create.')}</span>
</div>
</div>
@ -98,15 +115,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
</span>
</div>
</div>
<div id="copy_perms" class="field">
<div class="label label-checkbox">
<label for="repo_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
<span class="help-block">${_('Copy permissions from parent repository group.')}</span>
</div>
</div>
<div class="field">
<div class="label label-checkbox">
<label for="repo_private">${_('Private Repository')}:</label>
@ -116,31 +125,54 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
<span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
</div>
</div>
<div class="field repo-bootstrap-items repo-bootstrap-readme">
<div class="label label-checkbox">
<label for="repo_bootstrap_readme">${_('Add Readme')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('repo_bootstrap_readme',value="True")}
<span class="help-block">${_('Initialize repository with a README. Allows you to immediately clone this projects repository.')}</span>
</div>
</div>
<div class="field repo-bootstrap-items repo-bootstrap-svn" style="display: none">
<div class="label label-checkbox">
<label for="repo_bootstrap_svn_branch_struct">${_('Add SVN structure')}:</label>
</div>
<div class="checkboxes">
${h.checkbox('repo_bootstrap_svn_branch_struct',value="True")}
<span class="help-block">${_('Initialize repository with a branches/tags DIRS')}</span>
</div>
</div>
<div class="buttons">
${h.submit('save',_('Create Repository'),class_="btn")}
</div>
</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();
}
@ -152,15 +184,102 @@ ${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) {
let url = $(this).val().trim();
if (url) {
let parts = url.split('/').filter(Boolean);
let lastPart = parts[parts.length - 1];
let repoName = lastPart.replace(/\.git$/, "");
$('#repo_name_input').val(repoName).trigger('input');
} else {
$('#repo_name_input').val("").trigger('input');
}
});
let defaultVcsBackend = "${c.default_backend}";
if (defaultVcsBackend === "svn") {
$('.repo-bootstrap-svn').show();
}
$('input[name="repo_type"]').on('change', function (e) {
let selectedVcs = $(this).val();
if (selectedVcs === "svn") {
$('.repo-bootstrap-svn').show();
} else {
$('.repo-bootstrap-svn').hide();
}
})
})

View file

@ -548,10 +548,10 @@
% endif
% endif
<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>

View file

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

View file

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