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