diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index c5454883060bb27aef5bd06b116a94f53d02e204..d4f7d0bc52155fc0e8f422b675d9747800b49b74 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/models/protected_branch.rb b/app/models/protected_branch.rb
index 8769d3eb916b8a8e05c839b4ca5d5cb29ab5e7f3..1857a59e01c86d8d0190fc666f0ec0afc0e82542 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/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 1913d06a6f87f042785bf85b21f6672550b83a27..ff8dae08ad0530516abf7c1960bcf99d11d44b20 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 }
%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
@@ -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 }
+ %th
+ = s_("ProtectedBranch|Last commit")
+ %th
+ = s_("ProtectedBranch|Allowed to merge")
+ %th
+ = s_("ProtectedBranch|Allowed to push")
+
+ = render_if_exists 'projects/protected_branches/ee/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 bba4949277d449a81b68dbfbcc4d27e34b37da00..f84c7b39733e69d98fa73dd24cb6be95ee2a06f1 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,19 @@
.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
-
+ = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form'
.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 81dcab1d1ab440305aab2f9c568fcbc6d8f7eded..2768e4ac5a546c92742ebe9d68d3c430e95b0bd6 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,8 @@
= yield
+ = render_if_exists 'projects/protected_branches/ee/code_owner_approval_table', protected_branch: protected_branch
+
- 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/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 0000000000000000000000000000000000000000..b109f582909e67aae5e0b8ca8f2c5cefc4c85b1d
--- /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/doc/api/protected_branches.md b/doc/api/protected_branches.md
index debf1b264f9e36d32c58d12960b9baf6c88f7b3f..4a750b42f65f878f1994dab8771616fbe82b996b 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -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
+
+Update 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)|
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
Binary files a/doc/user/project/img/protected_branches_list_v12_3.png and b/doc/user/project/img/protected_branches_list_v12_3.png differ
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
Binary files a/doc/user/project/img/protected_branches_page_v12_3.png and b/doc/user/project/img/protected_branches_page_v12_3.png differ
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 9a6cd3dc3626630a92199f6e6c36c450aadd6824..1bd272bdd0c0e098638f76faf6c5a0b94024f11b 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -86,6 +86,20 @@ 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
+[`CODEOWNERS` file](code_owners.md) that matches a file changed in
+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.
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 973e256a1f9dacce0f249d21c5ee61b599b1f683..c54dd2cf92b33b33b716c3e7c1ab9c5d5d51254a 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,19 @@ export default class ProtectedBranchCreate {
this.currentProjectUserDefaults = {};
this.buildDropdowns();
this.$branchInput = this.$form.find('input[name="protected_branch[name]"]');
+ this.$codeOwnerToggle = this.$form.find('.js-code-owner-toggle');
this.bindEvents();
}
bindEvents() {
+ this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
this.$form.on('submit', this.onFormSubmit.bind(this));
}
+ onCodeOwnerToggleClick() {
+ this.$codeOwnerToggle.toggleClass('is-checked');
+ }
+
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
@@ -75,6 +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.$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 6eb7541501968565c21fb821059a8d0616ae4797..d94c42c20d1d82a858307cb6419cf02fab8c03f7 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.$codeOwnerToggle = this.$wrap.find('.js-code-owner-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.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
+ }
+
+ onCodeOwnerToggleClick() {
+ this.$codeOwnerToggle.toggleClass('is-checked');
+ this.$codeOwnerToggle.prop('disabled', true);
+
+ const formData = {
+ code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
+ };
+
+ this.updateCodeOwnerApproval(formData);
+ }
+
+ updateCodeOwnerApproval(formData) {
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_branch: formData,
+ })
+ .then(() => {
+ this.$codeOwnerToggle.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/app/controllers/ee/projects/protected_branches_controller.rb b/ee/app/controllers/ee/projects/protected_branches_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f1964210371557990855b35b35da268c291ed0ff
--- /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/app/models/approval_wrapped_rule.rb b/ee/app/models/approval_wrapped_rule.rb
index 0b7d2f3550d88d53cb8fb6fd4da6374e5ad2be61..4fa106aac60414e01312b0de10c3f3dc04ec4d45 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 ab79e18fa12909c705d86bbb571d0d99b5456dda..e6f05f505a4a717c4f58916f806e0659a26857b2 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 8f27a315efc06f5038614ad023ac80d23ec8acc2..11c3fa9d8eb07d8b983797124bffc231a1fcd94a 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)) }
@@ -397,13 +397,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/app/services/ee/protected_branches/create_service.rb b/ee/app/services/ee/protected_branches/create_service.rb
index 996bfa910b3faaa18f6b114ead636db70cb3c7d2..17c9ac21b64683a5dba46d89e069a767c49441ab 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/app/views/projects/_merge_request_approvals_settings_form.html.haml b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml
index 49179755d8ebc48f5b6ad52cb73b805c9e0b2f82..8800650f4297058358dc2034085898f84fca66ac 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)
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 6243e28818178c1435035fb1415924fa3bf21ff9..b20b98f5104dc0d31c916bf91fb80a7575e6614a 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 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?
diff --git a/ee/app/views/projects/protected_branches/ee/_code_owner_approval_form.html.haml b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..bea2e694d25aedfe4c3bc79e8b63b7fc81538aeb
--- /dev/null
+++ b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_form.html.haml
@@ -0,0 +1,13 @@
+- 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:")
+ .col-md-10
+ %button{ type: 'button',
+ 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')
+ = 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/projects/protected_branches/ee/_code_owner_approval_table.html.haml b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7c62aa17c41876e49150dcad66eb3e766a3d4a2b
--- /dev/null
+++ b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table.html.haml
@@ -0,0 +1,8 @@
+- if @project.feature_available?(:code_owner_approval_required)
+ %td
+ %button{ type: 'button',
+ 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')
+ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
diff --git a/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table_head.html.haml b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7fe0438944acafec563c7c3341ec2d08693ca7ff
--- /dev/null
+++ b/ee/app/views/projects/protected_branches/ee/_code_owner_approval_table_head.html.haml
@@ -0,0 +1,3 @@
+- if @project.feature_available?(:code_owner_approval_required)
+ %th
+ = s_("ProtectedBranch|Code owner approval")
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 0000000000000000000000000000000000000000..318da606dce978a17d4c2e92d1a2271895fe21d7
--- /dev/null
+++ b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml
@@ -0,0 +1,5 @@
+---
+title: Add configurable Code Owner approvals for protected branches
+merge_request: 15862
+author:
+type: added
diff --git a/ee/lib/ee/api/protected_branches.rb b/ee/lib/ee/api/protected_branches.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f37a7364a38294f7b563f4bcec93a502ced2904
--- /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!("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])
+
+ 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 0000000000000000000000000000000000000000..828b3d075a5244c561a17b599995295c4812c409
--- /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/merge_request/user_sees_approval_widget_spec.rb b/ee/spec/features/merge_request/user_sees_approval_widget_spec.rb
index a6cd0ce26a1413cca5c8cc2777b836c6ee612f7b..5e5e85b84d9376dbbc097a4a5acca43bc7a90fe9 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 a6cb06eaff8577c2b3ba61e7b3ccc981bf8e5df0..52ab52783456e772924b2155d2c5cee1cbe99315 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,11 @@
message: 'Add a ruby file',
branch_name: 'feature')
+ create(:protected_branch,
+ name: 'master',
+ 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/features/projects/settings/user_manages_approval_settings_spec.rb b/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb
index 395d2226258f22cca1c465821c0007bfda775357..17f64d10e6e040c99472027669cd3e84af4f597f 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 e64e4c7074a020d8f0325b885310047c3c1333e8..df7c0bcf532fbd2f19af213ee6c898d983b7b207 100644
--- a/ee/spec/features/protected_branches_spec.rb
+++ b/ee/spec/features/protected_branches_spec.rb
@@ -12,6 +12,117 @@
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)
+ 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') }
+
+ 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
+
+ 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) }
+
+ 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
+ expect(page).to have_css('.js-code-owner-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-code-owner-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)
+
+ 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
+ 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/frontend/protected_branches/protected_branch_edit_spec.js b/ee/spec/frontend/protected_branches/protected_branch_edit_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..79f95cc6b177cd931f1ea2f6996432dff5b247f5
--- /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/lib/ee/gitlab/usage_data_spec.rb b/ee/spec/lib/ee/gitlab/usage_data_spec.rb
index ec5fa90eb5967f4c66dd28d5dfbbd8254e5d668b..dd30d0868cf937abe40dbc401fd137cf8547c522 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/ee/spec/lib/gitlab/usage_data_spec.rb b/ee/spec/lib/gitlab/usage_data_spec.rb
index 7904e72cc8d3e25dee419a56458be4668531b9ea..ac22f3c9a14e1ae4d0e4d645821aefb17a75778f 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 a1fbfae1ee0bf34be47956bb4ad158605754a172..a65bc507375b15b3c9af61137895603355e13704 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 49b29870d892304de61ca37b31efb1c4b75afe3d..0a9b4262cb287e63ab439a1d052b11d5e4efffaa 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/ee/spec/requests/api/protected_branches_spec.rb b/ee/spec/requests/api/protected_branches_spec.rb
index 9721e5b225cdabb0ad8bc4ad49fd11c716774d3e..ebddcf61b95dc2cb78ef5a06d3da95399e60492e 100644
--- a/ee/spec/requests/api/protected_branches_spec.rb
+++ b/ee/spec/requests/api/protected_branches_spec.rb
@@ -82,6 +82,57 @@
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
+ 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(403)
+ 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/ee/spec/services/ee/protected_branches/create_service_spec.rb b/ee/spec/services/ee/protected_branches/create_service_spec.rb
index dedcc409284188db7b19bbd929540b7736e70f0d..2d810c536102b1d3cf71500a9bc28866cdfb5a0b 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,
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 068261d4196c029866326fb7f1f3a79b8c84793f..9f6497e633198c347a5551a33d61e2660db69cec 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
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index ca75ee906ce0c4578fe97b4c86aff4783e600ed2..c7665c20234362bda21533280929bdeac63e03ea 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')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a17d0460967101bae3374ff137f9104fa1bba268..cbde6291514327bacaaec6147bf836d0178eb65f 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 ""
@@ -13420,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 ""
diff --git a/qa/qa/ee.rb b/qa/qa/ee.rb
index a706e7da6dfc0f74e0641833e14d4843b0967be7..f57dc5fcdbc79674655a54b4741bfd18a8ba2601 100644
--- a/qa/qa/ee.rb
+++ b/qa/qa/ee.rb
@@ -115,7 +115,6 @@ module Settings
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
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 63b42c9bf9d7bec09bd5f18f76c28f92871e1359..0000000000000000000000000000000000000000
--- 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
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 0000000000000000000000000000000000000000..67ac40d4d3909bb7813759927aeeb9594ed6e618
--- /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