diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue index f5fb72e84bc5157676836578e9cb00690f5217d5..d1c143b96f7749a1424db2f211610fbc1e86f868 100644 --- a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue +++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue @@ -8,6 +8,11 @@ export default { RefSelector, }, props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, persistedDefaultBranch: { type: String, required: true, @@ -26,6 +31,7 @@ export default { </script> <template> <ref-selector + :disabled="disabled" :value="persistedDefaultBranch" class="gl-w-full" :project-id="projectId" diff --git a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js index 611561e38f2e0dbd923ab61bc00e28f65a13b3d0..8e64d29e947ea779677bbeb01ad4753bce303704 100644 --- a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js +++ b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import DefaultBranchSelector from './components/default_branch_selector.vue'; export default (el) => { @@ -6,13 +7,14 @@ export default (el) => { return null; } - const { projectId, defaultBranch } = el.dataset; + const { projectId, defaultBranch, disabled } = el.dataset; return new Vue({ el, render(createElement) { return createElement(DefaultBranchSelector, { props: { + disabled: parseBoolean(disabled), persistedDefaultBranch: defaultBranch, projectId, }, diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index ed9fd521e67bcb23ece7389c236687c9a4f39b33..0d6b19829f217fe9412cd881beca793ce61bc303 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -28,12 +28,17 @@ export default { }, inheritAttrs: false, props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, enabledRefTypes: { type: Array, required: false, default: () => ALL_REF_TYPES, validator: (val) => - // It has to be an arrray + // It has to be an array isArray(val) && // with at least one item val.length > 0 && @@ -234,6 +239,10 @@ export default { this.debouncedSearch(); }, selectRef(ref) { + if (this.disabled) { + return; + } + this.setSelectedRef(ref); this.$emit('input', this.selectedRef); }, @@ -262,6 +271,7 @@ export default { :toggle-class="extendedToggleButtonClass" :toggle-text="buttonText" :icon="dropdownIcon" + :disabled="disabled" v-bind="$attrs" v-on="$listeners" @hidden="$emit('hide')" diff --git a/app/views/projects/branch_defaults/_default_branch_fields.html.haml b/app/views/projects/branch_defaults/_default_branch_fields.html.haml index 78ce43ca8c9e579eb2254ed6aeb49efd4e63a175..f3a7e93a5f7ee7455eeb107d43c48f756be58be7 100644 --- a/app/views/projects/branch_defaults/_default_branch_fields.html.haml +++ b/app/views/projects/branch_defaults/_default_branch_fields.html.haml @@ -1,3 +1,13 @@ +- change_default_disabled = @default_branch_blocked_by_security_policy +- popover_data = {} + +- if change_default_disabled + - tag_pair_security_policies_page = tag_pair(link_to('', namespace_project_security_policies_path, target: '_blank', rel: 'noopener noreferrer'), :security_policies_link_start, :security_policies_link_end) + - tag_pair_security_policies_docs = tag_pair(link_to('', help_page_path('user/application_security/policies/scan-result-policies'), target: '_blank', rel: 'noopener noreferrer'), :learn_more_link_start, :learn_more_link_end) + - popover_content = safe_format(s_("SecurityOrchestration|You can't change the default branch because its protection is enforced by one or more %{security_policies_link_start}security policies%{security_policies_link_end}. %{learn_more_link_start}Learn more%{learn_more_link_end}."), tag_pair_security_policies_docs, tag_pair_security_policies_page) + - popover_title = s_("SecurityOrchestration|Security policy overwrites this setting") + - popover_data = { container: 'body', toggle: 'popover', html: 'true', triggers: 'hover', title: popover_title, content: popover_content } + %fieldset#default-branch-settings - if @project.empty_repo? .text-secondary @@ -6,8 +16,8 @@ .form-group = f.label :default_branch, _("Default branch"), class: 'label-bold' %p= s_('ProjectSettings|All merge requests and commits are made against this branch unless you specify a different one.') - .gl-form-input-xl - .js-select-default-branch{ data: { default_branch: @project.default_branch, project_id: @project.id } } + .gl-form-input-xl{ data: { **popover_data } } + .js-select-default-branch{ data: { default_branch: @project.default_branch, project_id: @project.id, disabled: change_default_disabled.to_s } } .form-group - help_text = _("When merge requests and commits in the default branch close, any issues they reference also close.") diff --git a/ee/spec/features/projects/settings/user_changes_default_branch_spec.rb b/ee/spec/features/projects/settings/user_changes_default_branch_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ab526049b094d407de746f18f9c69f7a5ff875a4 --- /dev/null +++ b/ee/spec/features/projects/settings/user_changes_default_branch_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :groups_and_projects do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, namespace: user.namespace) } + let_it_be(:protected_branch) { create(:protected_branch, project: project, name: project.default_branch) } + let(:default_branch_settings) { find('#default-branch-settings') } + let(:default_branch_button) do + within_testid('default-branch-dropdown') do + find('button') + end + end + + let(:visit_repository_page) { visit project_settings_repository_path(project) } + + before do + sign_in(user) + end + + context 'with branch not protected by security policy' do + it 'does not show popover if the default branch can be changed', :aggregate_failures, :js do + visit_repository_page + + expect(default_branch_settings).not_to have_selector('[data-toggle="popover"]') + expect(default_branch_button).not_to be_disabled + end + end + + context 'with branch protected by security policy' do + include_context 'with scan result policy blocking protected branches' do + let(:branch_name) { project.default_branch } + let(:policy_configuration) do + create(:security_orchestration_policy_configuration, project: project) + end + + it 'disables the button and shows the popover if the default branch cannot be changed', + :aggregate_failures, + :js do + visit_repository_page + + expect(default_branch_settings).to have_selector('[data-toggle="popover"]') + expect(default_branch_button).to be_disabled + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fea46aac1bc46f2c290c4049b2c4231f80f69951..0f27d1e1966d7d16d3b760eacadb92930c9694d6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -43291,6 +43291,9 @@ msgstr "" msgid "SecurityOrchestration|Security Scan" msgstr "" +msgid "SecurityOrchestration|Security policy overwrites this setting" +msgstr "" + msgid "SecurityOrchestration|Security policy project was linked successfully" msgstr "" @@ -43441,6 +43444,9 @@ msgstr "" msgid "SecurityOrchestration|You already have the maximum %{maximumAllowed} %{policyType} policies." msgstr "" +msgid "SecurityOrchestration|You can't change the default branch because its protection is enforced by one or more %{security_policies_link_start}security policies%{security_policies_link_end}. %{learn_more_link_start}Learn more%{learn_more_link_end}." +msgstr "" + msgid "SecurityOrchestration|You can't unprotect this branch because its protection is enforced by one or more %{security_policies_link_start}security policies%{security_policies_link_end}. %{learn_more_link_start}Learn more%{learn_more_link_end}." msgstr "" diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js index 9baea5c5517a7ed3531343b1ab7cc12dad9bb132..aa50683b1851e0a7059eae41009f47778b08a425 100644 --- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js +++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js @@ -4,6 +4,7 @@ import RefSelector from '~/ref/components/ref_selector.vue'; import { REF_TYPE_BRANCHES } from '~/ref/constants'; describe('projects/settings/components/default_branch_selector', () => { + const disabled = true; const persistedDefaultBranch = 'main'; const projectId = '123'; let wrapper; @@ -13,6 +14,7 @@ describe('projects/settings/components/default_branch_selector', () => { const buildWrapper = () => { wrapper = shallowMount(DefaultBranchSelector, { propsData: { + disabled, persistedDefaultBranch, projectId, }, @@ -25,6 +27,7 @@ describe('projects/settings/components/default_branch_selector', () => { it('displays a RefSelector component', () => { expect(findRefSelector().props()).toEqual({ + disabled, value: persistedDefaultBranch, enabledRefTypes: [REF_TYPE_BRANCHES], projectId, diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index 26010a1cfa6545462725ac6a8d809e2a1ab4c90a..39924a3a77af1a86ab9a176d65a58a663a8cee30 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -46,7 +46,7 @@ describe('Ref selector component', () => { let commitApiCallSpy; let requestSpies; - const createComponent = (mountOverrides = {}, propsData = {}) => { + const createComponent = ({ overrides = {}, propsData = {} } = {}) => { wrapper = mountExtended( RefSelector, merge( @@ -64,7 +64,7 @@ describe('Ref selector component', () => { }, store: createStore(), }, - mountOverrides, + overrides, ), ); }; @@ -211,7 +211,7 @@ describe('Ref selector component', () => { const id = 'git-ref'; beforeEach(() => { - createComponent({ attrs: { id } }); + createComponent({ overrides: { attrs: { id } } }); return waitForRequests(); }); @@ -326,7 +326,7 @@ describe('Ref selector component', () => { describe('branches', () => { describe('when the branches search returns results', () => { beforeEach(() => { - createComponent({}, { useSymbolicRefNames: true }); + createComponent({ propsData: { useSymbolicRefNames: true } }); return waitForRequests(); }); @@ -389,7 +389,7 @@ describe('Ref selector component', () => { describe('tags', () => { describe('when the tags search returns results', () => { beforeEach(() => { - createComponent({}, { useSymbolicRefNames: true }); + createComponent({ propsData: { useSymbolicRefNames: true } }); return waitForRequests(); }); @@ -569,6 +569,20 @@ describe('Ref selector component', () => { }); }); }); + + describe('disabled', () => { + it('does not disable the dropdown', () => { + createComponent(); + expect(findListbox().props('disabled')).toBe(false); + }); + + it('disables the dropdown', async () => { + createComponent({ propsData: { disabled: true } }); + expect(findListbox().props('disabled')).toBe(true); + await selectFirstBranch(); + expect(wrapper.emitted('input')).toBeUndefined(); + }); + }); }); describe('with non-default ref types', () => { @@ -691,9 +705,7 @@ describe('Ref selector component', () => { }); beforeEach(() => { - createComponent({ - scopedSlots: { footer: createFooter }, - }); + createComponent({ overrides: { scopedSlots: { footer: createFooter } } }); updateQuery('abcd1234');