diff --git a/ee/app/controllers/admin/push_rules_controller.rb b/ee/app/controllers/admin/push_rules_controller.rb index 6ce7ac3e5602b204bf18adcfc10b9ab1edce953c..771396e28e47f66d135d861ac5e28b67550a89ce 100644 --- a/ee/app/controllers/admin/push_rules_controller.rb +++ b/ee/app/controllers/admin/push_rules_controller.rb @@ -23,6 +23,14 @@ def update end end + def sync_all + return render_404 if ::Feature.disabled?(:inherited_push_rule_for_project) + + PushRule.normal.delete_all + + 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..7c765a6e8acafa7b6b5d7b5f8307ba9670eec3ad 100644 --- a/ee/app/controllers/groups/push_rules_controller.rb +++ b/ee/app/controllers/groups/push_rules_controller.rb @@ -27,6 +27,14 @@ def update redirect_to group_settings_repository_path(group, anchor: 'js-push-rules') end + def sync_all + return render_404 if ::Feature.disabled?(:inherited_push_rule_for_project) + + group.all_cascade_down_push_rules.delete_all + + 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 cd4a5e17eafd8a525193580243554a266005a875..a34e1ae1abe5190f73aaa854eafd7baf9e402c40 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -757,6 +757,12 @@ def code_owner_approval_required_available? feature_available?(:code_owner_approval_required) 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..8b1d7b0dbeeec5b11ccf239e6ffb17fea0169d44 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,9 @@ - @push_rule.errors.full_messages.each do |msg| %p= msg = render "shared/push_rules/form", f: f, context: nil + + - if ::Feature.enabled?(:inherited_push_rule_for_project) + = 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..d29e6651903505ab441fd4f1ec7e6b0fc280fec3 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,7 @@ - @push_rule.errors.full_messages.each do |msg| %p= msg = render "shared/push_rules/form", f: f, context: @group + - if ::Feature.enabled?(:inherited_push_rule_for_project) + = 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 419c87d4a4e4bae560e327a44d7af00105eedee0..bcea1d3490c9cd2d662d3112d3990117fa29fe7a 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 resources :protected_branches, only: [:create, :update, :destroy] diff --git a/ee/spec/controllers/admin/push_rules_controller_spec.rb b/ee/spec/controllers/admin/push_rules_controller_spec.rb index 6fa53b1e27e44d32c9d2c2c4975b29904c2b07c1..6f0e864fd6a0dd90d63174a06d8db08e4203ad72 100644 --- a/ee/spec/controllers/admin/push_rules_controller_spec.rb +++ b/ee/spec/controllers/admin/push_rules_controller_spec.rb @@ -59,6 +59,33 @@ end end + describe '#sync_all' do + let!(:sample) do + create(:push_rule_sample) + end + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + create(:push_rule) + end + + it 'delete all normal rules' do + expect do + put :sync_all + end.to change { PushRule.count }.by(-1) + end + + context 'when disable inherited_push_rule_for_project' do + before do + stub_feature_flags(inherited_push_rule_for_project: false) + end + + it 'does nothing' do + expect { put :sync_all }.not_to change { PushRule.count } + end + 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 51d316d2131226ec96d0af8c9b8c9fe4f63c03f0..8e8d691329c669488220f6956dba504e3ea6e460 100644 --- a/ee/spec/controllers/groups/push_rules_controller_spec.rb +++ b/ee/spec/controllers/groups/push_rules_controller_spec.rb @@ -288,4 +288,61 @@ 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) + 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) { create(:project, group: group) } + let!(:project2) { create(:project, group: child) } + let!(:group_project_rule) { create(:push_rule, project: project1) } + let!(:child_group_project_rule) { create(:push_rule, project: project2) } + + before do + group.update!(push_rule_id: push_rule_for_group.id) + end + + it 'delete all cascade down rules' do + put :sync_all, params: { group_id: group } + + expect { child_push_rule.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { group_project_rule.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { child_group_project_rule.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when feature disabled' do + before do + stub_feature_flags(inherited_push_rule_for_project: false) + end + + it 'does nothing' do + put :sync_all, params: { group_id: group } + + expect(response).to have_gitlab_http_status(:not_found) + end + 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..3d2921715a8c225774d4ed96b39a445e11827e7a --- /dev/null +++ b/ee/spec/features/admin/admin_push_rule_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Admin push rule', feature_category: :source_code_management 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 'Admin push rule page' do + before do + create(:push_rule) + end + + context 'when feature disabled' do + before do + stub_feature_flags(inherited_push_rule_for_project: false) + visit admin_push_rule_path + end + + it 'do not render apply all button' do + expect(page).not_to have_content('Apply to all push rules') + end + end + + context 'when feature enabled' do + before do + visit admin_push_rule_path + end + + it 'render button' 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.count).to eq(1) + end + end + end +end diff --git a/ee/spec/features/groups/push_rules_spec.rb b/ee/spec/features/groups/push_rules_spec.rb index 2c7a0c085553dc8622ec3115af8ffd266f77288c..8421336273a48808d3848a13366460df77729f56 100644 --- a/ee/spec/features/groups/push_rules_spec.rb +++ b/ee/spec/features/groups/push_rules_spec.rb @@ -42,6 +42,12 @@ expect(page).to have_content(title) end + it 'renders `apply push rule to all` button' do + visit group_settings_repository_path(group, anchor: 'js-push-rules') + + expect(page).to have_content('Apply to all push rules') + end + describe 'with GL.com plans', :saas do before do stub_application_setting(check_namespace_plan: true) diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index e3a994d403ee90105d0e286ba800aa4e17c7e5f5..ace148790732a0582c530f84dd14f968c9b2fdff 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -857,6 +857,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!(:group_project_rule) { create(:push_rule, project: project1) } + let!(:child_group_project_rule) { create(:push_rule, project: project2) } + + it { expect(group.all_cascade_down_push_rules.to_a).to match_array([group_project_rule, child_group_project_rule, child_push_rule]) } + it { expect(child.all_cascade_down_push_rules.to_a).to match_array([child_group_project_rule]) } + 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 71fbff57787e537a409f52bd606b11b8a03867ab..4eb946522a901c79ab68c68b55ec7fc5f619aa76 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -34414,6 +34414,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 ""