From 63454b5487da6f4e2a7d030b815b191faf16ab3f Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Thu, 29 Aug 2019 14:23:44 -0700 Subject: [PATCH 01/21] Add API endpoint for updating protected branch Included in this commit are 3 squashed commits: - Support strong param "chaining" Rather than duplicate the strong param declarations in an EE-override, let's rework this method so we can call `super(:new, :params, :to, :include)` - EE override to allow :code_owner_approval_required - Force COAR false if feature is disabled --- .../projects/protected_branches_controller.rb | 12 ++-- .../shared/_branches_list.html.haml | 15 ++-- .../projects/protected_branches_controller.rb | 20 ++++++ ee/lib/ee/api/protected_branches.rb | 38 +++++++++++ .../protected_branches_controller_spec.rb | 68 +++++++++++++++++++ ee/spec/features/protected_branches_spec.rb | 67 ++++++++++++++++++ .../requests/api/protected_branches_spec.rb | 48 +++++++++++++ lib/api/protected_branches.rb | 4 +- 8 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 ee/app/controllers/ee/projects/protected_branches_controller.rb create mode 100644 ee/lib/ee/api/protected_branches.rb create mode 100644 ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index c5454883060bb2..d4f7d0bc52155f 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -19,9 +19,13 @@ def access_levels [:merge_access_levels, :push_access_levels] end - def protected_ref_params - params.require(:protected_branch).permit(:name, - merge_access_levels_attributes: access_level_attributes, - push_access_levels_attributes: access_level_attributes) + def protected_ref_params(*attrs) + attrs = ([:name, + merge_access_levels_attributes: access_level_attributes, + push_access_levels_attributes: access_level_attributes] + attrs).uniq + + params.require(:protected_branch).permit(attrs) end end + +Projects::ProtectedBranchesController.prepend_if_ee('EE::Projects::ProtectedBranchesController') diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 1913d06a6f87f0..7615d60ba27385 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -15,10 +15,17 @@ %col %thead %tr - %th Protected branch (#{@protected_branches_count}) - %th Last commit - %th Allowed to merge - %th Allowed to push + %th + = (s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }).html_safe + %th + = s_("ProtectedBranch|Last commit") + %th + = s_("ProtectedBranch|Allowed to merge") + %th + = s_("ProtectedBranch|Allowed to push") + - if @project.merge_requests_require_code_owner_approval + %th + = s_("ProtectedBranch|Code owner approval") - if can_admin_project %th %tbody diff --git a/ee/app/controllers/ee/projects/protected_branches_controller.rb b/ee/app/controllers/ee/projects/protected_branches_controller.rb new file mode 100644 index 00000000000000..f1964210371557 --- /dev/null +++ b/ee/app/controllers/ee/projects/protected_branches_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module EE + module Projects + module ProtectedBranchesController + extend ::Gitlab::Utils::Override + + protected + + override :protected_ref_params + def protected_ref_params(*attrs) + params_hash = super(:code_owner_approval_required) + + params_hash[:code_owner_approval_required] = false unless project.code_owner_approval_required_available? + + params_hash + end + end + end +end diff --git a/ee/lib/ee/api/protected_branches.rb b/ee/lib/ee/api/protected_branches.rb new file mode 100644 index 00000000000000..736899a94eff08 --- /dev/null +++ b/ee/lib/ee/api/protected_branches.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module EE + module API + module ProtectedBranches + extend ActiveSupport::Concern + + BRANCH_ENDPOINT_REQUIREMENTS = ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: ::API::API::NO_SLASH_URL_PART_REGEX) + + prepended do + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Update the code_owner_approval_required state of an existing protected branch' do + success ::API::Entities::ProtectedBranch + end + params do + requires :name, type: String, desc: 'The name of the branch' + + use :optional_params_ee + end + # rubocop: disable CodeReuse/ActiveRecord + patch ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do + render_api_error!(protected_branch.errors.full_messages, 404) unless user_project.feature_available?(:code_owners) + + protected_branch = user_project.protected_branches.find_by!(name: params[:name]) + + protected_branch.update_attribute(:code_owner_approval_required, declared_params[:code_owner_approval_required]) + + present protected_branch, with: ::API::Entities::ProtectedBranch, project: user_project + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end + end +end diff --git a/ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb b/ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb new file mode 100644 index 00000000000000..828b3d075a5244 --- /dev/null +++ b/ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +require "spec_helper" + +describe Projects::ProtectedBranchesController do + let(:project) { create(:project, :repository) } + let(:protected_branch) { create(:protected_branch, project: project) } + let(:project_params) { { namespace_id: project.namespace.to_param, project_id: project } } + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + end + + shared_examples "protected branch with code owner approvals feature" do |boolean| + it "sets code owner approvals to #{boolean} when protecting the branch" do + expect do + post(:create, params: project_params.merge(protected_branch: create_params)) + end.to change(ProtectedBranch, :count).by(1) + + expect(ProtectedBranch.last.attributes["code_owner_approval_required"]).to eq(boolean) + end + end + + describe "POST #create" do + let(:maintainer_access_level) { [{ access_level: Gitlab::Access::MAINTAINER }] } + let(:access_level_params) do + { merge_access_levels_attributes: maintainer_access_level, + push_access_levels_attributes: maintainer_access_level } + end + let(:create_params) do + attributes_for(:protected_branch).merge(access_level_params) + end + + before do + sign_in(user) + end + + context "when code_owner_approval_required is 'false'" do + before do + create_params[:code_owner_approval_required] = false + end + + it_behaves_like "protected branch with code owner approvals feature", false + end + + context "when code_owner_approval_required is 'true'" do + before do + create_params[:code_owner_approval_required] = true + end + + context "when the feature is enabled" do + before do + stub_licensed_features(code_owner_approval_required: true) + end + + it_behaves_like "protected branch with code owner approvals feature", true + end + + context "when the feature is not enabled" do + before do + stub_licensed_features(code_owner_approval_required: false) + end + + it_behaves_like "protected branch with code owner approvals feature", false + end + end + end +end diff --git a/ee/spec/features/protected_branches_spec.rb b/ee/spec/features/protected_branches_spec.rb index e64e4c7074a020..7b55a18885e80c 100644 --- a/ee/spec/features/protected_branches_spec.rb +++ b/ee/spec/features/protected_branches_spec.rb @@ -12,6 +12,73 @@ sign_in(user) end + describe 'code owner approval' do + describe 'when project requires code owner approval' do + before do + stub_licensed_features(protected_refs_for_users: true, code_owner_approval_required: true) + project.update!(merge_requests_require_code_owner_approval: true) + end + + describe 'protect a branch form' do + let!(:protected_branch) { create(:protected_branch, project: project) } + + it 'has code owner toggle' do + visit project_settings_repository_path(project) + + expect(page).to have_content("Require approval from code owners") + end + end + + describe 'protect branch table' do + context 'has a protected branch with code owner approval toggled on' do + let!(:protected_branch) { create(:protected_branch, project: project, code_owner_approval_required: true) } + + it 'shows code owner approval toggle' do + visit project_settings_repository_path(project) + + expect(page).to have_content("Code owner approval") + end + + it 'displays toggle on' do + visit project_settings_repository_path(project) + + expect(page).to have_css('.js-project-feature-toggle.is-checked') + end + end + + context 'has a protected branch with code owner approval toggled off ' do + let!(:protected_branch) { create(:protected_branch, project: project, code_owner_approval_required: false) } + + it 'displays toggle off' do + visit project_settings_repository_path(project) + + page.within '.qa-protected-branches-list' do + expect(page).not_to have_css('.js-project-feature-toggle.is-checked') + end + end + end + end + end + + describe 'when project does not require code owner approval' do + before do + stub_licensed_features(protected_refs_for_users: true, code_owner_approval_required: false) + end + + it 'does not have code owner approval in the form' do + visit project_settings_repository_path(project) + + expect(page).not_to have_content("Require approval from code owners") + end + + it 'does not have code owner approval in the table' do + visit project_settings_repository_path(project) + + expect(page).not_to have_content("Code owner approval") + end + end + end + describe 'access control' do describe 'with ref permissions for users enabled' do before do diff --git a/ee/spec/requests/api/protected_branches_spec.rb b/ee/spec/requests/api/protected_branches_spec.rb index 9721e5b225cdab..379ec4e62c6c48 100644 --- a/ee/spec/requests/api/protected_branches_spec.rb +++ b/ee/spec/requests/api/protected_branches_spec.rb @@ -82,6 +82,54 @@ end end + describe "PATCH /projects/:id/protected_branches/:branch" do + let(:route) { "/projects/#{project.id}/protected_branches/#{branch_name}" } + + context 'when authenticated as a maintainer' do + before do + project.add_maintainer(user) + end + + context "when the feature is enabled" do + before do + stub_licensed_features(code_owner_approval_required: true) + end + + it "updates the protected branch" do + expect do + patch api(route, user), params: { code_owner_approval_required: true } + end.to change { protected_branch.reload.code_owner_approval_required } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['code_owner_approval_required']).to eq(true) + end + end + + context "when the feature is disabled" do + it "does not change the protected branch" do + expect do + patch api(route, user), params: { code_owner_approval_required: true } + end.not_to change { protected_branch.reload.code_owner_approval_required } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['code_owner_approval_required']).to eq(false) + end + end + end + + context 'when authenticated as a guest' do + before do + project.add_guest(user) + end + + it "returns a 403 response" do + patch api(route, user) + + expect(response).to have_gitlab_http_status(403) + end + end + end + describe 'POST /projects/:id/protected_branches' do let(:branch_name) { 'new_branch' } let(:post_endpoint) { api("/projects/#{project.id}/protected_branches", user) } diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index ca75ee906ce0c4..c7665c20234362 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -42,7 +42,7 @@ class ProtectedBranches < Grape::API end # rubocop: enable CodeReuse/ActiveRecord - desc 'Protect a single branch or wildcard' do + desc 'Protect a single branch' do success Entities::ProtectedBranch end params do @@ -93,3 +93,5 @@ class ProtectedBranches < Grape::API end end end + +API::ProtectedBranches.prepend_if_ee('EE::API::ProtectedBranches') -- GitLab From e82672e5d80087d7dc8c1aff63797b1cbc1ab303 Mon Sep 17 00:00:00 2001 From: Samantha Ming Date: Fri, 23 Aug 2019 17:26:58 -0700 Subject: [PATCH 02/21] Add views of code owner approval - squash 3c0eea75eb2 Add translation & consolidate bindEvents methods - squash ee2b7846d51 Move selector to constructor & add changelog Add translation & consolidate bindEvents methods - Translation for protech branch table & form - Move method under bindEvents and extract callback to separate method Move selector to constructor & add changelog - move toggle button selector - remove unused $ import - add changelog --- .../shared/_branches_list.html.haml | 4 +- .../shared/_create_protected_branch.html.haml | 30 ++++++++----- .../shared/_protected_branch.html.haml | 9 ++++ .../protected_branch_create.js | 8 ++++ .../protected_branch_edit.js | 33 ++++++++++++++- ...1-fe-require-approval-from-code-owners.yml | 5 +++ locale/gitlab.pot | 42 +++++++++++++++++++ 7 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 7615d60ba27385..f41e20e873dfa7 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,9 +1,9 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? .card-header.bg-white - Protected branch (#{@protected_branches_count}) + = (s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }).html_safe %p.settings-message.text-center - There are currently no protected branches, protect a branch with the form above. + = s_("ProtectedBranch|There are currently no protected branches, protect a branch with the form above.") - else %table.table.table-bordered %colgroup diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index bba4949277d449..8808ddef84978d 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -2,7 +2,7 @@ %input{ type: 'hidden', name: 'update_section', value: 'js-protected-branches-settings' } .card .card-header - Protect a branch + = s_("ProtectedBranch|Protect a branch") .card-body = form_errors(@protected_branch) .form-group.row @@ -11,22 +11,32 @@ .col-md-10 = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f } .form-text.text-muted - = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') - such as - %code *-stable - or - %code production/* - are supported + - wildcards_url = help_page_url('user/project/protected_branches', anchor: 'wildcard-protected-branches') + - wildcards_link_start = ''.html_safe % { url: wildcards_url } + = (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported") % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '', code_tag_start: '', code_tag_end: '' }).html_safe .form-group.row %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } - Allowed to merge: + = s_("ProtectedBranch|Allowed to merge:") .col-md-10 = yield :merge_access_levels .form-group.row %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } - Allowed to push: + = s_("ProtectedBranch|Allowed to push:") .col-md-10 = yield :push_access_levels + - if @project.merge_requests_require_code_owner_approval + .form-group.row + %label.col-md-2.text-right{ for: 'code_owner_approval_required' } + = s_("ProtectedBranch|Require approval from code owners:") + .col-md-10 + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle is-checked", + "aria-label": s_("ProtectedBranch|Toggle code owner approval") } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + .form-text.text-muted + = s_("ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected") .card-footer - = f.submit 'Protect', class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' } + = f.submit s_('ProtectedBranch|Protect'), class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' } diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 81dcab1d1ab440..71c8d616eb96ef 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -19,6 +19,15 @@ = yield + - if @project.merge_requests_require_code_owner_approval + %td + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", + "aria-label": s_("ProtectedBranch|Toggle code owner approval") } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch, { update_section: 'js-protected-branches-settings' }], disabled: local_assigns[:disabled], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning" diff --git a/ee/app/assets/javascripts/protected_branches/protected_branch_create.js b/ee/app/assets/javascripts/protected_branches/protected_branch_create.js index 973e256a1f9dac..e696af69e27a3a 100644 --- a/ee/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/ee/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -14,13 +14,20 @@ export default class ProtectedBranchCreate { this.currentProjectUserDefaults = {}; this.buildDropdowns(); this.$branchInput = this.$form.find('input[name="protected_branch[name]"]'); + this.$toggleButton = this.$form.find('.js-project-feature-toggle'); this.bindEvents(); } bindEvents() { + this.$toggleButton.on('click', this.onToggleButtonClick.bind(this)); this.$form.on('submit', this.onFormSubmit.bind(this)); } + onToggleButtonClick() { + const toggleButton = this.$toggleButton; + toggleButton.toggleClass('is-checked'); + } + buildDropdowns() { const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); @@ -75,6 +82,7 @@ export default class ProtectedBranchCreate { authenticity_token: this.$form.find('input[name="authenticity_token"]').val(), protected_branch: { name: this.$form.find('input[name="protected_branch[name]"]').val(), + code_owner_approval_required: this.$toggleButton.hasClass('is-checked'), }, }; diff --git a/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js b/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js index 6eb75415019685..91c5617e4e572c 100644 --- a/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import _ from 'underscore'; import axios from '~/lib/utils/axios_utils'; import Flash from '~/flash'; @@ -13,6 +12,7 @@ export default class ProtectedBranchEdit { this.$wrap = options.$wrap; this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + this.$toggleButton = this.$wrap.find('.js-project-feature-toggle'); this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest( `.${ACCESS_LEVELS.MERGE}-container`, @@ -22,6 +22,35 @@ export default class ProtectedBranchEdit { ); this.buildDropdowns(); + this.bindEvents(); + } + + bindEvents() { + this.$toggleButton.on('click', this.onToggleButtonClick.bind(this)); + } + + onToggleButtonClick() { + this.$toggleButton.toggleClass('is-checked'); + this.$toggleButton.prop('disabled', true); + + const formData = { + code_owner_approval_required: this.$toggleButton.hasClass('is-checked'), + }; + + this.updateCodeOwnerApproval(formData); + } + + updateCodeOwnerApproval(formData) { + axios + .patch(this.$wrap.data('url'), { + protected_branch: formData, + }) + .then(() => { + this.$toggleButton.prop('disabled', false); + }) + .catch(() => { + Flash(__('Failed to update branch!')); + }); } buildDropdowns() { @@ -85,7 +114,7 @@ export default class ProtectedBranchEdit { .catch(() => { this.$allowedToMergeDropdown.enable(); this.$allowedToPushDropdown.enable(); - Flash(__('Failed to update branch!'), null, $('.js-protected-branches-list')); + Flash(__('Failed to update branch!')); }); } diff --git a/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml new file mode 100644 index 00000000000000..3734c449de2b78 --- /dev/null +++ b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml @@ -0,0 +1,5 @@ +--- +title: Frontend Implementation of Code Owner Approval +merge_request: 15862 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a17d0460967101..ff011c70a63e4f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12773,6 +12773,48 @@ msgstr "" msgid "Protected branches" msgstr "" +msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported" +msgstr "" + +msgid "ProtectedBranch|Allowed to merge" +msgstr "" + +msgid "ProtectedBranch|Allowed to merge:" +msgstr "" + +msgid "ProtectedBranch|Allowed to push" +msgstr "" + +msgid "ProtectedBranch|Allowed to push:" +msgstr "" + +msgid "ProtectedBranch|Code owner approval" +msgstr "" + +msgid "ProtectedBranch|Last commit" +msgstr "" + +msgid "ProtectedBranch|Protect" +msgstr "" + +msgid "ProtectedBranch|Protect a branch" +msgstr "" + +msgid "ProtectedBranch|Protected branch (%{protected_branches_count})" +msgstr "" + +msgid "ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected" +msgstr "" + +msgid "ProtectedBranch|Require approval from code owners:" +msgstr "" + +msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above." +msgstr "" + +msgid "ProtectedBranch|Toggle code owner approval" +msgstr "" + msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?" msgstr "" -- GitLab From 72ea83e93d838e1b0cacf9ab3096d62457b3e2ab Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Thu, 12 Sep 2019 15:12:00 -0700 Subject: [PATCH 03/21] Update docs to reflect changes to API and UI Add "code_owner_approval_required" field to API response documentation. --- doc/api/protected_branches.md | 4 ++-- .../img/protected_branches_list_v12_3.png | Bin 8774 -> 32351 bytes .../img/protected_branches_page_v12_3.png | Bin 9445 -> 50657 bytes doc/user/project/protected_branches.md | 18 ++++++++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index debf1b264f9e36..ce709387a61250 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -172,8 +172,8 @@ curl --request POST --header "PRIVATE-TOKEN: " 'https://gitla | `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, maintainer access level) | | `allowed_to_push` | array | no | **(STARTER)** Array of access levels allowed to push, with each described by a hash | | `allowed_to_merge` | array | no | **(STARTER)** Array of access levels allowed to merge, with each described by a hash | -| `allowed_to_unprotect` | array | no | **(STARTER)** Array of access levels allowed to unprotect, with each described by a hash | -| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) | +| `allowed_to_unprotect` | array | no | **(STARTER)**Array of access levels allowed to unprotect, with each described by a hash | +| `code_owner_approval_required` | boolean | no | **(PREMIUM)**Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). | Example response: diff --git a/doc/user/project/img/protected_branches_list_v12_3.png b/doc/user/project/img/protected_branches_list_v12_3.png index 365d8d99e5a605d0067bcc97baf510bd7ba92b7c..2353ddd23beb838a093a6047c3cce8c8a1e40a90 100644 GIT binary patch literal 32351 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 UxU@w;D@GVRUHx3vIVCg!00%oi)c^nh diff --git a/doc/user/project/img/protected_branches_page_v12_3.png b/doc/user/project/img/protected_branches_page_v12_3.png index 17f196425527646a6d9d51c3b8f1102405b4abca..9a194c85c41b03d72fee6c15bf0dbf269b72cf1f 100644 GIT binary patch literal 50657 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 ae|~vQRuNre@kI;_3=E#GelF{r5}E*pip359 literal 9445 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 diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 9a6cd3dc362663..95252d2947e85a 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -86,6 +86,24 @@ Click **Protect** and the branch will appear in the "Protected branch" list. ![Roles and users list](img/protected_branches_select_roles_and_users_list.png) +## Code Owners approvals **(PREMIUM)** + +It is possible to require at least one approval for each entry in the +<<<<<<< HEAD +[`CODEOWNERS` file](code_owners.md) that matches a file changed in +======= +[`CODEOWNERS` file](../code_owners.md) that matches a file changed in +>>>>>>> Update docs to reflect changes to API and UI +the merge request. To enable this feature: + +1. Toggle the **Require approval from code owners** slider. + +1. Click **Protect**. + +When this feature is enabled, all merge requests need approval +from one code owner per matched rule before they can be merged. Additionally, +pushes to the protected branch are denied if a rule is matched. + ## Wildcard protected branches > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/4665) in GitLab 8.10. -- GitLab From 8a24fd1fc735308fe7d18daee2d79b75e85f83d6 Mon Sep 17 00:00:00 2001 From: Samantha Ming Date: Fri, 13 Sep 2019 12:30:47 -0700 Subject: [PATCH 04/21] Move haml files to EE render partials - create partials for code owner form, table, & table headers --- .../shared/_branches_list.html.haml | 6 +++--- .../shared/_create_protected_branch.html.haml | 15 +-------------- .../shared/_protected_branch.html.haml | 9 +-------- doc/user/project/protected_branches.md | 4 ---- .../projects/_code_owner_approval_form.html.haml | 13 +++++++++++++ .../projects/_code_owner_approval_table.html.haml | 8 ++++++++ .../_code_owner_approval_table_head.html.haml | 3 +++ 7 files changed, 29 insertions(+), 29 deletions(-) create mode 100644 ee/app/views/shared/projects/_code_owner_approval_form.html.haml create mode 100644 ee/app/views/shared/projects/_code_owner_approval_table.html.haml create mode 100644 ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index f41e20e873dfa7..47eac175c93f67 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -23,9 +23,9 @@ = s_("ProtectedBranch|Allowed to merge") %th = s_("ProtectedBranch|Allowed to push") - - if @project.merge_requests_require_code_owner_approval - %th - = s_("ProtectedBranch|Code owner approval") + + = render_if_exists 'shared/projects/code_owner_approval_table_head' + - if can_admin_project %th %tbody diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index 8808ddef84978d..79f52cb020cad0 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -24,19 +24,6 @@ = s_("ProtectedBranch|Allowed to push:") .col-md-10 = yield :push_access_levels - - - if @project.merge_requests_require_code_owner_approval - .form-group.row - %label.col-md-2.text-right{ for: 'code_owner_approval_required' } - = s_("ProtectedBranch|Require approval from code owners:") - .col-md-10 - %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle is-checked", - "aria-label": s_("ProtectedBranch|Toggle code owner approval") } - %span.toggle-icon - = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') - = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') - .form-text.text-muted - = s_("ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected") + = render_if_exists 'shared/projects/code_owner_approval_form' .card-footer = f.submit s_('ProtectedBranch|Protect'), class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' } diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 71c8d616eb96ef..53c16392357351 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -19,14 +19,7 @@ = yield - - if @project.merge_requests_require_code_owner_approval - %td - %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", - "aria-label": s_("ProtectedBranch|Toggle code owner approval") } - %span.toggle-icon - = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') - = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + = render_if_exists 'shared/projects/code_owner_approval_table', protected_branch: protected_branch - if can_admin_project %td diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 95252d2947e85a..1bd272bdd0c0e0 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -89,11 +89,7 @@ Click **Protect** and the branch will appear in the "Protected branch" list. ## Code Owners approvals **(PREMIUM)** It is possible to require at least one approval for each entry in the -<<<<<<< HEAD [`CODEOWNERS` file](code_owners.md) that matches a file changed in -======= -[`CODEOWNERS` file](../code_owners.md) that matches a file changed in ->>>>>>> Update docs to reflect changes to API and UI the merge request. To enable this feature: 1. Toggle the **Require approval from code owners** slider. diff --git a/ee/app/views/shared/projects/_code_owner_approval_form.html.haml b/ee/app/views/shared/projects/_code_owner_approval_form.html.haml new file mode 100644 index 00000000000000..a504cac457aa41 --- /dev/null +++ b/ee/app/views/shared/projects/_code_owner_approval_form.html.haml @@ -0,0 +1,13 @@ +- if @project.merge_requests_require_code_owner_approval + .form-group.row + %label.col-md-2.text-right{ for: 'code_owner_approval_required' } + = s_("ProtectedBranch|Require approval from code owners:") + .col-md-10 + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle is-checked", + "aria-label": s_("ProtectedBranch|Toggle code owner approval") } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + .form-text.text-muted + = s_("ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected") diff --git a/ee/app/views/shared/projects/_code_owner_approval_table.html.haml b/ee/app/views/shared/projects/_code_owner_approval_table.html.haml new file mode 100644 index 00000000000000..5d135e9f8da5ce --- /dev/null +++ b/ee/app/views/shared/projects/_code_owner_approval_table.html.haml @@ -0,0 +1,8 @@ +- if @project.merge_requests_require_code_owner_approval + %td + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", + "aria-label": s_("ProtectedBranch|Toggle code owner approval") } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') diff --git a/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml b/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml new file mode 100644 index 00000000000000..017058ee981190 --- /dev/null +++ b/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml @@ -0,0 +1,3 @@ +- if @project.merge_requests_require_code_owner_approval + %th + = s_("ProtectedBranch|Code owner approval") -- GitLab From 7846cf62644e1e41250f0b95a3c987f3e720e3ff Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Mon, 16 Sep 2019 10:05:13 -0700 Subject: [PATCH 05/21] Update error condition in guard clause --- ee/lib/ee/api/protected_branches.rb | 2 +- ee/spec/requests/api/protected_branches_spec.rb | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ee/lib/ee/api/protected_branches.rb b/ee/lib/ee/api/protected_branches.rb index 736899a94eff08..6f37a7364a3829 100644 --- a/ee/lib/ee/api/protected_branches.rb +++ b/ee/lib/ee/api/protected_branches.rb @@ -22,7 +22,7 @@ module ProtectedBranches end # rubocop: disable CodeReuse/ActiveRecord patch ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do - render_api_error!(protected_branch.errors.full_messages, 404) unless user_project.feature_available?(:code_owners) + render_api_error!("Feature 'Code Owner Approval Required' is not enabled", 403) unless user_project.code_owner_approval_required_available? protected_branch = user_project.protected_branches.find_by!(name: params[:name]) diff --git a/ee/spec/requests/api/protected_branches_spec.rb b/ee/spec/requests/api/protected_branches_spec.rb index 379ec4e62c6c48..ebddcf61b95dc2 100644 --- a/ee/spec/requests/api/protected_branches_spec.rb +++ b/ee/spec/requests/api/protected_branches_spec.rb @@ -106,13 +106,16 @@ end context "when the feature is disabled" do + before do + stub_licensed_features(code_owner_approval_required: false) + end + it "does not change the protected branch" do expect do patch api(route, user), params: { code_owner_approval_required: true } end.not_to change { protected_branch.reload.code_owner_approval_required } - expect(response).to have_gitlab_http_status(200) - expect(json_response['code_owner_approval_required']).to eq(false) + expect(response).to have_gitlab_http_status(403) end end end -- GitLab From faf2faf79ff0b87e847918030d25efd0329045e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lu=C3=ADs?= Date: Tue, 17 Sep 2019 17:57:57 +0000 Subject: [PATCH 06/21] Remove checkbox for requiring approval in project --- .../_merge_request_approvals_settings_form.html.haml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml index 49179755d8ebc4..8800650f429705 100644 --- a/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml +++ b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml @@ -11,14 +11,6 @@ .text-center.prepend-top-default = sprite_icon('spinner', size: 24, css_class: 'gl-spinner') -- if project.code_owner_approval_required_available? - .form-group.require-code-owner-approval - .form-check - = form.check_box(:merge_requests_require_code_owner_approval, class: 'form-check-input qa-require-code-owners-approval-checkbox') - = form.label :merge_requests_require_code_owner_approval, class: 'form-check-label' do - %span= _('Require approval from code owners') - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'editing-approvals-premium'), target: '_blank' - .form-group .form-check = form.check_box(:disable_overriding_approvers_per_merge_request, { checked: can_override_approvers, class: 'form-check-input' }, false, true) -- GitLab From ff41143945a8fb5dc934f5057e2ef17359864b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lu=C3=ADs?= Date: Tue, 17 Sep 2019 18:17:50 +0000 Subject: [PATCH 07/21] Remove tests for MR approval in project settings --- .../settings/merge_request_approval.rb | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 qa/qa/ee/page/project/settings/merge_request_approval.rb diff --git a/qa/qa/ee/page/project/settings/merge_request_approval.rb b/qa/qa/ee/page/project/settings/merge_request_approval.rb deleted file mode 100644 index 63b42c9bf9d7be..00000000000000 --- a/qa/qa/ee/page/project/settings/merge_request_approval.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module QA - module EE - module Page - module Project - module Settings - class MergeRequestApproval < QA::Page::Base - view 'ee/app/views/projects/_merge_request_approvals_settings_form.html.haml' do - element :require_code_owners_approval_checkbox - end - - view 'ee/app/views/projects/_merge_request_approvals_settings.html.haml' do - element :save_merge_request_approval_settings_button - end - - def click_require_code_owners_approval_checkbox - check_element :require_code_owners_approval_checkbox - end - - def click_save_merge_request_approval_button - click_element :save_merge_request_approval_settings_button - end - end - end - end - end - end -end -- GitLab From b6c1f14702daa8ffc53f442dd7bba7d2947530da Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Tue, 17 Sep 2019 11:26:07 -0700 Subject: [PATCH 08/21] Update to include PATCH endpoint --- doc/api/protected_branches.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index ce709387a61250..bce5101059b62a 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -172,8 +172,8 @@ curl --request POST --header "PRIVATE-TOKEN: " 'https://gitla | `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, maintainer access level) | | `allowed_to_push` | array | no | **(STARTER)** Array of access levels allowed to push, with each described by a hash | | `allowed_to_merge` | array | no | **(STARTER)** Array of access levels allowed to merge, with each described by a hash | -| `allowed_to_unprotect` | array | no | **(STARTER)**Array of access levels allowed to unprotect, with each described by a hash | -| `code_owner_approval_required` | boolean | no | **(PREMIUM)**Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). | +| `allowed_to_unprotect` | array | no | **(STARTER)** Array of access levels allowed to unprotect, with each described by a hash | +| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) | Example response: @@ -296,3 +296,21 @@ curl --request DELETE --header "PRIVATE-TOKEN: " 'https://git | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | yes | The name of the branch | + +## Require code owner approvals for a single branch + +Updated the "code owner approval required" option for the given protected branch protected branch. + +``` +PATCH /projects/:id/protected_branches/:name +``` + +```bash +curl --request PATCH --header "PRIVATE-TOKEN: " 'https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch' +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `name` | string | yes | The name of the branch | +| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false)| -- GitLab From 8579fbcd0f54b5f5cca91a86aa1aaa7539742d4e Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Tue, 17 Sep 2019 11:41:49 -0700 Subject: [PATCH 09/21] Remove deprecated string --- locale/gitlab.pot | 3 --- 1 file changed, 3 deletions(-) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ff011c70a63e4f..cbde6291514327 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13462,9 +13462,6 @@ msgstr "" msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab." msgstr "" -msgid "Require approval from code owners" -msgstr "" - msgid "Require user password to approve" msgstr "" -- GitLab From bfcd89b1fc8ae569ed888ec16b13b104017b9b36 Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Tue, 17 Sep 2019 11:54:15 -0700 Subject: [PATCH 10/21] Remove delete file from QA autoloader --- qa/qa/ee.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/qa/qa/ee.rb b/qa/qa/ee.rb index a706e7da6dfc0f..afc6bed53b094c 100644 --- a/qa/qa/ee.rb +++ b/qa/qa/ee.rb @@ -114,8 +114,6 @@ module Settings autoload :ProtectedBranches, 'qa/ee/page/project/settings/protected_branches' autoload :MirroringRepositories, 'qa/ee/page/project/settings/mirroring_repositories' autoload :Main, 'qa/ee/page/project/settings/main' - autoload :MergeRequest, 'qa/ee/page/project/settings/merge_request' - autoload :MergeRequestApproval, 'qa/ee/page/project/settings/merge_request_approval' autoload :Repository, 'qa/ee/page/project/settings/repository' autoload :PushRules, 'qa/ee/page/project/settings/push_rules' end -- GitLab From db4157ca6fca1f0f9a704bffbe91750f62660535 Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Tue, 17 Sep 2019 23:39:39 -0700 Subject: [PATCH 11/21] Fix small typo in API doc --- doc/api/protected_branches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index bce5101059b62a..4a750b42f65f87 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -299,7 +299,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: " 'https://git ## Require code owner approvals for a single branch -Updated the "code owner approval required" option for the given protected branch protected branch. +Update the "code owner approval required" option for the given protected branch protected branch. ``` PATCH /projects/:id/protected_branches/:name -- GitLab From 035b94f4c774933289eb65535fc395fc53e63986 Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Wed, 18 Sep 2019 16:34:17 +0000 Subject: [PATCH 12/21] Apply suggestion to ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml --- .../unreleased/13251-fe-require-approval-from-code-owners.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml index 3734c449de2b78..318da606dce978 100644 --- a/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml +++ b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml @@ -1,5 +1,5 @@ --- -title: Frontend Implementation of Code Owner Approval +title: Add configurable Code Owner approvals for protected branches merge_request: 15862 author: type: added -- GitLab From 0ff71c4046333960ac11ed4ccf14af72f0c73047 Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Wed, 18 Sep 2019 10:14:08 -0700 Subject: [PATCH 13/21] Add additional guard on incoming param --- .../ee/protected_branches/create_service.rb | 2 ++ .../protected_branches/create_service_spec.rb | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/ee/app/services/ee/protected_branches/create_service.rb b/ee/app/services/ee/protected_branches/create_service.rb index 996bfa910b3faa..17c9ac21b64683 100644 --- a/ee/app/services/ee/protected_branches/create_service.rb +++ b/ee/app/services/ee/protected_branches/create_service.rb @@ -18,6 +18,8 @@ def execute(skip_authorization: false) override :save_protected_branch def save_protected_branch + protected_branch.code_owner_approval_required = false unless project.feature_available?(:code_owner_approval_required) + super sync_code_owner_approval_rules if project.feature_available?(:code_owners) diff --git a/ee/spec/services/ee/protected_branches/create_service_spec.rb b/ee/spec/services/ee/protected_branches/create_service_spec.rb index dedcc409284188..2d810c536102b1 100644 --- a/ee/spec/services/ee/protected_branches/create_service_spec.rb +++ b/ee/spec/services/ee/protected_branches/create_service_spec.rb @@ -25,6 +25,41 @@ target_project.add_user(user, :developer) end + context "code_owner_approval_required" do + context "when unavailable" do + before do + stub_licensed_features(code_owner_approval_required: false) + + params[:code_owner_approval_required] = true + end + + it "ignores incoming params and sets code_owner_approval_required to false" do + expect { service.execute }.to change(ProtectedBranch, :count).by(1) + expect(ProtectedBranch.last.code_owner_approval_required).to be_falsy + end + end + + context "when available" do + before do + stub_licensed_features(code_owner_approval_required: true) + end + + it "sets code_owner_approval_required to true when param is true" do + params[:code_owner_approval_required] = true + + expect { service.execute }.to change(ProtectedBranch, :count).by(1) + expect(ProtectedBranch.last.code_owner_approval_required).to be_truthy + end + + it "sets code_owner_approval_required to false when param is false" do + params[:code_owner_approval_required] = false + + expect { service.execute }.to change(ProtectedBranch, :count).by(1) + expect(ProtectedBranch.last.code_owner_approval_required).to be_falsy + end + end + end + context "when there are open merge requests" do let!(:merge_request) do create(:merge_request, -- GitLab From 85b26aee98642af8fac1880e67336c404f201e5d Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Wed, 18 Sep 2019 12:22:17 -0700 Subject: [PATCH 14/21] Check feature availability rather than DB record Since the "code owner approval required" state of a project is no longer a global setting, we want to test this based on feature availabilty. --- .../merge_requests/_code_owner_approval_rules.html.haml | 2 +- .../views/shared/projects/_code_owner_approval_form.html.haml | 2 +- .../views/shared/projects/_code_owner_approval_table.html.haml | 2 +- .../shared/projects/_code_owner_approval_table_head.html.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml b/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml index 6243e28818178c..ee896a67dfd16e 100644 --- a/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml +++ b/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml @@ -1,4 +1,4 @@ -- return unless @project.merge_requests_require_code_owner_approval? +- return unless @project.feature_available?(:code_owner_approval_required) - code_owner_rules = merge_request.code_owner_rules_with_users - return unless code_owner_rules.any? diff --git a/ee/app/views/shared/projects/_code_owner_approval_form.html.haml b/ee/app/views/shared/projects/_code_owner_approval_form.html.haml index a504cac457aa41..b8c2b8e72a5bb8 100644 --- a/ee/app/views/shared/projects/_code_owner_approval_form.html.haml +++ b/ee/app/views/shared/projects/_code_owner_approval_form.html.haml @@ -1,4 +1,4 @@ -- if @project.merge_requests_require_code_owner_approval +- if @project.feature_available?(:code_owner_approval_required) .form-group.row %label.col-md-2.text-right{ for: 'code_owner_approval_required' } = s_("ProtectedBranch|Require approval from code owners:") diff --git a/ee/app/views/shared/projects/_code_owner_approval_table.html.haml b/ee/app/views/shared/projects/_code_owner_approval_table.html.haml index 5d135e9f8da5ce..189f5a149c7499 100644 --- a/ee/app/views/shared/projects/_code_owner_approval_table.html.haml +++ b/ee/app/views/shared/projects/_code_owner_approval_table.html.haml @@ -1,4 +1,4 @@ -- if @project.merge_requests_require_code_owner_approval +- if @project.feature_available?(:code_owner_approval_required) %td %button{ type: 'button', class: "js-project-feature-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", diff --git a/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml b/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml index 017058ee981190..7fe0438944acaf 100644 --- a/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml +++ b/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml @@ -1,3 +1,3 @@ -- if @project.merge_requests_require_code_owner_approval +- if @project.feature_available?(:code_owner_approval_required) %th = s_("ProtectedBranch|Code owner approval") -- GitLab From 61bf396863bf234575b4feeebeaf43dd9f76bd8b Mon Sep 17 00:00:00 2001 From: Kerri Miller Date: Thu, 5 Sep 2019 10:21:30 -0700 Subject: [PATCH 15/21] Migration to move MRRCOA from project to branch Originally this was set up as a post-deploy background migration, taking advantage of `each_batch` to set up jobs for Sidekiq workers to copy the project's `merge_requests_require_code_owner_approval` to each of its protected branch's `code_owner_approval_required` attribute. I initially added a pair of #after_commit hooks to make sure projects get updated while we were asynchronously chewing through the projects and branches. However, I abandoned this, as a database reviewer supplied a faster approach that shouldn't be an issue in terms of the total time required to process the entities. Additional commits (now squashed/fixup'd): + Add changlog entry + Move COAR checks projects -> protected branches + Added EE::ProtectedBranch, and check only branch for COAR, & ignore project Finding COAR status from protected_branches Updating scopes and methods on Project (and updating tests..) to reflect that we want to check the Code Owner Approval Required status of a given project not from an attribute on its DB record, but based on the COAR flag on its protected_branches. Since we no longer respect the attribute as a kind of global state, we have to update specs here to generate associated ProtectedBranches as required by the expectations of the individual tests. --- app/models/protected_branch.rb | 5 ++ ...status_to_protected_branches_in_batches.rb | 46 ++++++++++++++ ee/app/models/approval_wrapped_rule.rb | 7 +-- ee/app/models/concerns/ee/protected_branch.rb | 8 +++ ee/app/models/ee/project.rb | 6 +- .../user_sees_approval_widget_spec.rb | 4 ++ .../user_edits_merge_request_spec.rb | 5 +- ee/spec/lib/gitlab/usage_data_spec.rb | 12 +++- ee/spec/models/approval_wrapped_rule_spec.rb | 15 +---- ee/spec/models/project_spec.rb | 18 ++++-- ...s_to_protected_branches_in_batches_spec.rb | 63 +++++++++++++++++++ 11 files changed, 161 insertions(+), 28 deletions(-) create mode 100644 db/post_migrate/20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb create mode 100644 spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 8769d3eb916b8a..1857a59e01c86d 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -40,6 +40,11 @@ def self.default_branch_protected? def self.protected_refs(project) project.protected_branches.select(:name) end + + def self.branch_requires_code_owner_approval?(project, branch_name) + # NOOP + # + end end ProtectedBranch.prepend_if_ee('EE::ProtectedBranch') diff --git a/db/post_migrate/20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb b/db/post_migrate/20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb new file mode 100644 index 00000000000000..b109f582909e67 --- /dev/null +++ b/db/post_migrate/20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class MigrateCodeOwnerApprovalStatusToProtectedBranchesInBatches < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + BATCH_SIZE = 200 + + class Project < ActiveRecord::Base + include EachBatch + + self.table_name = 'projects' + self.inheritance_column = :_type_disabled + + has_many :protected_branches + end + + class ProtectedBranch < ActiveRecord::Base + include EachBatch + + self.table_name = 'protected_branches' + self.inheritance_column = :_type_disabled + + belongs_to :project + end + + def up + add_concurrent_index :projects, :id, name: "temp_active_projects_with_prot_branches", where: 'archived = false and pending_delete = false and merge_requests_require_code_owner_approval = true' + + ProtectedBranch + .joins(:project) + .where(projects: { archived: false, pending_delete: false, merge_requests_require_code_owner_approval: true }) + .each_batch(of: BATCH_SIZE) do |batch| + batch.update_all(code_owner_approval_required: true) + end + + remove_concurrent_index_by_name(:projects, "temp_active_projects_with_prot_branches") + end + + def down + # noop + # + end +end diff --git a/ee/app/models/approval_wrapped_rule.rb b/ee/app/models/approval_wrapped_rule.rb index 0b7d2f3550d88d..4fa106aac60414 100644 --- a/ee/app/models/approval_wrapped_rule.rb +++ b/ee/app/models/approval_wrapped_rule.rb @@ -92,16 +92,15 @@ def approvals_required def code_owner_approvals_required strong_memoize(:code_owner_approvals_required) do - next 0 unless merge_requests_require_code_owner_approval? + next 0 unless branch_requires_code_owner_approval? approvers.any? ? REQUIRED_APPROVALS_PER_CODE_OWNER_RULE : 0 end end - def merge_requests_require_code_owner_approval? + def branch_requires_code_owner_approval? return false unless project.code_owner_approval_required_available? - project.merge_requests_require_code_owner_approval? || - project.branch_requires_code_owner_approval?(merge_request.target_branch) + ProtectedBranch.branch_requires_code_owner_approval?(project, merge_request.target_branch) end end diff --git a/ee/app/models/concerns/ee/protected_branch.rb b/ee/app/models/concerns/ee/protected_branch.rb index ab79e18fa12909..e6f05f505a4a71 100644 --- a/ee/app/models/concerns/ee/protected_branch.rb +++ b/ee/app/models/concerns/ee/protected_branch.rb @@ -8,6 +8,14 @@ module ProtectedBranch protected_ref_access_levels :unprotect end + class_methods do + def branch_requires_code_owner_approval?(project, branch_name) + return false unless project.code_owner_approval_required_available? + + project.protected_branches.requiring_code_owner_approval.matching(branch_name).any? + end + end + def code_owner_approval_required super && project.code_owner_approval_required_available? end diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index 8f27a315efc06f..bf60cbc4e73eb1 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -115,6 +115,7 @@ module Project scope :github_imported, -> { where(import_type: 'github') } scope :with_protected_branches, -> { joins(:protected_branches) } scope :with_repositories_enabled, -> { joins(:project_feature).where(project_features: { repository_access_level: ::ProjectFeature::ENABLED }) } + -> { joins(:protected_branches).where(protected_branches: { code_owner_approval_required: true }) } scope :with_security_reports_stored, -> { where('EXISTS (?)', ::Vulnerabilities::Occurrence.scoped_project.select(1)) } scope :with_security_reports, -> { where('EXISTS (?)', ::Ci::JobArtifact.security_reports.scoped_project.select(1)) } @@ -397,13 +398,14 @@ def approver_group_ids=(value) end def merge_requests_require_code_owner_approval? - super && code_owner_approval_required_available? + code_owner_approval_required_available? && + protected_branches.requiring_code_owner_approval.any? end def branch_requires_code_owner_approval?(branch_name) return false unless code_owner_approval_required_available? - protected_branches.requiring_code_owner_approval.matching(branch_name).any? + ::ProtectedBranch.branch_requires_code_owner_approval?(self, branch_name) end def require_password_to_approve diff --git a/ee/spec/features/merge_request/user_sees_approval_widget_spec.rb b/ee/spec/features/merge_request/user_sees_approval_widget_spec.rb index a6cd0ce26a1413..5e5e85b84d9376 100644 --- a/ee/spec/features/merge_request/user_sees_approval_widget_spec.rb +++ b/ee/spec/features/merge_request/user_sees_approval_widget_spec.rb @@ -109,7 +109,11 @@ context 'when code owner approval is required' do before do stub_licensed_features(code_owner_approval_required: true, multiple_approval_rules: true) + project.update!(merge_requests_require_code_owner_approval: true) + + allow(ProtectedBranch) + .to receive(:branch_requires_code_owner_approval?).and_return(true) end it 'shows the code owner rule as required' do diff --git a/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb index a6cb06eaff8577..cb908edbf67437 100644 --- a/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb +++ b/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb @@ -18,7 +18,6 @@ let(:project) do create(:project, :custom_repo, - merge_requests_require_code_owner_approval: true, files: { 'docs/CODEOWNERS' => "*.rb @ruby-owner\n*.js @js-owner" }) end @@ -38,6 +37,10 @@ message: 'Add a ruby file', branch_name: 'feature') + create(:protected_branch, + code_owner_approval_required: true, + project: project) + # To make sure the rules are created for the merge request, the services # that do that aren't triggered from factories MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute diff --git a/ee/spec/lib/gitlab/usage_data_spec.rb b/ee/spec/lib/gitlab/usage_data_spec.rb index 7904e72cc8d3e2..ac22f3c9a14e1a 100644 --- a/ee/spec/lib/gitlab/usage_data_spec.rb +++ b/ee/spec/lib/gitlab/usage_data_spec.rb @@ -194,9 +194,15 @@ describe 'code owner approval required' do before do - create(:project, :archived, :requiring_code_owner_approval) - create(:project, :requiring_code_owner_approval, pending_delete: true) - create(:project, :requiring_code_owner_approval) + create(:protected_branch, code_owner_approval_required: true) + + create(:protected_branch, + code_owner_approval_required: true, + project: create(:project, :archived)) + + create(:protected_branch, + code_owner_approval_required: true, + project: create(:project, pending_delete: true)) end it 'counts the projects actively requiring code owner approval' do diff --git a/ee/spec/models/approval_wrapped_rule_spec.rb b/ee/spec/models/approval_wrapped_rule_spec.rb index a1fbfae1ee0bf3..a65bc507375b15 100644 --- a/ee/spec/models/approval_wrapped_rule_spec.rb +++ b/ee/spec/models/approval_wrapped_rule_spec.rb @@ -196,23 +196,12 @@ .to receive(:code_owner_approval_required_available?).and_return(true) end - context "when no protected_branches require code owner approval" do - it 'returns the correct number of approvals' do - allow(subject.project) - .to receive(:merge_requests_require_code_owner_approval?).and_return(feature_enabled) - allow(subject.project) - .to receive(:branch_requires_code_owner_approval?).with(branch.name).and_return(false) - - expect(subject.approvals_required).to eq(expected_required_approvals) - end - end - context "when the project doesn't require code owner approval on all MRs" do it 'returns the expected number of approvals for protected_branches that do require approval' do allow(subject.project) .to receive(:merge_requests_require_code_owner_approval?).and_return(false) - allow(subject.project) - .to receive(:branch_requires_code_owner_approval?).with(branch.name).and_return(feature_enabled) + allow(ProtectedBranch) + .to receive(:branch_requires_code_owner_approval?).with(subject.project, branch.name).and_return(feature_enabled) expect(subject.approvals_required).to eq(expected_required_approvals) end diff --git a/ee/spec/models/project_spec.rb b/ee/spec/models/project_spec.rb index 49b29870d89230..0a9b4262cb287e 100644 --- a/ee/spec/models/project_spec.rb +++ b/ee/spec/models/project_spec.rb @@ -45,11 +45,15 @@ context 'scopes' do describe '.requiring_code_owner_approval' do + let!(:project) { create(:project) } + let!(:expected_project) { protected_branch_needing_approval.project } + let!(:protected_branch_needing_approval) { create(:protected_branch, code_owner_approval_required: true) } + it 'only includes the right projects' do - create(:project) - expected_project = create(:project, merge_requests_require_code_owner_approval: true) + scoped_query_result = described_class.requiring_code_owner_approval - expect(described_class.requiring_code_owner_approval).to contain_exactly(expected_project) + expect(described_class.count).to eq(2) + expect(scoped_query_result).to contain_exactly(expected_project) end end @@ -1016,13 +1020,17 @@ true | true | true false | true | false true | false | false - true | nil | false end with_them do before do stub_licensed_features(code_owner_approval_required: feature_available) - project.merge_requests_require_code_owner_approval = feature_enabled + + if feature_enabled + create(:protected_branch, + project: project, + code_owner_approval_required: true) + end end it 'requires code owner approval when needed' do diff --git a/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb b/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb new file mode 100644 index 00000000000000..67ac40d4d3909b --- /dev/null +++ b/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb') + +describe MigrateCodeOwnerApprovalStatusToProtectedBranchesInBatches, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:protected_branches) { table(:protected_branches) } + + let(:namespace) do + namespaces.create!( + path: 'gitlab-instance-administrators', + name: 'GitLab Instance Administrators' + ) + end + + let(:project) do + projects.create!( + namespace_id: namespace.id, + name: 'GitLab Instance Administration' + ) + end + + let!(:protected_branch_1) do + protected_branches.create!( + name: "branch name", + project_id: project.id + ) + end + + describe '#up' do + context "when there's no projects needing approval" do + it "doesn't change any protected branch records" do + expect { migrate! } + .not_to change { ProtectedBranch.where(code_owner_approval_required: true).count } + end + end + + context "when there's a project needing approval" do + let!(:project_needing_approval) do + projects.create!( + namespace_id: namespace.id, + name: 'GitLab Instance Administration', + merge_requests_require_code_owner_approval: true + ) + end + + let!(:protected_branch_2) do + protected_branches.create!( + name: "branch name", + project_id: project_needing_approval.id + ) + end + + it "changes N protected branch records" do + expect { migrate! } + .to change { ProtectedBranch.where(code_owner_approval_required: true).count } + .by(1) + end + end + end +end -- GitLab From 3edfc45b2a4f8d1bbafda6873f7ea65e403a7904 Mon Sep 17 00:00:00 2001 From: samantha-dev Date: Mon, 30 Sep 2019 14:55:41 -0700 Subject: [PATCH 16/21] Rename toggle class & add more specs --- .../shared/_branches_list.html.haml | 4 +- .../protected_branch_create.js | 11 ++-- .../protected_branch_edit.js | 14 ++-- .../_code_owner_approval_form.html.haml | 2 +- .../_code_owner_approval_table.html.haml | 2 +- .../user_manages_approval_settings_spec.rb | 18 ----- ee/spec/features/protected_branches_spec.rb | 66 +++++++++++++++---- 7 files changed, 71 insertions(+), 46 deletions(-) diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 47eac175c93f67..8a1a115ac31480 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,7 +1,7 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? .card-header.bg-white - = (s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }).html_safe + = s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count } %p.settings-message.text-center = s_("ProtectedBranch|There are currently no protected branches, protect a branch with the form above.") - else @@ -16,7 +16,7 @@ %thead %tr %th - = (s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }).html_safe + = s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count } %th = s_("ProtectedBranch|Last commit") %th diff --git a/ee/app/assets/javascripts/protected_branches/protected_branch_create.js b/ee/app/assets/javascripts/protected_branches/protected_branch_create.js index e696af69e27a3a..c54dd2cf92b33b 100644 --- a/ee/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/ee/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -14,18 +14,17 @@ export default class ProtectedBranchCreate { this.currentProjectUserDefaults = {}; this.buildDropdowns(); this.$branchInput = this.$form.find('input[name="protected_branch[name]"]'); - this.$toggleButton = this.$form.find('.js-project-feature-toggle'); + this.$codeOwnerToggle = this.$form.find('.js-code-owner-toggle'); this.bindEvents(); } bindEvents() { - this.$toggleButton.on('click', this.onToggleButtonClick.bind(this)); + this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this)); this.$form.on('submit', this.onFormSubmit.bind(this)); } - onToggleButtonClick() { - const toggleButton = this.$toggleButton; - toggleButton.toggleClass('is-checked'); + onCodeOwnerToggleClick() { + this.$codeOwnerToggle.toggleClass('is-checked'); } buildDropdowns() { @@ -82,7 +81,7 @@ export default class ProtectedBranchCreate { authenticity_token: this.$form.find('input[name="authenticity_token"]').val(), protected_branch: { name: this.$form.find('input[name="protected_branch[name]"]').val(), - code_owner_approval_required: this.$toggleButton.hasClass('is-checked'), + code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'), }, }; diff --git a/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js b/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js index 91c5617e4e572c..d94c42c20d1d82 100644 --- a/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -12,7 +12,7 @@ export default class ProtectedBranchEdit { this.$wrap = options.$wrap; this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - this.$toggleButton = this.$wrap.find('.js-project-feature-toggle'); + this.$codeOwnerToggle = this.$wrap.find('.js-code-owner-toggle'); this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest( `.${ACCESS_LEVELS.MERGE}-container`, @@ -26,15 +26,15 @@ export default class ProtectedBranchEdit { } bindEvents() { - this.$toggleButton.on('click', this.onToggleButtonClick.bind(this)); + this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this)); } - onToggleButtonClick() { - this.$toggleButton.toggleClass('is-checked'); - this.$toggleButton.prop('disabled', true); + onCodeOwnerToggleClick() { + this.$codeOwnerToggle.toggleClass('is-checked'); + this.$codeOwnerToggle.prop('disabled', true); const formData = { - code_owner_approval_required: this.$toggleButton.hasClass('is-checked'), + code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'), }; this.updateCodeOwnerApproval(formData); @@ -46,7 +46,7 @@ export default class ProtectedBranchEdit { protected_branch: formData, }) .then(() => { - this.$toggleButton.prop('disabled', false); + this.$codeOwnerToggle.prop('disabled', false); }) .catch(() => { Flash(__('Failed to update branch!')); diff --git a/ee/app/views/shared/projects/_code_owner_approval_form.html.haml b/ee/app/views/shared/projects/_code_owner_approval_form.html.haml index b8c2b8e72a5bb8..bea2e694d25aed 100644 --- a/ee/app/views/shared/projects/_code_owner_approval_form.html.haml +++ b/ee/app/views/shared/projects/_code_owner_approval_form.html.haml @@ -4,7 +4,7 @@ = s_("ProtectedBranch|Require approval from code owners:") .col-md-10 %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle is-checked", + class: "js-code-owner-toggle project-feature-toggle is-checked", "aria-label": s_("ProtectedBranch|Toggle code owner approval") } %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') diff --git a/ee/app/views/shared/projects/_code_owner_approval_table.html.haml b/ee/app/views/shared/projects/_code_owner_approval_table.html.haml index 189f5a149c7499..7c62aa17c41876 100644 --- a/ee/app/views/shared/projects/_code_owner_approval_table.html.haml +++ b/ee/app/views/shared/projects/_code_owner_approval_table.html.haml @@ -1,7 +1,7 @@ - if @project.feature_available?(:code_owner_approval_required) %td %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", + class: "js-code-owner-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", "aria-label": s_("ProtectedBranch|Toggle code owner approval") } %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') diff --git a/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb b/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb index 395d2226258f22..17f64d10e6e040 100644 --- a/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb +++ b/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb @@ -34,24 +34,6 @@ end end - context 'when `code_owner_approval_required` is available' do - let(:licensed_features) { { code_owner_approval_required: true } } - - it_behaves_like 'dirty submit form', [{ form: '#js-merge-request-approval-settings', input: '#project_merge_requests_author_approval' }] - - it 'allows the user to enforce code owner approval' do - within('.require-code-owner-approval') do - check('Require approval from code owners') - end - - within('.merge-request-approval-settings-form') do - click_on('Save changes') - end - - expect(project.reload.merge_requests_require_code_owner_approval?).to be_truthy - end - end - context 'when `code_owner_approval_required` is not available' do let(:licensed_features) { { code_owner_approval_required: false } } diff --git a/ee/spec/features/protected_branches_spec.rb b/ee/spec/features/protected_branches_spec.rb index 7b55a18885e80c..df7c0bcf532fbd 100644 --- a/ee/spec/features/protected_branches_spec.rb +++ b/ee/spec/features/protected_branches_spec.rb @@ -16,16 +16,62 @@ describe 'when project requires code owner approval' do before do stub_licensed_features(protected_refs_for_users: true, code_owner_approval_required: true) - project.update!(merge_requests_require_code_owner_approval: true) end describe 'protect a branch form' do let!(:protected_branch) { create(:protected_branch, project: project) } + let(:container) { page.find('#new_protected_branch') } + let(:code_owner_toggle) { container.find('.js-code-owner-toggle') } + let(:branch_input) { container.find('.js-protected-branch-select') } + let(:allowed_to_merge_input) { container.find('.js-allowed-to-merge') } + let(:allowed_to_push) { container.find('.js-allowed-to-push') } - it 'has code owner toggle' do + before do visit project_settings_repository_path(project) + end + + def fill_in_form(branch_name) + branch_input.click + click_on branch_name + + allowed_to_merge_input.click + wait_for_requests + page.find('.dropdown.show').click_on 'No one' + + allowed_to_push.click + wait_for_requests + page.find('.dropdown.show').click_on 'No one' + end + + def submit_form + click_on 'Protect' + wait_for_requests + end + it 'has code owner toggle' do expect(page).to have_content("Require approval from code owners") + expect(code_owner_toggle[:class]).to include("is-checked") + end + + it 'can create new protected branch with code owner disabled' do + fill_in_form "with-codeowners" + + code_owner_toggle.click + expect(code_owner_toggle[:class]).not_to include("is-checked") + + submit_form + + expect(project.protected_branches.find_by_name("with-codeowners").code_owner_approval_required).to be(false) + end + + it 'can create new protected branch with code owner enabled' do + fill_in_form "with-codeowners" + + expect(code_owner_toggle[:class]).to include("is-checked") + + submit_form + + expect(project.protected_branches.find_by_name("with-codeowners").code_owner_approval_required).to be(true) end end @@ -33,16 +79,16 @@ context 'has a protected branch with code owner approval toggled on' do let!(:protected_branch) { create(:protected_branch, project: project, code_owner_approval_required: true) } - it 'shows code owner approval toggle' do + before do visit project_settings_repository_path(project) + end + it 'shows code owner approval toggle' do expect(page).to have_content("Code owner approval") end it 'displays toggle on' do - visit project_settings_repository_path(project) - - expect(page).to have_css('.js-project-feature-toggle.is-checked') + expect(page).to have_css('.js-code-owner-toggle.is-checked') end end @@ -53,7 +99,7 @@ visit project_settings_repository_path(project) page.within '.qa-protected-branches-list' do - expect(page).not_to have_css('.js-project-feature-toggle.is-checked') + expect(page).not_to have_css('.js-code-owner-toggle.is-checked') end end end @@ -63,17 +109,15 @@ describe 'when project does not require code owner approval' do before do stub_licensed_features(protected_refs_for_users: true, code_owner_approval_required: false) - end - it 'does not have code owner approval in the form' do visit project_settings_repository_path(project) + end + it 'does not have code owner approval in the form' do expect(page).not_to have_content("Require approval from code owners") end it 'does not have code owner approval in the table' do - visit project_settings_repository_path(project) - expect(page).not_to have_content("Code owner approval") end end -- GitLab From c1df9d94f005f6b9bd864dc47b2967bb91b55570 Mon Sep 17 00:00:00 2001 From: samantha-dev Date: Mon, 30 Sep 2019 16:07:23 -0700 Subject: [PATCH 17/21] Moved code_owner_approval_* partial to protected_branches folder --- .../projects/protected_branches/shared/_branches_list.html.haml | 2 +- .../shared/_create_protected_branch.html.haml | 2 +- .../protected_branches/shared/_protected_branch.html.haml | 2 +- .../protected_branches/ee}/_code_owner_approval_form.html.haml | 0 .../protected_branches/ee}/_code_owner_approval_table.html.haml | 0 .../ee}/_code_owner_approval_table_head.html.haml | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename ee/app/views/{shared/projects => projects/protected_branches/ee}/_code_owner_approval_form.html.haml (100%) rename ee/app/views/{shared/projects => projects/protected_branches/ee}/_code_owner_approval_table.html.haml (100%) rename ee/app/views/{shared/projects => projects/protected_branches/ee}/_code_owner_approval_table_head.html.haml (100%) diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 8a1a115ac31480..ff8dae08ad0530 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -24,7 +24,7 @@ %th = s_("ProtectedBranch|Allowed to push") - = render_if_exists 'shared/projects/code_owner_approval_table_head' + = render_if_exists 'projects/protected_branches/ee/code_owner_approval_table_head' - if can_admin_project %th diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index 79f52cb020cad0..f84c7b39733e69 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -24,6 +24,6 @@ = s_("ProtectedBranch|Allowed to push:") .col-md-10 = yield :push_access_levels - = render_if_exists 'shared/projects/code_owner_approval_form' + = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form' .card-footer = f.submit s_('ProtectedBranch|Protect'), class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' } diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 53c16392357351..2768e4ac5a546c 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -19,7 +19,7 @@ = yield - = render_if_exists 'shared/projects/code_owner_approval_table', protected_branch: protected_branch + = render_if_exists 'projects/protected_branches/ee/code_owner_approval_table', protected_branch: protected_branch - if can_admin_project %td diff --git a/ee/app/views/shared/projects/_code_owner_approval_form.html.haml b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_form.html.haml similarity index 100% rename from ee/app/views/shared/projects/_code_owner_approval_form.html.haml rename to ee/app/views/projects/protected_branches/ee/_code_owner_approval_form.html.haml diff --git a/ee/app/views/shared/projects/_code_owner_approval_table.html.haml b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table.html.haml similarity index 100% rename from ee/app/views/shared/projects/_code_owner_approval_table.html.haml rename to ee/app/views/projects/protected_branches/ee/_code_owner_approval_table.html.haml diff --git a/ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table_head.html.haml similarity index 100% rename from ee/app/views/shared/projects/_code_owner_approval_table_head.html.haml rename to ee/app/views/projects/protected_branches/ee/_code_owner_approval_table_head.html.haml -- GitLab From c6d9c117049e8151ca82593678189f7c9718b54a Mon Sep 17 00:00:00 2001 From: samantha-dev Date: Tue, 1 Oct 2019 19:38:37 -0700 Subject: [PATCH 18/21] Fix pipeline failure due to rebase --- ee/app/models/ee/project.rb | 3 +-- ee/spec/lib/ee/gitlab/usage_data_spec.rb | 2 +- qa/qa/page/project/settings/merge_request.rb | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index bf60cbc4e73eb1..11c3fa9d8eb07d 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -106,7 +106,7 @@ module Project scope :verification_failed_wikis, -> { joins(:repository_state).merge(ProjectRepositoryState.verification_failed_wikis) } scope :for_plan_name, -> (name) { joins(namespace: :plan).where(plans: { name: name }) } scope :requiring_code_owner_approval, - -> { where(merge_requests_require_code_owner_approval: true) } + -> { joins(:protected_branches).where(protected_branches: { code_owner_approval_required: true }) } scope :with_active_services, -> { joins(:services).merge(::Service.active) } scope :with_active_jira_services, -> { joins(:services).merge(::JiraService.active) } scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) } @@ -115,7 +115,6 @@ module Project scope :github_imported, -> { where(import_type: 'github') } scope :with_protected_branches, -> { joins(:protected_branches) } scope :with_repositories_enabled, -> { joins(:project_feature).where(project_features: { repository_access_level: ::ProjectFeature::ENABLED }) } - -> { joins(:protected_branches).where(protected_branches: { code_owner_approval_required: true }) } scope :with_security_reports_stored, -> { where('EXISTS (?)', ::Vulnerabilities::Occurrence.scoped_project.select(1)) } scope :with_security_reports, -> { where('EXISTS (?)', ::Ci::JobArtifact.security_reports.scoped_project.select(1)) } diff --git a/ee/spec/lib/ee/gitlab/usage_data_spec.rb b/ee/spec/lib/ee/gitlab/usage_data_spec.rb index ec5fa90eb5967f..dd30d0868cf937 100644 --- a/ee/spec/lib/ee/gitlab/usage_data_spec.rb +++ b/ee/spec/lib/ee/gitlab/usage_data_spec.rb @@ -57,7 +57,7 @@ deploy_keys: 1, keys: 1, merge_requests: 1, - projects_enforcing_code_owner_approval: 1, + projects_enforcing_code_owner_approval: 0, projects_imported_from_github: 1, projects_with_repositories_enabled: 1, protected_branches: 1, diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index 7da2c9d168cca1..752d582aa6dc10 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -28,5 +28,3 @@ def enable_ff_only end end end - -QA::Page::Project::Settings::MergeRequest.prepend_if_ee("QA::EE::Page::Project::Settings::MergeRequest") -- GitLab From a00dedf52582393527926a7cef5336b623d3ed92 Mon Sep 17 00:00:00 2001 From: samantha-dev Date: Fri, 4 Oct 2019 12:58:52 -0700 Subject: [PATCH 19/21] Adjust approval check per mr comment --- .../merge_requests/_code_owner_approval_rules.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml b/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml index ee896a67dfd16e..b20b98f5104dc0 100644 --- a/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml +++ b/ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml @@ -1,4 +1,4 @@ -- return unless @project.feature_available?(:code_owner_approval_required) +- return unless ProtectedBranch.branch_requires_code_owner_approval?(@project, merge_request.target_branch) - code_owner_rules = merge_request.code_owner_rules_with_users - return unless code_owner_rules.any? -- GitLab From 7566023d1c652b0e9ca67f0ca9ab2ff22f41932e Mon Sep 17 00:00:00 2001 From: Samantha Ming Date: Tue, 8 Oct 2019 12:59:25 -0700 Subject: [PATCH 20/21] Apply patch to fix pipeline --- .../user_edits_merge_request_spec.rb | 1 + .../protected_branch_edit_spec.js | 90 +++++++++++++++++++ .../issuable/_approvals.html.haml_spec.rb | 1 + 3 files changed, 92 insertions(+) create mode 100644 ee/spec/frontend/protected_branches/protected_branch_edit_spec.js diff --git a/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb index cb908edbf67437..52ab52783456e7 100644 --- a/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb +++ b/ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb @@ -38,6 +38,7 @@ branch_name: 'feature') create(:protected_branch, + name: 'master', code_owner_approval_required: true, project: project) diff --git a/ee/spec/frontend/protected_branches/protected_branch_edit_spec.js b/ee/spec/frontend/protected_branches/protected_branch_edit_spec.js new file mode 100644 index 00000000000000..79f95cc6b177cd --- /dev/null +++ b/ee/spec/frontend/protected_branches/protected_branch_edit_spec.js @@ -0,0 +1,90 @@ +import $ from 'jquery'; +import MockAdapter from 'axios-mock-adapter'; +import ProtectedBranchEdit from 'ee/protected_branches/protected_branch_edit'; +import flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { TEST_HOST } from 'helpers/test_constants'; + +jest.mock('~/flash'); + +const TEST_URL = `${TEST_HOST}/url`; +const IS_CHECKED_CLASS = 'is-checked'; + +describe('EE ProtectedBranchEdit', () => { + let mock; + + beforeEach(() => { + setFixtures(`

