Commit 0ca5b03f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 3ac320ce f7ccd64c
...@@ -26,7 +26,7 @@ export default { ...@@ -26,7 +26,7 @@ export default {
</script> </script>
<template> <template>
<div v-show="draftsCount > 0"> <div v-show="draftsCount > 0">
<nav class="review-bar-component"> <nav class="review-bar-component" data-testid="review_bar_component">
<div <div
class="review-bar-content d-flex gl-justify-content-end" class="review-bar-content d-flex gl-justify-content-end"
data-qa-selector="review_bar_content" data-qa-selector="review_bar_content"
......
...@@ -75,6 +75,13 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ...@@ -75,6 +75,13 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
icon: 'approval', icon: 'approval',
tag: '@approved-by', tag: '@approved-by',
}, },
tokenAlternative: {
formattedKey: __('Approved-By'),
key: 'approved-by',
type: 'string',
param: 'usernames',
symbol: '@',
},
condition: [ condition: [
{ {
url: 'approved_by_usernames[]=None', url: 'approved_by_usernames[]=None',
...@@ -105,7 +112,11 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ...@@ -105,7 +112,11 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const tokenPosition = 3; const tokenPosition = 3;
IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]); IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]);
IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvedBy.token]); IssuableTokenKeys.tokenKeysWithAlternative.splice(
tokenPosition,
0,
...[approvedBy.token, approvedBy.tokenAlternative],
);
IssuableTokenKeys.conditions.push(...approvedBy.condition); IssuableTokenKeys.conditions.push(...approvedBy.condition);
const environmentToken = { const environmentToken = {
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%;
background: $white;
z-index: $zindex-dropdown-menu; z-index: $zindex-dropdown-menu;
padding: 7px 0 6px; // to keep aligned with "collapse sidebar" button on the left sidebar display: flex;
border-top: 1px solid $border-color; align-items: center;
width: 100%;
height: $toggle-sidebar-height;
padding-left: $contextual-sidebar-width; padding-left: $contextual-sidebar-width;
padding-right: $gutter_collapsed_width; padding-right: $gutter_collapsed_width;
background: $white;
border-top: 1px solid $border-color;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
.page-with-icon-sidebar & { .page-with-icon-sidebar & {
......
...@@ -17,7 +17,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -17,7 +17,7 @@ class Projects::ForksController < Projects::ApplicationController
feature_category :source_code_management feature_category :source_code_management
before_action do before_action do
push_frontend_feature_flag(:fork_project_form) push_frontend_feature_flag(:fork_project_form, @project, default_enabled: :yaml)
end end
def index def index
......
...@@ -76,6 +76,7 @@ class MergeRequestsFinder < IssuableFinder ...@@ -76,6 +76,7 @@ class MergeRequestsFinder < IssuableFinder
def filter_negated_items(items) def filter_negated_items(items)
items = super(items) items = super(items)
items = by_negated_reviewer(items) items = by_negated_reviewer(items)
items = by_negated_approved_by(items)
by_negated_target_branch(items) by_negated_target_branch(items)
end end
...@@ -119,6 +120,12 @@ class MergeRequestsFinder < IssuableFinder ...@@ -119,6 +120,12 @@ class MergeRequestsFinder < IssuableFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def by_negated_approved_by(items)
return items unless not_params[:approved_by_usernames]
items.not_approved_by_users_with_usernames(not_params[:approved_by_usernames])
end
def source_project_id def source_project_id
@source_project_id ||= params[:source_project_id].presence @source_project_id ||= params[:source_project_id].presence
end end
......
...@@ -24,6 +24,19 @@ module ApprovableBase ...@@ -24,6 +24,19 @@ module ApprovableBase
.group(:id) .group(:id)
.having("COUNT(users.id) = ?", usernames.size) .having("COUNT(users.id) = ?", usernames.size)
end end
scope :not_approved_by_users_with_usernames, -> (usernames) do
users = User.where(username: usernames).select(:id)
self_table = self.arel_table
app_table = Approval.arel_table
where(
Approval.where(approvals: { user_id: users })
.where(app_table[:merge_request_id].eq(self_table[:id]))
.select('true')
.arel.exists.not
)
end
end end
class_methods do class_methods do
......
...@@ -22,7 +22,7 @@ module Projects ...@@ -22,7 +22,7 @@ module Projects
# Ensure HEAD points to the default branch in case it is not master # Ensure HEAD points to the default branch in case it is not master
project.change_head(default_branch) project.change_head(default_branch)
create_protected_branch if protect_branch? create_protected_branch if protect_branch? && !protected_branch_exists?
end end
def create_protected_branch def create_protected_branch
...@@ -44,6 +44,10 @@ module Projects ...@@ -44,6 +44,10 @@ module Projects
!ProtectedBranch.protected?(project, default_branch) !ProtectedBranch.protected?(project, default_branch)
end end
def protected_branch_exists?
project.protected_branches.find_by_name(default_branch).present?
end
def default_branch def default_branch
project.default_branch project.default_branch
end end
......
...@@ -23,6 +23,10 @@ ...@@ -23,6 +23,10 @@
.home-panel-buttons.col-md-12.col-lg-6 .home-panel-buttons.col-md-12.col-lg-6
- if current_user - if current_user
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } } .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } }
- if current_user.admin?
= link_to [:admin, @group], class: 'btn btn-default gl-button btn-icon gl-mt-3 gl-mr-2', title: s_('View group in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin')
- if @notification_setting - if @notification_setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top' } } .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top' } }
- if can_create_subgroups - if can_create_subgroups
......
...@@ -47,6 +47,10 @@ ...@@ -47,6 +47,10 @@
= cache_if(cache_enabled, [@project, :buttons, current_user, @notification_setting], expires_in: 1.day) do = cache_if(cache_enabled, [@project, :buttons, current_user, @notification_setting], expires_in: 1.day) do
.project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5 .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
- if current_user - if current_user
- if current_user.admin?
= link_to [:admin, @project], class: 'btn gl-button btn-icon gl-align-self-start gl-py-2! gl-mr-3', title: s_('View project in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin')
.gl-display-flex.gl-align-items-start.gl-mr-3 .gl-display-flex.gl-align-items-start.gl-mr-3
- if @notification_setting - if @notification_setting
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } } .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } }
......
- page_title s_("ForkProject|Fork project") - page_title s_("ForkProject|Fork project")
- if Feature.enabled?(:fork_project_form) - if Feature.enabled?(:fork_project_form, @project, default_enabled: :yaml)
#fork-groups-mount-element{ data: { fork_illustration: image_path('illustrations/project-create-new-sm.svg'), #fork-groups-mount-element{ data: { fork_illustration: image_path('illustrations/project-create-new-sm.svg'),
endpoint: new_project_fork_path(@project, format: :json), endpoint: new_project_fork_path(@project, format: :json),
new_group_path: new_group_path, new_group_path: new_group_path,
......
...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321387 ...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321387
milestone: '13.10' milestone: '13.10'
type: development type: development
group: group::source code group: group::source code
default_enabled: false default_enabled: true
...@@ -37,6 +37,13 @@ const approvers = { ...@@ -37,6 +37,13 @@ const approvers = {
icon: 'approval', icon: 'approval',
tag: '@approver', tag: '@approver',
}, },
tokenAlternative: {
formattedKey: __('Approver'),
key: 'approver',
type: 'string',
param: 'usernames',
symbol: '@',
},
}; };
export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
...@@ -44,6 +51,10 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ...@@ -44,6 +51,10 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const tokenPosition = 3; const tokenPosition = 3;
IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvers.token]); IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvers.token]);
IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvers.token]); IssuableTokenKeys.tokenKeysWithAlternative.splice(
tokenPosition,
0,
...[approvers.token, approvers.tokenAlternative],
);
IssuableTokenKeys.conditions.push(...approvers.condition); IssuableTokenKeys.conditions.push(...approvers.condition);
}; };
...@@ -13,7 +13,7 @@ module Security ...@@ -13,7 +13,7 @@ module Security
project.create_security_orchestration_policy_configuration! do |p| project.create_security_orchestration_policy_configuration! do |p|
p.security_policy_management_project_id = policy_project.id p.security_policy_management_project_id = policy_project.id
end end
create_protected_branch(policy_project) create_or_update_protected_branch(policy_project)
members = add_members(policy_project) members = add_members(policy_project)
errors = members.flat_map { |member| member.errors.full_messages } errors = members.flat_map { |member| member.errors.full_messages }
...@@ -25,13 +25,21 @@ module Security ...@@ -25,13 +25,21 @@ module Security
private private
def create_protected_branch(policy_project) def create_or_update_protected_branch(policy_project)
protected_branch = policy_project.protected_branches.find_by_name(policy_project.default_branch_or_main)
params = { params = {
name: policy_project.default_branch_or_main, name: policy_project.default_branch_or_main,
push_access_levels_attributes: [{ access_level: Gitlab::Access::NO_ACCESS }], push_access_levels_attributes: [{ access_level: Gitlab::Access::NO_ACCESS }],
merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]
} }
if protected_branch.present?
ProtectedBranch::UpdateService
.new(policy_project, current_user, params)
.execute(protected_branch)
return
end
ProtectedBranches::CreateService ProtectedBranches::CreateService
.new(policy_project, current_user, params) .new(policy_project, current_user, params)
.execute(skip_authorization: true) .execute(skip_authorization: true)
......
...@@ -23,6 +23,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do ...@@ -23,6 +23,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
expect(project.security_orchestration_policy_configuration.security_policy_management_project).to eq(policy_project) expect(project.security_orchestration_policy_configuration.security_policy_management_project).to eq(policy_project)
expect(policy_project.namespace).to eq(project.namespace) expect(policy_project.namespace).to eq(project.namespace)
expect(policy_project.protected_branches.map(&:name)).to contain_exactly(project.default_branch_or_main) expect(policy_project.protected_branches.map(&:name)).to contain_exactly(project.default_branch_or_main)
expect(policy_project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
expect(policy_project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
expect(policy_project.team.developers).to contain_exactly(maintainer) expect(policy_project.team.developers).to contain_exactly(maintainer)
end end
end end
......
...@@ -8,7 +8,7 @@ module API ...@@ -8,7 +8,7 @@ module API
before { authenticated_as_admin! } before { authenticated_as_admin! }
feature_category :continuous_integration feature_category :pipeline_authoring
namespace 'admin' do namespace 'admin' do
namespace 'ci' do namespace 'ci' do
......
...@@ -7,7 +7,7 @@ module API ...@@ -7,7 +7,7 @@ module API
content_type :txt, 'text/plain' content_type :txt, 'text/plain'
feature_category :continuous_integration feature_category :runner
resource :runners do resource :runners do
desc 'Registers a new Runner' do desc 'Registers a new Runner' do
......
...@@ -7,7 +7,7 @@ module API ...@@ -7,7 +7,7 @@ module API
before { authenticate! } before { authenticate! }
before { authorize! :admin_build, user_project } before { authorize! :admin_build, user_project }
feature_category :continuous_integration feature_category :pipeline_authoring
helpers Helpers::VariablesHelpers helpers Helpers::VariablesHelpers
......
...@@ -35860,6 +35860,9 @@ msgstr "" ...@@ -35860,6 +35860,9 @@ msgstr ""
msgid "View full log" msgid "View full log"
msgstr "" msgstr ""
msgid "View group in admin area"
msgstr ""
msgid "View group labels" msgid "View group labels"
msgstr "" msgstr ""
...@@ -35911,6 +35914,9 @@ msgstr "" ...@@ -35911,6 +35914,9 @@ msgstr ""
msgid "View project" msgid "View project"
msgstr "" msgstr ""
msgid "View project in admin area"
msgstr ""
msgid "View project labels" msgid "View project labels"
msgstr "" msgstr ""
......
...@@ -24,7 +24,7 @@ RSpec.describe 'Merge request > Batch comments', :js do ...@@ -24,7 +24,7 @@ RSpec.describe 'Merge request > Batch comments', :js do
end end
it 'has review bar' do it 'has review bar' do
expect(page).to have_css('.review-bar-component', visible: false) expect(page).to have_selector('[data-testid="review_bar_component"]', visible: false)
end end
it 'adds draft note' do it 'adds draft note' do
...@@ -32,7 +32,7 @@ RSpec.describe 'Merge request > Batch comments', :js do ...@@ -32,7 +32,7 @@ RSpec.describe 'Merge request > Batch comments', :js do
expect(find('.draft-note-component')).to have_content('Line is wrong') expect(find('.draft-note-component')).to have_content('Line is wrong')
expect(page).to have_css('.review-bar-component') expect(page).to have_selector('[data-testid="review_bar_component"]')
expect(find('.review-bar-content .btn-confirm')).to have_content('1') expect(find('.review-bar-content .btn-confirm')).to have_content('1')
end end
......
...@@ -520,6 +520,44 @@ RSpec.describe MergeRequestsFinder do ...@@ -520,6 +520,44 @@ RSpec.describe MergeRequestsFinder do
end end
end end
context 'filtering by approved by' do
let(:params) { { approved_by_usernames: user2.username } }
before do
create(:approval, merge_request: merge_request3, user: user2)
end
it 'returns merge requests approved by that user' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request3)
end
context 'not filter' do
let(:params) { { not: { approved_by_usernames: user2.username } } }
it 'returns merge requests not approved by that user' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request4, merge_request5)
end
end
context 'when filtering by author and not approved by' do
let(:params) { { not: { approved_by_usernames: user2.username }, author_username: user.username } }
before do
merge_request4.update!(author: user2)
end
it 'returns merge requests authored by user and not approved by user2' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request5)
end
end
end
context 'filtering by created_at/updated_at' do context 'filtering by created_at/updated_at' do
let(:new_project) { create(:project, forked_from_project: project1) } let(:new_project) { create(:project, forked_from_project: project1) }
......
...@@ -59,4 +59,25 @@ RSpec.describe ApprovableBase do ...@@ -59,4 +59,25 @@ RSpec.describe ApprovableBase do
end end
end end
end end
describe '.not_approved_by_users_with_usernames' do
subject { MergeRequest.not_approved_by_users_with_usernames([user.username, user2.username]) }
let!(:merge_request2) { create(:merge_request) }
let!(:merge_request3) { create(:merge_request) }
let!(:merge_request4) { create(:merge_request) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
before do
create(:approval, merge_request: merge_request, user: user)
create(:approval, merge_request: merge_request2, user: user2)
create(:approval, merge_request: merge_request2, user: user3)
create(:approval, merge_request: merge_request4, user: user3)
end
it 'has the merge request that is not approved at all and not approved by either user' do
expect(subject).to contain_exactly(merge_request3, merge_request4)
end
end
end end
...@@ -99,6 +99,53 @@ RSpec.describe Projects::ProtectDefaultBranchService do ...@@ -99,6 +99,53 @@ RSpec.describe Projects::ProtectDefaultBranchService do
.not_to have_received(:create_protected_branch) .not_to have_received(:create_protected_branch)
end end
end end
context 'when protected branch does not exist' do
before do
allow(service)
.to receive(:protected_branch_exists?)
.and_return(false)
allow(service)
.to receive(:protect_branch?)
.and_return(true)
end
it 'changes the HEAD of the project' do
service.protect_default_branch
expect(project)
.to have_received(:change_head)
end
it 'protects the default branch' do
service.protect_default_branch
expect(service)
.to have_received(:create_protected_branch)
end
end
context 'when protected branch already exists' do
before do
allow(service)
.to receive(:protected_branch_exists?)
.and_return(true)
end
it 'changes the HEAD of the project' do
service.protect_default_branch
expect(project)
.to have_received(:change_head)
end
it 'does not protect the default branch' do
service.protect_default_branch
expect(service)
.not_to have_received(:create_protected_branch)
end
end
end end
describe '#create_protected_branch' do describe '#create_protected_branch' do
......
...@@ -14,4 +14,30 @@ RSpec.describe 'groups/_home_panel' do ...@@ -14,4 +14,30 @@ RSpec.describe 'groups/_home_panel' do
expect(rendered).to have_content("Group ID: #{group.id}") expect(rendered).to have_content("Group ID: #{group.id}")
end end
context 'admin area link' do
it 'renders admin area link for admin' do
allow(view).to receive(:current_user).and_return(create(:admin))
render
expect(rendered).to have_link(href: admin_group_path(group))
end
it 'does not render admin area link for non-admin' do
allow(view).to receive(:current_user).and_return(create(:user))
render
expect(rendered).not_to have_link(href: admin_group_path(group))
end
it 'does not render admin area link for anonymous' do
allow(view).to receive(:current_user).and_return(nil)
render
expect(rendered).not_to have_link(href: admin_group_path(group))
end
end
end end
...@@ -5,6 +5,38 @@ require 'spec_helper' ...@@ -5,6 +5,38 @@ require 'spec_helper'
RSpec.describe 'projects/_home_panel' do RSpec.describe 'projects/_home_panel' do
include ProjectForksHelper include ProjectForksHelper
context 'admin area link' do
let(:project) { create(:project) }
before do
assign(:project, project)
end
it 'renders admin area link for admin' do
allow(view).to receive(:current_user).and_return(create(:admin))
render
expect(rendered).to have_link(href: admin_project_path(project))
end
it 'does not render admin area link for non-admin' do
allow(view).to receive(:current_user).and_return(create(:user))
render
expect(rendered).not_to have_link(href: admin_project_path(project))
end
it 'does not render admin area link for anonymous' do
allow(view).to receive(:current_user).and_return(nil)
render
expect(rendered).not_to have_link(href: admin_project_path(project))
end
end
context 'notifications' do context 'notifications' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment