diff --git a/ee/app/controllers/admin/push_rules_controller.rb b/ee/app/controllers/admin/push_rules_controller.rb index 6ce7ac3e5602b204bf18adcfc10b9ab1edce953c..bfe2aad4564e3f9e0ee8d8874163d7eb3e433957 100644 --- a/ee/app/controllers/admin/push_rules_controller.rb +++ b/ee/app/controllers/admin/push_rules_controller.rb @@ -23,6 +23,13 @@ def update end end + def sync_all + attributes = @push_rule.attributes.slice(*PushRule.column_names).except("id", "is_sample") + PushRule.normal.update_all(attributes) + + render :show + end + private def check_push_rules_available! diff --git a/ee/app/controllers/groups/push_rules_controller.rb b/ee/app/controllers/groups/push_rules_controller.rb index d1d732eace4f51148fc01ef7a1c0d50ec22d21f5..edd298d099cf0d3b88f63ca6c1932e21d8d13575 100644 --- a/ee/app/controllers/groups/push_rules_controller.rb +++ b/ee/app/controllers/groups/push_rules_controller.rb @@ -27,6 +27,15 @@ def update redirect_to group_settings_repository_path(group, anchor: 'js-push-rules') end + def sync_all + if push_rule.persisted? + attributes = push_rule.attributes.slice(*PushRule.column_names).except("id", "is_sample", "project_id") + group.all_cascade_down_push_rules.update_all(attributes) + end + + redirect_to group_settings_repository_path(group, anchor: 'js-push-rules') + end + private def push_rule_params diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 856c3ba43d3278c2cd4417a04f5f9e03046cc8f5..653fbdf651248c22a9bbd10f7d20b060ffa21cc3 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -695,6 +695,12 @@ def parent_epic_ids_in_ancestor_groups ids.to_a end + def all_cascade_down_push_rules + ids = descendants.pluck(:push_rule_id) + ids += all_projects.map { |project| project.push_rule&.id } + PushRule.where(id: ids.compact) + end + private override :post_create_hook diff --git a/ee/app/models/push_rule.rb b/ee/app/models/push_rule.rb index 48e333da6b5f032fcb12415b3c681d2ac8032862..7cfcb17d4e9ef6044b921548ef8279dfc0c75470 100644 --- a/ee/app/models/push_rule.rb +++ b/ee/app/models/push_rule.rb @@ -51,6 +51,8 @@ class PushRule < ApplicationRecord DCO_COMMIT_REGEX = 'Signed-off-by:.+<.+@.+>' + scope :normal, -> { where(is_sample: false) } + def self.global find_by(is_sample: true) end diff --git a/ee/app/views/admin/push_rules/_push_rules.html.haml b/ee/app/views/admin/push_rules/_push_rules.html.haml index 881c3bc1dc749ad8e8511438b5243805b8dfa809..d22bf74147d27ef111521169013823aaa2464f8a 100644 --- a/ee/app/views/admin/push_rules/_push_rules.html.haml +++ b/ee/app/views/admin/push_rules/_push_rules.html.haml @@ -5,3 +5,6 @@ - @push_rule.errors.full_messages.each do |msg| %p= msg = render "shared/push_rules/form", f: f, context: nil + = render Pajamas::ButtonComponent.new(type: :submit, variant: :danger, method: :put, + href: sync_all_admin_push_rule_path, button_options: { title: s_('PushRules|Apply to all push rules') } ) do + = s_('PushRules|Apply to all push rules') diff --git a/ee/app/views/groups/settings/repository/_push_rules.html.haml b/ee/app/views/groups/settings/repository/_push_rules.html.haml index 2a000eac263f0bc4ac7d1f8036c93d398c641744..85ad22c61e298aa44a4b9514de6bbe9bde64128f 100644 --- a/ee/app/views/groups/settings/repository/_push_rules.html.haml +++ b/ee/app/views/groups/settings/repository/_push_rules.html.haml @@ -14,3 +14,6 @@ - @push_rule.errors.full_messages.each do |msg| %p= msg = render "shared/push_rules/form", f: f, context: @group + = render Pajamas::ButtonComponent.new(type: :submit, variant: :danger, method: :put, href: sync_all_group_push_rules_path) do + = s_('PushRules|Apply to all push rules') + diff --git a/ee/config/routes/admin.rb b/ee/config/routes/admin.rb index 36d18d4a47881fc4fc4c923fb77ac918e015eed8..2bd26a995e816499c8d1ef282dc088515e686188 100644 --- a/ee/config/routes/admin.rb +++ b/ee/config/routes/admin.rb @@ -16,7 +16,12 @@ end end - resource :push_rule, only: [:show, :update] + resource :push_rule, only: [:show, :update] do + member do + put :sync_all + end + end + resource :email, only: [:show, :create] resources :audit_logs, controller: 'audit_logs', only: [:index] resources :audit_log_reports, only: [:index], constraints: { format: :csv } diff --git a/ee/config/routes/group.rb b/ee/config/routes/group.rb index 2ffb3111212451e5337c30143510f9267dbeb1c0..449d985d24d9c48cd10f6b289d488f70b0a2aa3b 100644 --- a/ee/config/routes/group.rb +++ b/ee/config/routes/group.rb @@ -191,7 +191,11 @@ resources :merge_commit_reports, only: [:index], constraints: { format: :csv } end - resource :push_rules, only: [:update] + resource :push_rules, only: [:update] do + member do + put :sync_all + end + end resource :saml_providers, path: 'saml', only: [:show, :create, :update] do callback_methods = Rails.env.test? ? [:get, :post] : [:post] diff --git a/ee/spec/controllers/admin/push_rules_controller_spec.rb b/ee/spec/controllers/admin/push_rules_controller_spec.rb index 6fa53b1e27e44d32c9d2c2c4975b29904c2b07c1..6ab89adb36a1cfb861e10bc31e88e918c5c0b8d8 100644 --- a/ee/spec/controllers/admin/push_rules_controller_spec.rb +++ b/ee/spec/controllers/admin/push_rules_controller_spec.rb @@ -59,6 +59,37 @@ end end + describe '#sync_all' do + let!(:sample) do + create(:push_rule_sample, deny_delete_tag: "true", delete_branch_regex: "any", + commit_message_regex: "any", branch_name_regex: "any", + force_push_regex: "any", author_email_regex: "any", + member_check: "true", file_name_regex: "any", + max_file_size: "0", prevent_secrets: "true", commit_committer_check: "true", + reject_unsigned_commits: "true", + reject_non_dco_commits: "true", commit_committer_name_check: "true") + end + + let!(:normal_rule) { create(:push_rule) } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + stub_licensed_features( + commit_committer_check: true, + reject_unsigned_commits: true, + reject_non_dco_commits: true, + commit_committer_name_check: true) + end + + it 'returns 200 and apply_to_all' do + put :sync_all + + expect(response).to have_gitlab_http_status(:ok) + sample_attributes = sample.attributes.except("id", "is_sample", "created_at", "updated_at", "project_id") + expect(normal_rule.reload).to have_attributes(sample_attributes) + end + end + describe '#show' do it 'returns 200' do get :show diff --git a/ee/spec/controllers/groups/push_rules_controller_spec.rb b/ee/spec/controllers/groups/push_rules_controller_spec.rb index bc704f32ce3fe8d5fd4ea2f4d9228c71365b0e5d..50237210cd19cc0a86e28446d14a1051de13f786 100644 --- a/ee/spec/controllers/groups/push_rules_controller_spec.rb +++ b/ee/spec/controllers/groups/push_rules_controller_spec.rb @@ -288,4 +288,56 @@ def do_update end end end + + describe '#sync_all' do + before do + group.add_maintainer(user) + + sign_in(user) + end + + context 'push rules unlicensed' do + before do + stub_licensed_features(push_rules: false) + end + + it 'returns 404 status' do + put :sync_all, params: { group_id: group } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when sync all cascade down rules' do + let(:push_rule_for_group) do + create(:push_rule_without_project, deny_delete_tag: "true", delete_branch_regex: "any", + commit_message_regex: "any", branch_name_regex: "any", + force_push_regex: "any", author_email_regex: "any", + member_check: "true", file_name_regex: "any", + max_file_size: "0", prevent_secrets: "true", commit_committer_check: "true", + reject_unsigned_commits: "true", + reject_non_dco_commits: "true", commit_committer_name_check: "true") + end + + let(:child_push_rule) { create(:push_rule_without_project) } + let!(:child) { create(:group, :private, parent: group, push_rule_id: child_push_rule.id) } + let!(:project1) { build(:project, group: group) } + let!(:project2) { build(:project, group: child) } + let!(:push_rule1) { create(:push_rule, project: project1) } + let!(:push_rule2) { create(:push_rule, project: project2) } + + before do + group.update!(push_rule_id: push_rule_for_group.id) + end + + it 'success' do + put :sync_all, params: { group_id: group } + + sample_attributes = push_rule_for_group.attributes.except("id", "created_at", "updated_at", "project_id") + expect(child_push_rule.reload).to have_attributes(sample_attributes) + expect(push_rule1.reload).to have_attributes(sample_attributes) + expect(push_rule2.reload).to have_attributes(sample_attributes) + end + end + end end diff --git a/ee/spec/features/admin/admin_push_rule_spec.rb b/ee/spec/features/admin/admin_push_rule_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..97403acd224a5684cc9d2a8bd38726059cf9d66f --- /dev/null +++ b/ee/spec/features/admin/admin_push_rule_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Admin push rule' do + let_it_be(:sample_rule) { create(:push_rule_sample, force_push_regex: 'test') } + + before do + admin = create(:admin) + sign_in(admin) + gitlab_enable_admin_mode_sign_in(admin) + end + + describe 'rule page' do + before do + visit admin_push_rule_path + create(:push_rule) + end + + it 'apply to all push rules' do + expect(page).to have_content('Apply to all push rules') + + Sidekiq::Testing.fake! do + find("a[title='Apply to all push rules']").click + end + + expect(PushRule.where(force_push_regex: 'test').count).to eq(PushRule.count) + end + end +end diff --git a/ee/spec/features/groups/push_rules_spec.rb b/ee/spec/features/groups/push_rules_spec.rb index 2b777eb33f42f4309ac42fbcca8b5ba9d998c830..59c9b0f95e6899c9a7ea47fdcb90af8904ddd250 100644 --- a/ee/spec/features/groups/push_rules_spec.rb +++ b/ee/spec/features/groups/push_rules_spec.rb @@ -65,6 +65,14 @@ expect(page).to have_content(title) end + + it 'renders `apply push rule to all` button' do + create(:gitlab_subscription, :ultimate, namespace: group) + + visit group_settings_repository_path(group, anchor: 'js-push-rules') + + expect(page).to have_content('Apply to all push rules') + end end end end diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index c883cf9193e05e35b3a7398f2903fe127b465cf9..99d644578a883050a2d26565b15a78579aae66d6 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -794,6 +794,20 @@ end end + describe '#all_cascade_down_push_rules' do + context 'with sub_groups and sub_group projects' do + let(:child_push_rule) { create(:push_rule_without_project) } + let(:child) { build(:group, parent: group, push_rule: child_push_rule) } + let(:project1) { build(:project_empty_repo, group: group) } + let(:project2) { build(:project_empty_repo, group: child) } + let!(:push_rule1) { create(:push_rule, project: project1) } + let!(:push_rule2) { create(:push_rule, project: project2) } + + it { expect(group.all_cascade_down_push_rules.to_a).to match_array([push_rule1, push_rule2, child_push_rule]) } + it { expect(child.all_cascade_down_push_rules.to_a).to match_array([push_rule2]) } + end + end + describe '#checked_file_template_project' do let(:valid_project) { create(:project, namespace: group) } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e9cb7b5145193ac0fd77590c84512fa19da33104..9b6655715bfa2dd4bdc347e8a468af45f79a3d64 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33394,6 +33394,9 @@ msgstr "" msgid "PushRules|All committed filenames cannot match this %{wiki_syntax_link_start}regular expression%{wiki_syntax_link_end}. If empty, any filename is allowed." msgstr "" +msgid "PushRules|Apply to all push rules" +msgstr "" + msgid "PushRules|Branch name" msgstr ""