+ +
`); + + jest.spyOn(ProtectedBranchEdit.prototype, 'buildDropdowns').mockImplementation(); + + mock = new MockAdapter(axios); + }); + + const findCodeOwnerToggle = () => document.querySelector('.js-code-owner-toggle'); + + const create = ({ isChecked = false }) => { + if (isChecked) { + findCodeOwnerToggle().classList.add(IS_CHECKED_CLASS); + } + + return new ProtectedBranchEdit({ $wrap: $('#wrap') }); + }; + + afterEach(() => { + mock.restore(); + }); + + describe('when unchecked toggle button', () => { + let toggle; + + beforeEach(() => { + create({ isChecked: false }); + + toggle = findCodeOwnerToggle(); + }); + + it('is not changed', () => { + expect(toggle).not.toHaveClass(IS_CHECKED_CLASS); + expect(toggle).not.toBeDisabled(); + }); + + describe('when clicked', () => { + beforeEach(() => { + mock + .onPatch(TEST_URL, { protected_branch: { code_owner_approval_required: true } }) + .replyOnce(200, {}); + + toggle.click(); + }); + + it('checks and disables button', () => { + expect(toggle).toHaveClass(IS_CHECKED_CLASS); + expect(toggle).toBeDisabled(); + }); + + it('sends update to BE', () => + axios.waitForAll().then(() => { + // Args are asserted in the `.onPatch` call + expect(mock.history.patch.length).toEqual(1); + + expect(toggle).not.toBeDisabled(); + expect(flash).not.toHaveBeenCalled(); + })); + }); + + describe('when clikced and BE error', () => { + beforeEach(() => { + mock.onPatch(TEST_URL).replyOnce(500); + toggle.click(); + }); + + it('flashes error', () => + axios.waitForAll().then(() => { + expect(flash).toHaveBeenCalled(); + })); + }); + }); +}); diff --git a/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb b/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb index 068261d4196c02..9f6497e633198c 100644 --- a/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb +++ b/ee/spec/views/shared/issuable/_approvals.html.haml_spec.rb @@ -18,6 +18,7 @@ allow(MergeRequestApproverPresenter).to receive(:new).and_return(approver_presenter) assign(:project, project) assign(:target_project, project) + assign(:mr_presenter, merge_request.present(current_user: user)) end context 'has no approvers' do -- GitLab From 6c4ce3eae4029c05b3b796be82000614fabd16eb Mon Sep 17 00:00:00 2001 From: Paul Slaughter Date: Tue, 8 Oct 2019 16:08:42 -0500 Subject: [PATCH 21/21] Add back qa lines --- qa/qa/ee.rb | 1 + qa/qa/page/project/settings/merge_request.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/qa/qa/ee.rb b/qa/qa/ee.rb index afc6bed53b094c..f57dc5fcdbc796 100644 --- a/qa/qa/ee.rb +++ b/qa/qa/ee.rb @@ -114,6 +114,7 @@ module Settings autoload :ProtectedBranches, 'qa/ee/page/project/settings/protected_branches' autoload :MirroringRepositories, 'qa/ee/page/project/settings/mirroring_repositories' autoload :Main, 'qa/ee/page/project/settings/main' + autoload :MergeRequest, 'qa/ee/page/project/settings/merge_request' autoload :Repository, 'qa/ee/page/project/settings/repository' autoload :PushRules, 'qa/ee/page/project/settings/push_rules' end diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index 752d582aa6dc10..7da2c9d168cca1 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -28,3 +28,5 @@ def enable_ff_only end end end + +QA::Page::Project::Settings::MergeRequest.prepend_if_ee("QA::EE::Page::Project::Settings::MergeRequest") -- GitLab