From c67bb94291106bf5769269d3c3b868800d7145dd Mon Sep 17 00:00:00 2001 From: Andrii V Date: Fri, 24 Oct 2025 16:20:56 +0200 Subject: [PATCH 1/3] Setting for default branch and creating new repo+commits on empty repo --- rhodecode/apps/repository/views/repo_files.py | 8 +-- rhodecode/lib/celerylib/tasks.py | 11 +++- .../dbmigrate/versions/120_version_5_8_1.py | 53 +++++++++++++++ rhodecode/lib/vcs/backends/base.py | 9 ++- rhodecode/model/forms.py | 9 +++ rhodecode/model/repo.py | 15 ++++- rhodecode/model/settings.py | 28 +++++--- rhodecode/templates/base/vcs_settings.mako | 14 ++++ .../models/settings/test_vcs_settings.py | 64 +++++++++++++++++++ 9 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 rhodecode/lib/dbmigrate/versions/120_version_5_8_1.py diff --git a/rhodecode/apps/repository/views/repo_files.py b/rhodecode/apps/repository/views/repo_files.py index 54338235..8505fabb 100644 --- a/rhodecode/apps/repository/views/repo_files.py +++ b/rhodecode/apps/repository/views/repo_files.py @@ -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) diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py index a7626c6f..43f16ce8 100644 --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -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( diff --git a/rhodecode/lib/dbmigrate/versions/120_version_5_8_1.py b/rhodecode/lib/dbmigrate/versions/120_version_5_8_1.py new file mode 100644 index 00000000..5030296a --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/120_version_5_8_1.py @@ -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() diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py index 15aca4be..0072280a 100644 --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -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 diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index f1245d5a..ee5258be 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -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) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index d8e90201..9f24367d 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -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) diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py index 8838ce7a..fb277f2b 100644 --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -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) diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako index 924e17b6..28456469 100644 --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -101,6 +101,20 @@ ${_('Enable lfs extensions for this repository.')} % endif + + % if display_globals: +
+
+ +
+
+ ${h.text('vcs_git_default_branch' + suffix, size=30, **kwargs)} +
+
+ ${_('Default branch name for new Git repositories. Common values: "main", "master", "develop". Defaults to "master" if empty.')} +
+
+ % endif % endif diff --git a/rhodecode/tests/models/settings/test_vcs_settings.py b/rhodecode/tests/models/settings/test_vcs_settings.py index 63fe3e22..0d81518e 100644 --- a/rhodecode/tests/models/settings/test_vcs_settings.py +++ b/rhodecode/tests/models/settings/test_vcs_settings.py @@ -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): From 9aff70f2369ac101df18fe9a8c9e5f9aa0b191b9 Mon Sep 17 00:00:00 2001 From: Andrii V Date: Tue, 28 Oct 2025 19:56:02 +0100 Subject: [PATCH 2/3] Fix UI layout and fixed tests --- rhodecode/templates/base/vcs_settings.mako | 6 ++-- .../models/settings/test_vcs_settings.py | 32 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako index 28456469..8ad99b35 100644 --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -110,9 +110,9 @@
${h.text('vcs_git_default_branch' + suffix, size=30, **kwargs)}
-
- ${_('Default branch name for new Git repositories. Common values: "main", "master", "develop". Defaults to "master" if empty.')} -
+ +
+ ${_('Default branch name for new Git repositories. Common values: "main", "master", "develop". Defaults to "master" if empty.')}
% endif diff --git a/rhodecode/tests/models/settings/test_vcs_settings.py b/rhodecode/tests/models/settings/test_vcs_settings.py index 0d81518e..16933039 100644 --- a/rhodecode/tests/models/settings/test_vcs_settings.py +++ b/rhodecode/tests/models/settings/test_vcs_settings.py @@ -595,23 +595,24 @@ class TestCreateOrUpdateGlobalGitSettings(object): def test_valid_branch_names_are_accepted(self): """Test that valid Git branch names pass validation""" import formencode - from rhodecode.model.forms import ApplicationSettingsForm + from rhodecode.model.forms import v valid_names = ["main", "develop", "feature/my-feature", "release-1.0", "hotfix_urgent"] + validator = v.Regex( + r"^(?!\.)(?!.*\.\.)(?!.*[~^:?*\[\\\s])(?!.*//)(?!.*/$)(?!.*\.lock$).+$", + not_empty=False, + if_missing="master", + ) + 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 + result = validator.to_python(branch_name) + assert result == 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 + from rhodecode.model.forms import v invalid_names = [ ".hidden", # starts with dot @@ -626,14 +627,15 @@ class TestCreateOrUpdateGlobalGitSettings(object): "feature.lock", # ends with .lock ] - schema = ApplicationSettingsForm(lambda s: s)() + validator = v.Regex( + r"^(?!\.)(?!.*\.\.)(?!.*[~^:?*\[\\\s])(?!.*//)(?!.*/$)(?!.*\.lock$).+$", + not_empty=False, + if_missing="master", + ) + 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) + validator.to_python(branch_name) assert False, f"Expected validation error for branch name: {branch_name}" except formencode.Invalid: pass # Expected From 06e61cee19778808c86bc98735fb10833262ee6d Mon Sep 17 00:00:00 2001 From: Andrii V Date: Fri, 31 Oct 2025 16:59:54 +0100 Subject: [PATCH 3/3] Small fix for unpacking --- rhodecode/model/settings.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py index fb277f2b..51d57f29 100644 --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -643,8 +643,15 @@ class VcsSettingsModel(object): self._set_evolution(self.global_settings, is_enabled=data[evolve_key]) def create_or_update_repo_git_settings(self, data): - lfs_enabled, default_branch = self.GIT_SETTINGS - lfs_enabled_key, default_branch_key = self._get_settings_keys(self.GIT_SETTINGS, data) + # NOTE(marcink): # comma makes unpack work properly + ( + 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] @@ -655,8 +662,15 @@ class VcsSettingsModel(object): ) def create_or_update_global_git_settings(self, data): - lfs_enabled, default_branch = self.GLOBAL_GIT_SETTINGS - lfs_enabled_key, default_branch_key = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data) + # NOTE(marcink): # comma makes unpack work properly + ( + 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]