Setting for default branch and creating new repo+commits on empty repo

This commit is contained in:
Andrii V 2025-10-24 16:20:56 +02:00
parent 7d2a9d1d48
commit c67bb94291
9 changed files with 195 additions and 16 deletions

View file

@ -1293,7 +1293,7 @@ class RepoFilesView(RepoAppView):
c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
if c.commit is None:
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias, branch=self.db_repo.landing_ref_name)
if self.rhodecode_vcs_repo.is_empty():
# for empty repository we cannot check for current branch, we rely on
@ -1329,7 +1329,7 @@ class RepoFilesView(RepoAppView):
c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
if c.commit is None:
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias, branch=self.db_repo.landing_ref_name)
# calculate redirect URL
if self.rhodecode_vcs_repo.is_empty():
@ -1415,7 +1415,7 @@ class RepoFilesView(RepoAppView):
c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
if c.commit is None:
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias, branch=self.db_repo.landing_ref_name)
# calculate redirect URL
if self.rhodecode_vcs_repo.is_empty():
@ -1517,7 +1517,7 @@ class RepoFilesView(RepoAppView):
c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
if c.commit is None:
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias, branch=self.db_repo.landing_ref_name)
if self.rhodecode_vcs_repo.is_empty():
default_redirect_url = h.route_path("repo_summary", repo_name=self.db_repo_name)

View file

