diff --git a/app/models/project.rb b/app/models/project.rb index 224193fba0830554c355d199c5c130af5a47abf5..8ad45e5b87d2bcc8458923c87ca00cef230f58f9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2871,7 +2871,13 @@ def deploy_token_revoke_url_for(token) def default_branch_protected? branch_protection = Gitlab::Access::BranchProtection.new(self.namespace.default_branch_protection) - branch_protection.fully_protected? || branch_protection.developer_can_merge? + branch_protection.fully_protected? || branch_protection.developer_can_merge? || branch_protection.developer_can_initial_push? + end + + def initial_push_to_default_branch_allowed_for_developer? + branch_protection = Gitlab::Access::BranchProtection.new(self.namespace.default_branch_protection) + + !branch_protection.any? || branch_protection.developer_can_push? || branch_protection.developer_can_initial_push? end def environments_for_scope(scope) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 09a0cfc91dc6126f2cd8750811fc4eaec840ff13..aebce59a040045d9af45b6bdc3842913c0364db1 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -26,10 +26,16 @@ def self.get_ids_by_name(name) end def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) - # Maintainers, owners and admins are allowed to create the default branch + if project.empty_repo? + member_access = project.team.max_member_access(user.id) - if project.empty_repo? && project.default_branch_protected? + # Admins are always allowed to create the default branch return true if user.admin? || user.can?(:admin_project, project) + + # Developers can push if it is allowed by default branch protection settings + if member_access == Gitlab::Access::DEVELOPER && project.initial_push_to_default_branch_allowed_for_developer? + return true + end end super diff --git a/doc/api/groups.md b/doc/api/groups.md index 6d295b50a01a30b6e1a040fd4e0df5c394ad32a6..be663a6a9c558118d6f396b65b08632e02de8c3f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -834,6 +834,7 @@ The `default_branch_protection` attribute determines whether users with the Deve | `1` | Partial protection. Users with the Developer or Maintainer role can:
- Push new commits | | `2` | Full protection. Only users with the Maintainer role can:
- Push new commits | | `3` | Protected against pushes. Users with the Maintainer role can:
- Push new commits
- Force push changes
- Accept merge requests
Users with the Developer role can:
- Accept merge requests| +| `4` | Protected against pushes except initial push. User with the Developer rope can:
- Push commit to empty repository.
Users with the Maintainer role can:
- Push new commits
- Force push changes
- Accept merge requests
Users with the Developer role can:
- Accept merge requests| ## New Subgroup diff --git a/doc/user/project/repository/branches/default.md b/doc/user/project/repository/branches/default.md index e58cc0bf6a482010003821d976fc24d9261388c0..96f5f6887d9b4497faa879f1c2c87520d4808d58 100644 --- a/doc/user/project/repository/branches/default.md +++ b/doc/user/project/repository/branches/default.md @@ -95,6 +95,8 @@ unless a subgroup configuration overrides it. ## Protect initial default branches **(FREE SELF)** +> Full protection after initial push [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118729) in GitLab 16.0. + GitLab administrators and group owners can define [branch protections](../../../project/protected_branches.md) to apply to every repository's [default branch](#default-branch) at the [instance level](#instance-level-default-branch-protection) and @@ -108,6 +110,8 @@ at the [instance level](#instance-level-default-branch-protection) and but cannot force push. - **Fully protected** - Developers cannot push new commits, but maintainers can. No one can force push. +- **Fully protected after initial push** - Developers can push the initial commit + to a repository, but none afterward. Maintainers can always push. No one can force push. ### Instance-level default branch protection **(FREE SELF)** diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index bafda11170aa9471efbf45ad61322222439f58ae..f1777e059ed04de6253131d58955af0116acc527 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -23,6 +23,7 @@ module Access PROTECTION_DEV_CAN_PUSH = 1 PROTECTION_FULL = 2 PROTECTION_DEV_CAN_MERGE = 3 + PROTECTION_DEV_CAN_INITIAL_PUSH = 4 # Default project creation level NO_ONE_PROJECT_ACCESS = 0 @@ -95,6 +96,11 @@ def protection_options label: s_('DefaultBranchProtection|Fully protected'), help_text: s_('DefaultBranchProtection|Developers cannot push new commits, but maintainers can. No one can force push.'), value: PROTECTION_FULL + }, + { + label: s_('DefaultBranchProtection|Fully protected after initial push'), + help_text: s_('DefaultBranchProtection|Developers can push the initial commit to a repository, but none afterward. Maintainers can always push. No one can force push.'), + value: PROTECTION_DEV_CAN_INITIAL_PUSH } ] end diff --git a/lib/gitlab/access/branch_protection.rb b/lib/gitlab/access/branch_protection.rb index 339a99eb06868505cf9cacc3c1764c8c7043f3da..6ac8de407b0056ac4d940f73f7daeb4d7b11523b 100644 --- a/lib/gitlab/access/branch_protection.rb +++ b/lib/gitlab/access/branch_protection.rb @@ -34,6 +34,10 @@ def developer_can_push? level == PROTECTION_DEV_CAN_PUSH end + def developer_can_initial_push? + level == PROTECTION_DEV_CAN_INITIAL_PUSH + end + def developer_can_merge? level == PROTECTION_DEV_CAN_MERGE end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 84729dba6ef8c8bfda45c2cc6322d2f44ced038e..8660e77a6628c4e083cd21dc466ca7e8f21e75a6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14275,6 +14275,9 @@ msgstr "" msgid "DefaultBranchProtection|Both developers and maintainers can push new commits, force push, or delete the branch." msgstr "" +msgid "DefaultBranchProtection|Developers can push the initial commit to a repository, but none afterward. Maintainers can always push. No one can force push." +msgstr "" + msgid "DefaultBranchProtection|Developers cannot push new commits, but are allowed to accept merge requests to the branch. Maintainers can push to the branch." msgstr "" @@ -14284,6 +14287,9 @@ msgstr "" msgid "DefaultBranchProtection|Fully protected" msgstr "" +msgid "DefaultBranchProtection|Fully protected after initial push" +msgstr "" + msgid "DefaultBranchProtection|Not protected" msgstr "" diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb index 44c30d1f596cb84d41aebfa7170db240754f9ec3..5ab610dfc8fd8113b62703f29bcf26d2071d62b6 100644 --- a/spec/lib/gitlab/access/branch_protection_spec.rb +++ b/spec/lib/gitlab/access/branch_protection_spec.rb @@ -7,10 +7,11 @@ describe '#any?' do where(:level, :result) do - Gitlab::Access::PROTECTION_NONE | false - Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true - Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true - Gitlab::Access::PROTECTION_FULL | true + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | true + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | true end with_them do @@ -20,10 +21,11 @@ describe '#developer_can_push?' do where(:level, :result) do - Gitlab::Access::PROTECTION_NONE | false - Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true - Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false - Gitlab::Access::PROTECTION_FULL | false + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | false + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | false end with_them do @@ -35,10 +37,11 @@ describe '#developer_can_merge?' do where(:level, :result) do - Gitlab::Access::PROTECTION_NONE | false - Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false - Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true - Gitlab::Access::PROTECTION_FULL | false + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | false + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | false end with_them do @@ -50,10 +53,11 @@ describe '#fully_protected?' do where(:level, :result) do - Gitlab::Access::PROTECTION_NONE | false - Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false - Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false - Gitlab::Access::PROTECTION_FULL | true + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | true + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | false end with_them do @@ -62,4 +66,20 @@ end end end + + describe '#developer_can_initial_push?' do + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | false + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | true + end + + with_them do + it do + expect(described_class.new(level).developer_can_initial_push?).to eq(result) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e9bb01f4b230302f31acf5a392878b479f437cc8..0f6580529e8a1cd5370419564d1cf88f40efda39 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2684,10 +2684,34 @@ def has_external_wiki subject { project.default_branch_protected? } where(:default_branch_protection_level, :result) do - Gitlab::Access::PROTECTION_NONE | false - Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false - Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true - Gitlab::Access::PROTECTION_FULL | true + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | true + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | true + end + + with_them do + before do + expect(project.namespace).to receive(:default_branch_protection).and_return(default_branch_protection_level) + end + + it { is_expected.to eq(result) } + end + end + + describe 'initial_push_to_default_branch_allowed_for_developer?' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, namespace: namespace) } + + subject { project.initial_push_to_default_branch_allowed_for_developer? } + + where(:default_branch_protection_level, :result) do + Gitlab::Access::PROTECTION_NONE | true + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | false + Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | true end with_them do diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index d14a7dd1a7e7a030fea1dac005ff5b7d430c5e10..b8357fc30b891ea06d41735fb1338a4396363ef7 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -507,6 +507,44 @@ it { is_expected.to eq(true) } end + + context 'when project is an empty repository' do + before do + allow(project).to receive(:empty_repo?).and_return(true) + end + + context 'when user is an admin' do + let(:current_user) { admin } + + it { is_expected.to eq(true) } + end + + context 'when user is maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to eq(true) } + end + + context 'when user is developer and initial push is allowed' do + let(:current_user) { developer } + + before do + allow(project).to receive(:initial_push_to_default_branch_allowed_for_developer?).and_return(true) + end + + it { is_expected.to eq(true) } + end + + context 'when user is developer and initial push is not allowed' do + let(:current_user) { developer } + + before do + allow(project).to receive(:initial_push_to_default_branch_allowed_for_developer?).and_return(false) + end + + it { is_expected.to eq(false) } + end + end end describe '.by_name' do