@ -187,7 +187,16 @@ def create_repo(form_data, cur_user):
enable_downloads = form_data.get("enable_downloads", defs.get("repo_enable_downloads"))
# set landing rev based on default branches for SCM
landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
if repo_type == "git":
# Get default branch from global Git settings
from rhodecode.model.settings import VcsSettingsModel
settings_model = VcsSettingsModel()
global_settings = settings_model.get_global_settings()
default_branch = global_settings.get("vcs_git_default_branch", "master")
landing_ref = f"branch:{default_branch}"
else:
landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
try:
RepoModel()._create_repo(

View file

@ -0,0 +1,53 @@
from rhodecode.lib.dbmigrate.versions import _reset_base
from rhodecode.model import meta, init_model_encryption
def upgrade(migrate_engine):
"""
Add Git default branch configuration
"""
_reset_base(migrate_engine)
from rhodecode.lib.dbmigrate.schema import db_4_20_0_1 as db
init_model_encryption(db)
from rhodecode.model.db import RhodeCodeUi
session = meta.Session()
# Check if setting already exists
existing = (
session.query(RhodeCodeUi)
.filter(RhodeCodeUi.ui_section == "vcs_git", RhodeCodeUi.ui_key == "default_branch")
.first()
)
if not existing:
# Add default branch setting with 'master' as default
setting = RhodeCodeUi()
setting.ui_section = "vcs_git"
setting.ui_key = "default_branch"
setting.ui_value = "master"
setting.ui_active = True
session.add(setting)
session.commit()
def downgrade(migrate_engine):
"""
Remove Git default branch configuration
"""
_reset_base(migrate_engine)
from rhodecode.lib.dbmigrate.schema import db_4_20_0_1 as db
init_model_encryption(db)
from rhodecode.model.db import RhodeCodeUi
session = meta.Session()
session.query(RhodeCodeUi).filter(
RhodeCodeUi.ui_section == "vcs_git", RhodeCodeUi.ui_key == "default_branch"
).delete()
session.commit()

View file

@ -1671,7 +1671,9 @@ class EmptyCommit(BaseCommit):
an EmptyCommit
"""
def __init__(self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1, message="", author="", date=None):
def __init__(
self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1, message="", author="", date=None, branch=None
):
self._empty_commit_id = commit_id
# TODO: johbo: Solve idx parameter, default value does not make
# too much sense
@ -1681,6 +1683,7 @@ class EmptyCommit(BaseCommit):
self.date = date or datetime.datetime.fromtimestamp(0)
self.repository = repo
self.alias = alias
self._branch = branch
@LazyProperty
def raw_id(self):
@ -1693,6 +1696,10 @@ class EmptyCommit(BaseCommit):
@LazyProperty
def branch(self):
# If branch was explicitly set, return it
if self._branch:
return self._branch
if self.alias:
from rhodecode.lib.vcs.backends import get_backend

View file

@ -452,6 +452,15 @@ class _BaseVcsSettingsForm(formencode.Schema):
# git
vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
vcs_git_default_branch = v.Regex(
r"^(?!\.)(?!.*\.\.)(?!.*[~^:?*\[\\\s])(?!.*//)(?!.*/$)(?!.*\.lock$).+$",
not_empty=False,
if_missing="master",
messages={
"invalid": "Invalid branch name. Branch names cannot: start with '.', contain '..', "
"contain special characters (~^:?*[\\), end with '/', contain '//', or end with '.lock'."
},
)
rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
rhodecode_git_merge_strategy_selector = v.StringBoolean(if_missing=False)

View file

@ -562,8 +562,19 @@ class RepoModel(BaseModel):
owner = self._get_user(owner)
fork_of = self._get_repo(fork_of)
repo_group = self._get_repo_group(safe_int(repo_group))
default_landing_ref, _lbl = ScmModel.backend_landing_ref(repo_type)
landing_rev = landing_rev or default_landing_ref
if repo_type == "git" and not landing_rev:
# Get default branch from global Git settings
from rhodecode.model.settings import VcsSettingsModel
settings_model = VcsSettingsModel()
global_settings = settings_model.get_global_settings()
default_branch = global_settings.get("vcs_git_default_branch", "master")
landing_rev = f"branch:{default_branch}"
else:
# Other VCS types or explicit landing_rev provided
default_landing_ref, _lbl = ScmModel.backend_landing_ref(repo_type)
landing_rev = landing_rev or default_landing_ref
try:
repo_name = safe_str(repo_name)

View file

@ -479,7 +479,10 @@ class VcsSettingsModel(object):
("experimental", "evolution"),
("experimental", "evolution.exchange"),
)
GIT_SETTINGS = (("vcs_git_lfs", "enabled"),)
GIT_SETTINGS = (
("vcs_git_lfs", "enabled"),
("vcs_git", "default_branch"),
)
GLOBAL_HG_SETTINGS = (
("extensions", "largefiles"),
("phases", "publish"),
@ -489,7 +492,10 @@ class VcsSettingsModel(object):
("experimental", "evolution.exchange"),
)
GLOBAL_GIT_SETTINGS = (("vcs_git_lfs", "enabled"),)
GLOBAL_GIT_SETTINGS = (
("vcs_git_lfs", "enabled"),
("vcs_git", "default_branch"),
)
SVN_BRANCH_SECTION = "vcs_svn_branch"
SVN_TAG_SECTION = "vcs_svn_tag"
@ -637,23 +643,29 @@ class VcsSettingsModel(object):
self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
def create_or_update_repo_git_settings(self, data):
# NOTE(marcink): # comma makes unpack work properly
(lfs_enabled,) = self.GIT_SETTINGS
(lfs_enabled_key,) = self._get_settings_keys(self.GIT_SETTINGS, data)
lfs_enabled, default_branch = self.GIT_SETTINGS
lfs_enabled_key, default_branch_key = self._get_settings_keys(self.GIT_SETTINGS, data)
self._create_or_update_ui(
self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key], active=data[lfs_enabled_key]
)
self._create_or_update_ui(
self.repo_settings, *default_branch, value=data[default_branch_key] or "master", active=True
)
def create_or_update_global_git_settings(self, data):
lfs_enabled = self.GLOBAL_GIT_SETTINGS[0]
lfs_enabled_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)[0]
lfs_enabled, default_branch = self.GLOBAL_GIT_SETTINGS
lfs_enabled_key, default_branch_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
self._create_or_update_ui(
self.global_settings, *lfs_enabled, value=data[lfs_enabled_key], active=data[lfs_enabled_key]
)
self._create_or_update_ui(
self.global_settings, *default_branch, value=data[default_branch_key] or "master", active=True
)
def create_or_update_global_svn_settings(self, data):
# branch/tags patterns
self._create_svn_settings(self.global_settings, data)

View file

@ -101,6 +101,20 @@
<span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
% endif
</div>
% if display_globals:
<div class="field">
<div class="label">
<label for="vcs_git_default_branch${suffix}">${_('Default branch name')}</label>
</div>
<div class="input">
${h.text('vcs_git_default_branch' + suffix, size=30, **kwargs)}
</div>
<div class="label">
<span class="help-block">${_('Default branch name for new Git repositories. Common values: "main", "master", "develop". Defaults to "master" if empty.')}</span>
</div>
</div>
% endif
</div>
</div>
% endif

View file

@ -561,6 +561,7 @@ class TestCreateOrUpdateGlobalHgSettings(object):
class TestCreateOrUpdateGlobalGitSettings(object):
FORM_DATA = {
"vcs_git_lfs_enabled": False,
"vcs_git_default_branch": "main",
}
def test_creates_repo_hg_settings_when_data_is_correct(self):
@ -571,9 +572,72 @@ class TestCreateOrUpdateGlobalGitSettings(object):
expected_calls = [
mock.call(model.global_settings, "vcs_git_lfs", "enabled", active=False, value=False),
mock.call(model.global_settings, "vcs_git", "default_branch", active=True, value="main"),
]
assert expected_calls == create_mock.call_args_list
def test_default_branch_falls_back_to_master_when_empty(self):
model = VcsSettingsModel()
form_data = {
"vcs_git_lfs_enabled": False,
"vcs_git_default_branch": "",
}
with mock.patch.object(model, "_create_or_update_ui") as create_mock:
model.create_or_update_global_git_settings(form_data)
Session().commit()
expected_calls = [
mock.call(model.global_settings, "vcs_git_lfs", "enabled", active=False, value=False),
mock.call(model.global_settings, "vcs_git", "default_branch", active=True, value="master"),
]
assert expected_calls == create_mock.call_args_list
def test_valid_branch_names_are_accepted(self):
"""Test that valid Git branch names pass validation"""
import formencode
from rhodecode.model.forms import ApplicationSettingsForm
valid_names = ["main", "develop", "feature/my-feature", "release-1.0", "hotfix_urgent"]
for branch_name in valid_names:
form_data = {
"vcs_git_lfs_enabled": False,
"vcs_git_default_branch": branch_name,
}
schema = ApplicationSettingsForm(lambda s: s)()
result = schema.to_python(form_data)
assert result["vcs_git_default_branch"] == branch_name
def test_invalid_branch_names_are_rejected(self):
"""Test that invalid Git branch names fail validation"""
import formencode
from rhodecode.model.forms import ApplicationSettingsForm
invalid_names = [
".hidden", # starts with dot
"feature..bug", # contains double dots
"feature~1", # contains tilde
"branch:name", # contains colon
"what?", # contains question mark
"wild*card", # contains asterisk
"feature[test]", # contains brackets
"trailing/", # ends with slash
"double//slash", # contains double slash
"feature.lock", # ends with .lock
]
schema = ApplicationSettingsForm(lambda s: s)()
for branch_name in invalid_names:
form_data = {
"vcs_git_lfs_enabled": False,
"vcs_git_default_branch": branch_name,
}
try:
schema.to_python(form_data)
assert False, f"Expected validation error for branch name: {branch_name}"
except formencode.Invalid:
pass # Expected
class TestDeleteRepoSvnPattern(object):
def test_success_when_repo_is_set(self, backend_svn, settings_util):