Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-23 06:18:49 +00:00
parent a9fa13e4ba
commit bf1a77ead7
80 changed files with 488 additions and 354 deletions

View File

@ -1 +1 @@
838d4c8bc4d82ef2ad79437b37f0d8b43394c7f7 d56698332e3e5380c4e6723828bb01ac32edd4c9

View File

@ -393,6 +393,8 @@ group :development, :test do
gem 'parallel', '~> 1.19', require: false gem 'parallel', '~> 1.19', require: false
gem 'test_file_finder', '~> 0.1.3' gem 'test_file_finder', '~> 0.1.3'
gem 'sigdump', '~> 0.2.4', require: 'sigdump/setup'
end end
group :development, :test, :danger do group :development, :test, :danger do

View File

@ -1181,6 +1181,7 @@ GEM
sidekiq-cron (1.2.0) sidekiq-cron (1.2.0)
fugit (~> 1.1) fugit (~> 1.1)
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
sigdump (0.2.4)
signet (0.14.0) signet (0.14.0)
addressable (~> 2.3) addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
@ -1622,6 +1623,7 @@ DEPENDENCIES
shoulda-matchers (~> 4.0.1) shoulda-matchers (~> 4.0.1)
sidekiq (~> 6.4) sidekiq (~> 6.4)
sidekiq-cron (~> 1.2) sidekiq-cron (~> 1.2)
sigdump (~> 0.2.4)
simple_po_parser (~> 1.1.2) simple_po_parser (~> 1.1.2)
simplecov (~> 0.18.5) simplecov (~> 0.18.5)
simplecov-cobertura (~> 1.3.1) simplecov-cobertura (~> 1.3.1)

View File

@ -377,10 +377,6 @@ span.idiff {
color: $gl-text-color; color: $gl-text-color;
} }
.file-actions .ide-edit-button {
z-index: 2;
}
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
.file-actions { .file-actions {
margin-top: $gl-padding-8; margin-top: $gl-padding-8;

View File

@ -40,7 +40,7 @@ module Projects
avenues = [authorizable_project_members] avenues = [authorizable_project_members]
avenues << if project.personal? avenues << if project.personal?
project_owner_acting_as_maintainer project_owner
else else
authorizable_group_members authorizable_group_members
end end
@ -85,9 +85,15 @@ module Projects
Member.from_union(members) Member.from_union(members)
end end
def project_owner_acting_as_maintainer # workaround until we migrate Project#owners to have membership with
# OWNER access level
def project_owner
user_id = project.namespace.owner.id user_id = project.namespace.owner.id
access_level = Gitlab::Access::MAINTAINER access_level = if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
Gitlab::Access::OWNER
else
Gitlab::Access::MAINTAINER
end
Member Member
.from(generate_from_statement([[user_id, access_level]])) # rubocop: disable CodeReuse/ActiveRecord .from(generate_from_statement([[user_id, access_level]])) # rubocop: disable CodeReuse/ActiveRecord

View File

@ -65,40 +65,13 @@ module BlobHelper
return unless blob = readable_blob(options, path, project, ref) return unless blob = readable_blob(options, path, project, ref)
common_classes = "btn gl-button btn-confirm js-edit-blob gl-ml-3 #{options[:extra_class]}" common_classes = "btn gl-button btn-confirm js-edit-blob gl-ml-3 #{options[:extra_class]}"
data = { track_action: 'click_edit', track_label: 'edit' }
if Feature.enabled?(:web_ide_primary_edit, project.group)
common_classes += " btn-inverted"
data[:track_property] = 'secondary'
end
edit_button_tag(blob, edit_button_tag(blob,
common_classes, common_classes,
_('Edit'), _('Edit'),
edit_blob_path(project, ref, path, options), edit_blob_path(project, ref, path, options),
project, project,
ref, ref)
data)
end
def ide_edit_button(project = @project, ref = @ref, path = @path, blob:)
return unless blob
common_classes = 'btn gl-button btn-confirm ide-edit-button gl-ml-3'
data = { track_action: 'click_edit_ide', track_label: 'web_ide' }
unless Feature.enabled?(:web_ide_primary_edit, project.group)
common_classes += " btn-inverted"
data[:track_property] = 'secondary'
end
edit_button_tag(blob,
common_classes,
_('Web IDE'),
ide_edit_path(project, ref, path),
project,
ref,
data)
end end
def modify_file_button(project = @project, ref = @ref, path = @path, blob:, label:, action:, btn_class:, modal_type:) def modify_file_button(project = @project, ref = @ref, path = @path, blob:, label:, action:, btn_class:, modal_type:)
@ -363,16 +336,16 @@ module BlobHelper
content_tag(:span, button, class: 'has-tooltip', title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) content_tag(:span, button, class: 'has-tooltip', title: _('You can only edit files when you are on a branch'), data: { container: 'body' })
end end
def edit_link_tag(link_text, edit_path, common_classes, data) def edit_link_tag(link_text, edit_path, common_classes)
link_to link_text, edit_path, class: "#{common_classes}", data: data link_to link_text, edit_path, class: "#{common_classes}"
end end
def edit_button_tag(blob, common_classes, text, edit_path, project, ref, data) def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
edit_disabled_button_tag(text, common_classes) edit_disabled_button_tag(text, common_classes)
# This condition only applies to users who are logged in # This condition only applies to users who are logged in
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes, data) edit_link_tag(text, edit_path, common_classes)
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end end

View File

@ -14,6 +14,14 @@ module MergeRequestReviewerState
presence: true, presence: true,
inclusion: { in: self.states.keys } inclusion: { in: self.states.keys }
belongs_to :updated_state_by, class_name: 'User', foreign_key: :updated_state_by_user_id
after_initialize :set_state, unless: :persisted? after_initialize :set_state, unless: :persisted?
def attention_requested_by
return unless attention_requested?
updated_state_by
end
end end
end end

View File

@ -8,8 +8,14 @@ module SelectForProjectAuthorization
select("projects.id AS project_id", "members.access_level") select("projects.id AS project_id", "members.access_level")
end end
def select_as_maintainer_for_project_authorization # workaround until we migrate Project#owners to have membership with
select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"]) # OWNER access level
def select_project_owner_for_project_authorization
if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
select(["projects.id AS project_id", "#{Gitlab::Access::OWNER} AS access_level"])
else
select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"])
end
end end
end end
end end

View File

@ -94,9 +94,16 @@ class ProjectMember < Member
override :access_level_inclusion override :access_level_inclusion
def access_level_inclusion def access_level_inclusion
return if access_level.in?(Gitlab::Access.values) allowed_values = if ::Feature.enabled?(:personal_project_owner_with_owner_access,
default_enabled: :yaml)
Gitlab::Access.all_values
else
Gitlab::Access.values
end
errors.add(:access_level, "is not included in the list") unless access_level.in?(allowed_values)
errors.add(:access_level, "is not included in the list")
end
end end
override :refresh_member_authorized_projects override :refresh_member_authorized_projects

View File

@ -1936,10 +1936,18 @@ class MergeRequest < ApplicationRecord
merge_request_assignees.find_by(user_id: user.id) merge_request_assignees.find_by(user_id: user.id)
end end
def merge_request_assignees_with(user_ids)
merge_request_assignees.where(user_id: user_ids)
end
def find_reviewer(user) def find_reviewer(user)
merge_request_reviewers.find_by(user_id: user.id) merge_request_reviewers.find_by(user_id: user.id)
end end
def merge_request_reviewers_with(user_ids)
merge_request_reviewers.where(user_id: user_ids)
end
def enabled_reports def enabled_reports
{ {
sast: report_type_enabled?(:sast), sast: report_type_enabled?(:sast),

View File

@ -459,7 +459,7 @@ class Project < ApplicationRecord
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role, to: :team
delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true
delegate :root_ancestor, to: :namespace, allow_nil: true delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true delegate :last_pipeline, to: :commit, allow_nil: true

View File

@ -23,6 +23,10 @@ class ProjectTeam
add_user(user, :maintainer, current_user: current_user) add_user(user, :maintainer, current_user: current_user)
end end
def add_owner(user, current_user: nil)
add_user(user, :owner, current_user: current_user)
end
def add_role(user, role, current_user: nil) def add_role(user, role, current_user: nil)
public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end end
@ -103,7 +107,9 @@ class ProjectTeam
if group if group
group.owners group.owners
else else
[project.owner] # workaround until we migrate Project#owners to have membership with
# OWNER access level
Array.wrap(fetch_members(Gitlab::Access::OWNER)) | Array.wrap(project.owner)
end end
end end

View File

@ -4,7 +4,7 @@ module Members
module Projects module Projects
class CreatorService < Members::CreatorService class CreatorService < Members::CreatorService
def self.access_levels def self.access_levels
Gitlab::Access.sym_options Gitlab::Access.sym_options_with_owner
end end
private private

View File

@ -61,6 +61,8 @@ module MergeRequests
unless new_reviewers.include?(current_user) unless new_reviewers.include?(current_user)
remove_attention_requested(merge_request, current_user) remove_attention_requested(merge_request, current_user)
merge_request.merge_request_reviewers_with(new_reviewers).update_all(updated_state_by_user_id: current_user.id)
end end
end end

View File

@ -21,6 +21,8 @@ module MergeRequests
merge_request_activity_counter.track_users_assigned_to_mr(users: new_assignees) merge_request_activity_counter.track_users_assigned_to_mr(users: new_assignees)
merge_request_activity_counter.track_assignees_changed_action(user: current_user) merge_request_activity_counter.track_assignees_changed_action(user: current_user)
merge_request.merge_request_assignees_with(new_assignees).update_all(updated_state_by_user_id: current_user.id)
execute_assignees_hooks(merge_request, old_assignees) if options[:execute_hooks] execute_assignees_hooks(merge_request, old_assignees) if options[:execute_hooks]
unless new_assignees.include?(current_user) unless new_assignees.include?(current_user)

View File

@ -59,7 +59,8 @@ module MergeRequests
end end
def update_state(reviewer_or_assignee) def update_state(reviewer_or_assignee)
reviewer_or_assignee&.update(state: reviewer_or_assignee&.attention_requested? ? :reviewed : :attention_requested) reviewer_or_assignee&.update(state: reviewer_or_assignee&.attention_requested? ? :reviewed : :attention_requested,
updated_state_by: current_user)
end end
end end
end end

View File

@ -14,6 +14,7 @@ module NotificationRecipients
return [] unless project return [] unless project
add_recipients(project.team.maintainers, :mention, nil) add_recipients(project.team.maintainers, :mention, nil)
add_recipients(project.team.owners, :mention, nil)
end end
def acting_user def acting_user

View File

@ -147,7 +147,11 @@ module Projects
priority: UserProjectAccessChangedService::LOW_PRIORITY priority: UserProjectAccessChangedService::LOW_PRIORITY
) )
else else
@project.add_maintainer(@project.namespace.owner, current_user: current_user) if ::Feature.enabled?(:personal_project_owner_with_owner_access, default_enabled: :yaml)
@project.add_owner(@project.namespace.owner, current_user: current_user)
else
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
end
end end
end end

View File

@ -4,7 +4,6 @@
title: s_('AdminArea|Get security updates from GitLab and stay up to date'), title: s_('AdminArea|Get security updates from GitLab and stay up to date'),
variant: :tip, variant: :tip,
alert_class: 'js-security-newsletter-callout', alert_class: 'js-security-newsletter-callout',
is_contained: true,
alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' }, alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' },
close_button_data: { testid: 'close-security-newsletter-callout' } do close_button_data: { testid: 'close-security-newsletter-callout' } do
.gl-alert-body .gl-alert-body

View File

@ -9,7 +9,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :warning, variant: :warning,
alert_class: 'hidden js-cluster-api-unreachable', alert_class: 'hidden js-cluster-api-unreachable',
is_contained: true,
close_button_class: 'js-close' do close_button_class: 'js-close' do
.gl-alert-body .gl-alert-body
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.') = s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
@ -17,7 +16,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :warning, variant: :warning,
alert_class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable', alert_class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable',
is_contained: true,
close_button_class: 'js-close' do close_button_class: 'js-close' do
.gl-alert-body .gl-alert-body
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.') = s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')

View File

@ -5,7 +5,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :info, variant: :info,
alert_class: 'gl-my-5', alert_class: 'gl-my-5',
is_contained: true,
dismissible: false do dismissible: false do
.gl-alert-body .gl-alert-body
= s_('Profiles|Some options are unavailable for LDAP accounts') = s_('Profiles|Some options are unavailable for LDAP accounts')
@ -14,7 +13,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :success, variant: :success,
alert_class: 'gl-my-5', alert_class: 'gl-my-5',
is_contained: true,
close_button_class: 'js-close-2fa-enabled-success-alert' do close_button_class: 'js-close-2fa-enabled-success-alert' do
.gl-alert-body .gl-alert-body
= html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe } = html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe }

View File

@ -4,8 +4,7 @@
- if @error - if @error
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :danger, variant: :danger,
close_button_class: 'js-close', close_button_class: 'js-close' do
is_contained: true do
.gl-alert-body .gl-alert-body
= @error = @error
%h3.page-title %h3.page-title

View File

@ -1,7 +1,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
title: _('Too many changes to show.'), title: _('Too many changes to show.'),
variant: :warning, variant: :warning,
is_contained: true,
alert_class: 'gl-mb-5' do alert_class: 'gl-mb-5' do
.gl-alert-body .gl-alert-body
= html_escape(_("To preserve performance only %{strong_open}%{display_size} of %{real_size}%{strong_close} files are displayed.")) % { display_size: diff_files.size, real_size: diff_files.real_size, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } = html_escape(_("To preserve performance only %{strong_open}%{display_size} of %{real_size}%{strong_close} files are displayed.")) % { display_size: diff_files.size, real_size: diff_files.real_size, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }

View File

@ -4,7 +4,6 @@
title: _('Fork Error!'), title: _('Fork Error!'),
variant: :danger, variant: :danger,
alert_class: 'gl-mt-5', alert_class: 'gl-mt-5',
is_contained: true,
dismissible: false do dismissible: false do
.gl-alert-body .gl-alert-body
%p %p

View File

@ -4,7 +4,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :warning, variant: :warning,
is_contained: true,
close_button_class: 'js-close', close_button_class: 'js-close',
alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5' do alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5' do
.gl-alert-body.gl-mr-3 .gl-alert-body.gl-mr-3

View File

@ -15,7 +15,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :info, variant: :info,
dismissible: false, dismissible: false,
is_contained: true,
alert_data: { testid: 'no-issues-alert' }, alert_data: { testid: 'no-issues-alert' },
alert_class: 'gl-mt-3 gl-mb-5' do alert_class: 'gl-mt-3 gl-mb-5' do
.gl-alert-body .gl-alert-body

View File

@ -8,16 +8,14 @@
- close_button_class = local_assigns.fetch(:close_button_class, nil) - close_button_class = local_assigns.fetch(:close_button_class, nil)
- close_button_data = local_assigns.fetch(:close_button_data, nil) - close_button_data = local_assigns.fetch(:close_button_data, nil)
- icon = icons[variant] - icon = icons[variant]
- alert_container_class = [container_class, @content_class] unless fluid_layout || local_assigns.fetch(:is_contained, false)
%div{ role: 'alert', class: ['gl-alert', "gl-alert-#{variant}", alert_class], data: alert_data } %div{ role: 'alert', class: ['gl-alert', "gl-alert-#{variant}", alert_class], data: alert_data }
.gl-alert-container{ class: alert_container_class } = sprite_icon(icon, css_class: "gl-alert-icon#{' gl-alert-icon-no-title' if title.nil?}")
= sprite_icon(icon, size: 16, css_class: "gl-alert-icon#{' gl-alert-icon-no-title' if title.nil?}") - if dismissible
- if dismissible %button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button', aria: { label: _('Dismiss') }, class: close_button_class, data: close_button_data }
%button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button', aria: { label: _('Dismiss') }, class: close_button_class, data: close_button_data } = sprite_icon('close')
= sprite_icon('close', size: 16) .gl-alert-content{ role: 'alert' }
.gl-alert-content{ role: 'alert' } - if title
- if title %h4.gl-alert-title
%h4.gl-alert-title = title
= title = yield
= yield

View File

@ -1,7 +1,6 @@
- if session[:ask_for_usage_stats_consent] - if session[:ask_for_usage_stats_consent]
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :info, variant: :info,
is_contained: true,
alert_class: 'service-ping-consent-message' do alert_class: 'service-ping-consent-message' do
.gl-alert-body .gl-alert-body
- docs_link = link_to _('collect usage information'), help_page_path('user/admin_area/settings/usage_statistics.md'), class: 'gl-link' - docs_link = link_to _('collect usage information'), help_page_path('user/admin_area/settings/usage_statistics.md'), class: 'gl-link'

View File

@ -9,7 +9,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :danger, variant: :danger,
dismissible: false, dismissible: false,
is_contained: true,
alert_class: 'gl-mb-5' do alert_class: 'gl-mb-5' do
.gl-alert-body .gl-alert-body
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did. Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.

View File

@ -3,7 +3,6 @@
- if milestone.complete? && milestone.active? - if milestone.complete? && milestone.active?
= render 'shared/global_alert', = render 'shared/global_alert',
variant: :success, variant: :success,
is_contained: true,
alert_data: { testid: 'all-issues-closed-alert' }, alert_data: { testid: 'all-issues-closed-alert' },
dismissible: false do dismissible: false do
.gl-alert-body .gl-alert-body

View File

@ -13,7 +13,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
title: s_('Webhooks|Webhook was automatically disabled'), title: s_('Webhooks|Webhook was automatically disabled'),
variant: :danger, variant: :danger,
is_contained: true,
close_button_class: 'js-close' do close_button_class: 'js-close' do
.gl-alert-body .gl-alert-body
= s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders = s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders
@ -21,7 +20,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
title: s_('Webhooks|Webhook failed to connect'), title: s_('Webhooks|Webhook failed to connect'),
variant: :danger, variant: :danger,
is_contained: true,
close_button_class: 'js-close' do close_button_class: 'js-close' do
.gl-alert-body .gl-alert-body
= s_('Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % { strong_start: strong_start, strong_end: strong_end } = s_('Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % { strong_start: strong_start, strong_end: strong_end }
@ -35,7 +33,6 @@
= render 'shared/global_alert', = render 'shared/global_alert',
title: s_('Webhooks|Webhook fails to connect'), title: s_('Webhooks|Webhook fails to connect'),
variant: :warning, variant: :warning,
is_contained: true,
close_button_class: 'js-close' do close_button_class: 'js-close' do
.gl-alert-body .gl-alert-body
= s_('Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end}, and will retry in %{retry_time}. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % placeholders = s_('Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end}, and will retry in %{retry_time}. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % placeholders

View File

@ -1,8 +1,8 @@
- add_to_breadcrumbs _('Wiki'), wiki_path(@wiki) - add_to_breadcrumbs _('Wiki'), wiki_path(@wiki)
- breadcrumb_title s_("Wiki|Pages") - breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki") - page_title s_("Wiki|Pages"), _("Wiki")
- sort_title = wiki_sort_title(params[:sort])
- add_page_specific_style 'page_bundles/wiki' - add_page_specific_style 'page_bundles/wiki'
- wiki_sort_options = [{ text: s_("Wiki|Title"), value: 'title', href: wiki_path(@wiki, action: :pages, sort: Wiki::TITLE_ORDER)}, { text: s_("Wiki|Created date"), value: 'created_at', href: wiki_path(@wiki, action: :pages, sort: Wiki::CREATED_AT_ORDER) }]
.wiki-page-header.top-area.flex-column.flex-lg-row .wiki-page-header.top-area.flex-column.flex-lg-row
%h3.page-title.gl-flex-grow-1 %h3.page-title.gl-flex-grow-1
@ -15,14 +15,7 @@
.dropdown.inline.wiki-sort-dropdown .dropdown.inline.wiki-sort-dropdown
.btn-group{ role: 'group' } .btn-group{ role: 'group' }
.btn-group{ role: 'group' } = gl_redirect_listbox_tag wiki_sort_options, params[:sort], data: { right: true }
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn gl-button btn-default' }
= sort_title
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(s_("Wiki|Title"), wiki_path(@wiki, action: :pages, sort: Wiki::TITLE_ORDER), sort_title)
= sortable_item(s_("Wiki|Created date"), wiki_path(@wiki, action: :pages, sort: Wiki::CREATED_AT_ORDER), sort_title)
= wiki_sort_controls(@wiki, params[:sort], params[:direction]) = wiki_sort_controls(@wiki, params[:sort], params[:direction])
%ul.wiki-pages-list.content-list %ul.wiki-pages-list.content-list

View File

@ -0,0 +1,8 @@
---
name: personal_project_owner_with_owner_access
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78193
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351919
milestone: '14.8'
type: development
group: group::workspace
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: web_ide_primary_edit
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35957
rollout_issue_url:
milestone: '13.3'
type: development
group: group::editor
default_enabled: false

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddUpdatedStateByUserIdToMergeRequestReviewers < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
add_column :merge_request_reviewers, :updated_state_by_user_id, :bigint
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIndexToMergeRequestReviewersUpdatedStateByUserId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_on_merge_request_reviewers_updated_state_by_user_id'
def up
add_concurrent_index :merge_request_reviewers, :updated_state_by_user_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :merge_request_reviewers, INDEX_NAME
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddUpdatedStateByUserIdToMergeRequestAssignees < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
add_column :merge_request_assignees, :updated_state_by_user_id, :bigint
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIndexToMergeRequestAssigneesUpdatedStateByUserId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_on_merge_request_assignees_updated_state_by_user_id'
def up
add_concurrent_index :merge_request_assignees, :updated_state_by_user_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :merge_request_assignees, INDEX_NAME
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddForeignKeyToUpdatedStateByUserIdToMergeRequestAssignees < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :merge_request_assignees, :users, column: :updated_state_by_user_id, on_delete: :nullify
end
def down
with_lock_retries do
remove_foreign_key :merge_request_assignees, column: :updated_state_by_user_id
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddForeignKeyToUpdatedStateByUserIdToMergeRequestReviewers < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :merge_request_reviewers, :users, column: :updated_state_by_user_id, on_delete: :nullify
end
def down
with_lock_retries do
remove_foreign_key :merge_request_reviewers, column: :updated_state_by_user_id
end
end
end

View File

@ -0,0 +1 @@
f25c65dfcb5b7b4a663cc4c792ffd985f6afd3156036485a5a43a791ee799e7b

View File

@ -0,0 +1 @@
01482a299a7dac9d3f786f0dbe4650c686911bf788467146d3e9a91eafd0fc32

View File

@ -0,0 +1 @@
1b895e979ba2f1696559179c46c000e349da2d1ab94c968dd95103f188425103

View File

@ -0,0 +1 @@
62432b2679cafa381671c9555f503867c254a7b3734e10cf634b34998d5fb5a3

View File

@ -0,0 +1 @@
0f1ea41fae57710e0e05c9b71a14800394c4c57e37a39e92be49c50120d7d2ee

View File

@ -0,0 +1 @@
8194c695a809f2eb29e5033f089c1d20874f61731a4289026f2d550854e7097d

View File

@ -16236,7 +16236,8 @@ CREATE TABLE merge_request_assignees (
user_id integer NOT NULL, user_id integer NOT NULL,
merge_request_id integer NOT NULL, merge_request_id integer NOT NULL,
created_at timestamp with time zone, created_at timestamp with time zone,
state smallint DEFAULT 0 NOT NULL state smallint DEFAULT 0 NOT NULL,
updated_state_by_user_id bigint
); );
CREATE SEQUENCE merge_request_assignees_id_seq CREATE SEQUENCE merge_request_assignees_id_seq
@ -16462,7 +16463,8 @@ CREATE TABLE merge_request_reviewers (
user_id bigint NOT NULL, user_id bigint NOT NULL,
merge_request_id bigint NOT NULL, merge_request_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
state smallint DEFAULT 0 NOT NULL state smallint DEFAULT 0 NOT NULL,
updated_state_by_user_id bigint
); );
CREATE SEQUENCE merge_request_reviewers_id_seq CREATE SEQUENCE merge_request_reviewers_id_seq
@ -27232,8 +27234,12 @@ CREATE INDEX index_on_label_links_all_columns ON label_links USING btree (target
CREATE INDEX index_on_merge_request_assignees_state ON merge_request_assignees USING btree (state) WHERE (state = 2); CREATE INDEX index_on_merge_request_assignees_state ON merge_request_assignees USING btree (state) WHERE (state = 2);
CREATE INDEX index_on_merge_request_assignees_updated_state_by_user_id ON merge_request_assignees USING btree (updated_state_by_user_id);
CREATE INDEX index_on_merge_request_reviewers_state ON merge_request_reviewers USING btree (state) WHERE (state = 2); CREATE INDEX index_on_merge_request_reviewers_state ON merge_request_reviewers USING btree (state) WHERE (state = 2);
CREATE INDEX index_on_merge_request_reviewers_updated_state_by_user_id ON merge_request_reviewers USING btree (updated_state_by_user_id);
CREATE INDEX index_on_merge_requests_for_latest_diffs ON merge_requests USING btree (target_project_id) INCLUDE (id, latest_merge_request_diff_id); CREATE INDEX index_on_merge_requests_for_latest_diffs ON merge_requests USING btree (target_project_id) INCLUDE (id, latest_merge_request_diff_id);
COMMENT ON INDEX index_on_merge_requests_for_latest_diffs IS 'Index used to efficiently obtain the oldest merge request for a commit SHA'; COMMENT ON INDEX index_on_merge_requests_for_latest_diffs IS 'Index used to efficiently obtain the oldest merge request for a commit SHA';
@ -29678,6 +29684,9 @@ ALTER TABLE ONLY epics
ALTER TABLE ONLY ci_pipelines ALTER TABLE ONLY ci_pipelines
ADD CONSTRAINT fk_3d34ab2e06 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE SET NULL; ADD CONSTRAINT fk_3d34ab2e06 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE SET NULL;
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_3d674b9f23 FOREIGN KEY (updated_state_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_pipeline_schedule_variables ALTER TABLE ONLY ci_pipeline_schedule_variables
ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE; ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE;
@ -30002,6 +30011,9 @@ ALTER TABLE ONLY merge_request_metrics
ALTER TABLE ONLY dast_profile_schedules ALTER TABLE ONLY dast_profile_schedules
ADD CONSTRAINT fk_aef03d62e5 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; ADD CONSTRAINT fk_aef03d62e5 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY merge_request_assignees
ADD CONSTRAINT fk_af036e3261 FOREIGN KEY (updated_state_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY analytics_cycle_analytics_group_stages ALTER TABLE ONLY analytics_cycle_analytics_group_stages
ADD CONSTRAINT fk_analytics_cycle_analytics_group_stages_group_value_stream_id FOREIGN KEY (group_value_stream_id) REFERENCES analytics_cycle_analytics_group_value_streams(id) ON DELETE CASCADE; ADD CONSTRAINT fk_analytics_cycle_analytics_group_stages_group_value_stream_id FOREIGN KEY (group_value_stream_id) REFERENCES analytics_cycle_analytics_group_value_streams(id) ON DELETE CASCADE;

View File

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Install GitLab under a relative URL **(FREE SELF)** # Install GitLab under a relative URL **(FREE SELF)**
While it is recommended to install GitLab on its own (sub)domain, sometimes While we recommend to install GitLab on its own (sub)domain, sometimes
this is not possible due to a variety of reasons. In that case, GitLab can also this is not possible due to a variety of reasons. In that case, GitLab can also
be installed under a relative URL, for example `https://example.com/gitlab`. be installed under a relative URL, for example `https://example.com/gitlab`.
@ -19,8 +19,8 @@ first time.
There is no limit to how deeply nested the relative URL can be. For example you There is no limit to how deeply nested the relative URL can be. For example you
could serve GitLab under `/foo/bar/gitlab/git` without any issues. could serve GitLab under `/foo/bar/gitlab/git` without any issues.
Note that by changing the URL on an existing GitLab installation, all remote Changing the URL on an existing GitLab installation, changes all remote
URLs will change, so you'll have to manually edit them in any local repository URLs, so you have to manually edit them in any local repository
that points to your GitLab instance. that points to your GitLab instance.
The list of configuration files you must change to serve GitLab from a The list of configuration files you must change to serve GitLab from a
@ -32,7 +32,7 @@ relative URL is:
- `/home/git/gitlab-shell/config.yml` - `/home/git/gitlab-shell/config.yml`
- `/etc/default/gitlab` - `/etc/default/gitlab`
After all the changes you need to recompile the assets and [restart GitLab](../administration/restart_gitlab.md#installations-from-source). After all the changes, you must recompile the assets and [restart GitLab](../administration/restart_gitlab.md#installations-from-source).
## Relative URL requirements ## Relative URL requirements

View File

@ -33,14 +33,27 @@ usernames. A GitLab administrator can configure the GitLab instance to
## Project members permissions ## Project members permissions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219299) in GitLab 14.8, personal namespace owners appear with Owner role in new projects in their namespace. Introduced [with a flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`. Disabled by default.
FLAG:
On self-managed GitLab, personal namespace owners appearing with the Owner role in new projects in their namespace is disabled. To make it available,
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`.
The feature is not ready for production use.
On GitLab.com, this feature is not available.
A user's role determines what permissions they have on a project. The Owner role provides all permissions but is A user's role determines what permissions they have on a project. The Owner role provides all permissions but is
available only: available only:
- For group owners. The role is inherited for a group's projects. - For group owners. The role is inherited for a group's projects.
- For Administrators. - For Administrators.
Personal namespace owners have the same permissions as an Owner, but are displayed with the Maintainer role on projects created in their personal namespace. Personal [namespace](group/index.md#namespaces) owners:
For more information, see [projects members documentation](project/members/index.md).
- Are displayed as having the Maintainer role on projects in the namespace, but have the same permissions as a user with the Owner role.
- (Disabled by default) In GitLab 14.8 and later, for new projects in the namespace, are displayed as having the Owner role.
For more information about how to manage project members, see
[members of a project](project/members/index.md).
The following table lists project permissions available for each role: The following table lists project permissions available for each role:

View File

@ -9,13 +9,11 @@ module Backup
DEFAULT_EXCLUDE = 'lost+found' DEFAULT_EXCLUDE = 'lost+found'
attr_reader :name, :app_files_dir, :backup_tarball, :excludes, :files_parent_dir attr_reader :name, :backup_tarball, :excludes
def initialize(name, app_files_dir, excludes: []) def initialize(name, app_files_dir, excludes: [])
@name = name @name = name
@app_files_dir = File.realpath(app_files_dir) @app_files_dir = app_files_dir
@files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
@backup_files_dir = File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
@backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz') @backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
@excludes = [DEFAULT_EXCLUDE].concat(excludes) @excludes = [DEFAULT_EXCLUDE].concat(excludes)
end end
@ -26,7 +24,7 @@ module Backup
FileUtils.rm_f(backup_tarball) FileUtils.rm_f(backup_tarball)
if ENV['STRATEGY'] == 'copy' if ENV['STRATEGY'] == 'copy'
cmd = [%w[rsync -a --delete], exclude_dirs(:rsync), %W[#{app_files_dir} #{Gitlab.config.backup.path}]].flatten cmd = [%w[rsync -a --delete], exclude_dirs(:rsync), %W[#{app_files_realpath} #{Gitlab.config.backup.path}]].flatten
output, status = Gitlab::Popen.popen(cmd) output, status = Gitlab::Popen.popen(cmd)
# Retry if rsync source files vanish # Retry if rsync source files vanish
@ -40,11 +38,11 @@ module Backup
raise_custom_error raise_custom_error
end end
tar_cmd = [tar, exclude_dirs(:tar), %W[-C #{@backup_files_dir} -cf - .]].flatten tar_cmd = [tar, exclude_dirs(:tar), %W[-C #{backup_files_realpath} -cf - .]].flatten
status_list, output = run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600]) status_list, output = run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir) FileUtils.rm_rf(backup_files_realpath)
else else
tar_cmd = [tar, exclude_dirs(:tar), %W[-C #{app_files_dir} -cf - .]].flatten tar_cmd = [tar, exclude_dirs(:tar), %W[-C #{app_files_realpath} -cf - .]].flatten
status_list, output = run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600]) status_list, output = run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600])
end end
@ -56,7 +54,7 @@ module Backup
def restore def restore
backup_existing_files_dir backup_existing_files_dir
cmd_list = [%w[gzip -cd], %W[#{tar} --unlink-first --recursive-unlink -C #{app_files_dir} -xf -]] cmd_list = [%w[gzip -cd], %W[#{tar} --unlink-first --recursive-unlink -C #{app_files_realpath} -xf -]]
status_list, output = run_pipeline!(cmd_list, in: backup_tarball) status_list, output = run_pipeline!(cmd_list, in: backup_tarball)
unless pipeline_succeeded?(gzip_status: status_list[0], tar_status: status_list[1], output: output) unless pipeline_succeeded?(gzip_status: status_list[0], tar_status: status_list[1], output: output)
raise Backup::Error, "Restore operation failed: #{output}" raise Backup::Error, "Restore operation failed: #{output}"
@ -78,17 +76,17 @@ module Backup
def backup_existing_files_dir def backup_existing_files_dir
timestamped_files_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}.#{Time.now.to_i}") timestamped_files_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}.#{Time.now.to_i}")
if File.exist?(app_files_dir) if File.exist?(app_files_realpath)
# Move all files in the existing repos directory except . and .. to # Move all files in the existing repos directory except . and .. to
# repositories.old.<timestamp> directory # repositories.old.<timestamp> directory
FileUtils.mkdir_p(timestamped_files_path, mode: 0700) FileUtils.mkdir_p(timestamped_files_path, mode: 0700)
files = Dir.glob(File.join(app_files_dir, "*"), File::FNM_DOTMATCH) - [File.join(app_files_dir, "."), File.join(app_files_dir, "..")] files = Dir.glob(File.join(app_files_realpath, "*"), File::FNM_DOTMATCH) - [File.join(app_files_realpath, "."), File.join(app_files_realpath, "..")]
begin begin
FileUtils.mv(files, timestamped_files_path) FileUtils.mv(files, timestamped_files_path)
rescue Errno::EACCES rescue Errno::EACCES
access_denied_error(app_files_dir) access_denied_error(app_files_realpath)
rescue Errno::EBUSY rescue Errno::EBUSY
resource_busy_error(app_files_dir) resource_busy_error(app_files_realpath)
end end
end end
end end
@ -141,7 +139,7 @@ module Backup
if s == DEFAULT_EXCLUDE if s == DEFAULT_EXCLUDE
'--exclude=' + s '--exclude=' + s
elsif fmt == :rsync elsif fmt == :rsync
'--exclude=/' + File.join(File.basename(app_files_dir), s) '--exclude=/' + File.join(File.basename(app_files_realpath), s)
elsif fmt == :tar elsif fmt == :tar
'--exclude=./' + s '--exclude=./' + s
end end
@ -149,7 +147,17 @@ module Backup
end end
def raise_custom_error def raise_custom_error
raise FileBackupError.new(app_files_dir, backup_tarball) raise FileBackupError.new(app_files_realpath, backup_tarball)
end
private
def app_files_realpath
@app_files_realpath ||= File.realpath(@app_files_dir)
end
def backup_files_realpath
@backup_files_realpath ||= File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
end end
end end
end end

View File

@ -22,7 +22,7 @@ module Gitlab
user.projects_with_active_memberships.select_for_project_authorization, user.projects_with_active_memberships.select_for_project_authorization,
# The personal projects of the user. # The personal projects of the user.
user.personal_projects.select_as_maintainer_for_project_authorization, user.personal_projects.select_project_owner_for_project_authorization,
# Projects that belong directly to any of the groups the user has # Projects that belong directly to any of the groups the user has
# access to. # access to.

View File

@ -38560,10 +38560,8 @@ msgstr ""
msgid "Trending" msgid "Trending"
msgstr "" msgstr ""
msgid "Trials|%{planName} Trial %{enDash} %{num} day left" msgid "Trials|%{planName} Trial"
msgid_plural "Trials|%{planName} Trial %{enDash} %{num} days left" msgstr ""
msgstr[0] ""
msgstr[1] ""
msgid "Trials|Compare all plans" msgid "Trials|Compare all plans"
msgstr "" msgstr ""
@ -38571,6 +38569,9 @@ msgstr ""
msgid "Trials|Create a new group to start your GitLab Ultimate trial." msgid "Trials|Create a new group to start your GitLab Ultimate trial."
msgstr "" msgstr ""
msgid "Trials|Day %{daysUsed}/%{duration}"
msgstr ""
msgid "Trials|Go back to GitLab" msgid "Trials|Go back to GitLab"
msgstr "" msgstr ""

View File

@ -665,7 +665,7 @@ RSpec.describe Projects::ProjectMembersController do
sign_in(user) sign_in(user)
end end
it 'does not create a member' do it 'creates a member' do
expect do expect do
post :create, params: { post :create, params: {
user_ids: stranger.id, user_ids: stranger.id,
@ -673,7 +673,9 @@ RSpec.describe Projects::ProjectMembersController do
access_level: Member::OWNER, access_level: Member::OWNER,
project_id: project project_id: project
} }
end.to change { project.members.count }.by(0) end.to change { project.members.count }.by(1)
expect(project.team_members).to include(user)
end end
end end

View File

@ -2,7 +2,7 @@
require "spec_helper" require "spec_helper"
RSpec.describe 'Project wikis' do RSpec.describe 'Project wikis', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, user: user, project: project) } let(:wiki) { create(:project_wiki, user: user, project: project) }

View File

@ -11,21 +11,40 @@ RSpec.describe Projects::Members::EffectiveAccessLevelFinder, '#execute' do
context 'for a personal project' do context 'for a personal project' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
shared_examples_for 'includes access level of the owner of the project as Maintainer' do shared_examples_for 'includes access level of the owner of the project' do
it 'includes access level of the owner of the project as Maintainer' do context 'when personal_project_owner_with_owner_access feature flag is enabled' do
expect(subject).to( it 'includes access level of the owner of the project as Owner' do
contain_exactly( expect(subject).to(
hash_including( contain_exactly(
'user_id' => project.namespace.owner.id, hash_including(
'access_level' => Gitlab::Access::MAINTAINER 'user_id' => project.namespace.owner.id,
'access_level' => Gitlab::Access::OWNER
)
) )
) )
) end
end
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
before do
stub_feature_flags(personal_project_owner_with_owner_access: false)
end
it 'includes access level of the owner of the project as Maintainer' do
expect(subject).to(
contain_exactly(
hash_including(
'user_id' => project.namespace.owner.id,
'access_level' => Gitlab::Access::MAINTAINER
)
)
)
end
end end
end end
context 'when the project owner is a member of the project' do context 'when the project owner is a member of the project' do
it_behaves_like 'includes access level of the owner of the project as Maintainer' it_behaves_like 'includes access level of the owner of the project'
end end
context 'when the project owner is not explicitly a member of the project' do context 'when the project owner is not explicitly a member of the project' do
@ -33,7 +52,7 @@ RSpec.describe Projects::Members::EffectiveAccessLevelFinder, '#execute' do
project.members.find_by(user_id: project.namespace.owner.id).destroy! project.members.find_by(user_id: project.namespace.owner.id).destroy!
end end
it_behaves_like 'includes access level of the owner of the project as Maintainer' it_behaves_like 'includes access level of the owner of the project'
end end
end end
@ -84,17 +103,32 @@ RSpec.describe Projects::Members::EffectiveAccessLevelFinder, '#execute' do
context 'for a project within a group' do context 'for a project within a group' do
context 'project in a root group' do context 'project in a root group' do
it 'includes access levels of users who are direct members of the parent group' do context 'includes access levels of users who are direct members of the parent group' do
group_member = create(:group_member, :developer, source: group) it 'when access level is developer' do
group_member = create(:group_member, :developer, source: group)
expect(subject).to( expect(subject).to(
include( include(
hash_including( hash_including(
'user_id' => group_member.user.id, 'user_id' => group_member.user.id,
'access_level' => Gitlab::Access::DEVELOPER 'access_level' => Gitlab::Access::DEVELOPER
)
) )
) )
) end
it 'when access level is owner' do
group_member = create(:group_member, :owner, source: group)
expect(subject).to(
include(
hash_including(
'user_id' => group_member.user.id,
'access_level' => Gitlab::Access::OWNER
)
)
)
end
end end
end end

View File

@ -54,42 +54,6 @@ RSpec.describe BlobHelper do
expect(Capybara.string(link_with_mr).find_link('Edit')[:href]).to eq("/#{project.full_path}/-/edit/master/README.md?mr_id=10") expect(Capybara.string(link_with_mr).find_link('Edit')[:href]).to eq("/#{project.full_path}/-/edit/master/README.md?mr_id=10")
end end
context 'when edit is the primary button' do
before do
stub_feature_flags(web_ide_primary_edit: false)
end
it 'is rendered as primary' do
expect(link).not_to match(/btn-inverted/)
end
it 'passes on primary tracking attributes' do
parsed_link = Capybara.string(link).find_link('Edit')
expect(parsed_link[:'data-track-action']).to eq("click_edit")
expect(parsed_link[:'data-track-label']).to eq("edit")
expect(parsed_link[:'data-track-property']).to eq(nil)
end
end
context 'when Web IDE is the primary button' do
before do
stub_feature_flags(web_ide_primary_edit: true)
end
it 'is rendered as inverted' do
expect(link).to match(/btn-inverted/)
end
it 'passes on secondary tracking attributes' do
parsed_link = Capybara.string(link).find_link('Edit')
expect(parsed_link[:'data-track-action']).to eq("click_edit")
expect(parsed_link[:'data-track-label']).to eq("edit")
expect(parsed_link[:'data-track-property']).to eq("secondary")
end
end
end end
describe "#relative_raw_path" do describe "#relative_raw_path" do
@ -324,63 +288,6 @@ RSpec.describe BlobHelper do
end end
end end
describe `#ide_edit_button` do
let_it_be(:namespace) { create(:namespace, name: 'gitlab') }
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
let_it_be(:current_user) { create(:user) }
let(:can_push_code) { true }
let(:blob) { project.repository.blob_at('refs/heads/master', 'README.md') }
subject(:link) { helper.ide_edit_button(project, 'master', 'README.md', blob: blob) }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).with(current_user, :push_code, project).and_return(can_push_code)
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'returns a link with a Web IDE route' do
expect(Capybara.string(link).find_link('Web IDE')[:href]).to eq("/-/ide/project/#{project.full_path}/edit/master/-/README.md")
end
context 'when edit is the primary button' do
before do
stub_feature_flags(web_ide_primary_edit: false)
end
it 'is rendered as inverted' do
expect(link).to match(/btn-inverted/)
end
it 'passes on secondary tracking attributes' do
parsed_link = Capybara.string(link).find_link('Web IDE')
expect(parsed_link[:'data-track-action']).to eq("click_edit_ide")
expect(parsed_link[:'data-track-label']).to eq("web_ide")
expect(parsed_link[:'data-track-property']).to eq("secondary")
end
end
context 'when Web IDE is the primary button' do
before do
stub_feature_flags(web_ide_primary_edit: true)
end
it 'is rendered as primary' do
expect(link).not_to match(/btn-inverted/)
end
it 'passes on primary tracking attributes' do
parsed_link = Capybara.string(link).find_link('Web IDE')
expect(parsed_link[:'data-track-action']).to eq("click_edit_ide")
expect(parsed_link[:'data-track-label']).to eq("web_ide")
expect(parsed_link[:'data-track-property']).to eq(nil)
end
end
end
describe '#ide_edit_path' do describe '#ide_edit_path' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:current_user) { create(:user) } let(:current_user) { create(:user) }

View File

@ -7,16 +7,6 @@ RSpec.describe Backup::Artifacts do
subject(:backup) { described_class.new(progress) } subject(:backup) { described_class.new(progress) }
describe '#initialize' do
it 'uses the correct upload dir' do
Dir.mktmpdir do |tmpdir|
allow(JobArtifactUploader).to receive(:root) { "#{tmpdir}" }
expect(backup.app_files_dir).to eq("#{File.realpath(tmpdir)}")
end
end
end
describe '#dump' do describe '#dump' do
before do before do
allow(File).to receive(:realpath).with('/var/gitlab-artifacts').and_return('/var/gitlab-artifacts') allow(File).to receive(:realpath).with('/var/gitlab-artifacts').and_return('/var/gitlab-artifacts')
@ -24,10 +14,6 @@ RSpec.describe Backup::Artifacts do
allow(JobArtifactUploader).to receive(:root) { '/var/gitlab-artifacts' } allow(JobArtifactUploader).to receive(:root) { '/var/gitlab-artifacts' }
end end
it 'uses the correct artifact dir' do
expect(backup.app_files_dir).to eq('/var/gitlab-artifacts')
end
it 'excludes tmp from backup tar' do it 'excludes tmp from backup tar' do
expect(backup).to receive(:tar).and_return('blabla-tar') expect(backup).to receive(:tar).and_return('blabla-tar')
expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found --exclude=./tmp -C /var/gitlab-artifacts -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], '']) expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found --exclude=./tmp -C /var/gitlab-artifacts -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], ''])

View File

@ -16,7 +16,6 @@ RSpec.describe Backup::Lfs do
end end
it 'uses the correct lfs dir in tar command', :aggregate_failures do it 'uses the correct lfs dir in tar command', :aggregate_failures do
expect(backup.app_files_dir).to eq('/var/lfs-objects')
expect(backup).to receive(:tar).and_return('blabla-tar') expect(backup).to receive(:tar).and_return('blabla-tar')
expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found -C /var/lfs-objects -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], '']) expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found -C /var/lfs-objects -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], ''])
expect(backup).to receive(:pipeline_succeeded?).and_return(true) expect(backup).to receive(:pipeline_succeeded?).and_return(true)

View File

@ -12,11 +12,6 @@ RSpec.describe Backup::Manager do
before do before do
allow(progress).to receive(:puts) allow(progress).to receive(:puts)
allow(progress).to receive(:print) allow(progress).to receive(:print)
FileUtils.mkdir_p('tmp/tests/public/uploads')
end
after do
FileUtils.rm_rf('tmp/tests/public/uploads', secure: true)
end end
describe '#pack' do describe '#pack' do

View File

@ -17,7 +17,6 @@ RSpec.shared_examples 'backup object' do |setting|
end end
it 'uses the correct storage dir in tar command and excludes tmp', :aggregate_failures do it 'uses the correct storage dir in tar command and excludes tmp', :aggregate_failures do
expect(backup.app_files_dir).to eq(backup_path)
expect(backup).to receive(:tar).and_return('blabla-tar') expect(backup).to receive(:tar).and_return('blabla-tar')
expect(backup).to receive(:run_pipeline!).with([%W(blabla-tar --exclude=lost+found --exclude=./tmp -C #{backup_path} -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], '']) expect(backup).to receive(:run_pipeline!).with([%W(blabla-tar --exclude=lost+found --exclude=./tmp -C #{backup_path} -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], ''])
expect(backup).to receive(:pipeline_succeeded?).and_return(true) expect(backup).to receive(:pipeline_succeeded?).and_return(true)

View File

@ -13,12 +13,6 @@ RSpec.describe Backup::Pages do
end end
describe '#dump' do describe '#dump' do
it 'uses the correct pages dir' do
allow(Gitlab.config.pages).to receive(:path) { '/var/gitlab-pages' }
expect(subject.app_files_dir).to eq('/var/gitlab-pages')
end
it 'excludes tmp from backup tar' do it 'excludes tmp from backup tar' do
allow(Gitlab.config.pages).to receive(:path) { '/var/gitlab-pages' } allow(Gitlab.config.pages).to receive(:path) { '/var/gitlab-pages' }

View File

@ -7,18 +7,6 @@ RSpec.describe Backup::Uploads do
subject(:backup) { described_class.new(progress) } subject(:backup) { described_class.new(progress) }
describe '#initialize' do
it 'uses the correct upload dir' do
Dir.mktmpdir do |tmpdir|
FileUtils.mkdir_p("#{tmpdir}/uploads")
allow(Gitlab.config.uploads).to receive(:storage_path) { tmpdir }
expect(backup.app_files_dir).to eq("#{File.realpath(tmpdir)}/uploads")
end
end
end
describe '#dump' do describe '#dump' do
before do before do
allow(File).to receive(:realpath).and_call_original allow(File).to receive(:realpath).and_call_original
@ -27,10 +15,6 @@ RSpec.describe Backup::Uploads do
allow(Gitlab.config.uploads).to receive(:storage_path) { '/var' } allow(Gitlab.config.uploads).to receive(:storage_path) { '/var' }
end end
it 'uses the correct upload dir' do
expect(backup.app_files_dir).to eq('/var/uploads')
end
it 'excludes tmp from backup tar' do it 'excludes tmp from backup tar' do
expect(backup).to receive(:tar).and_return('blabla-tar') expect(backup).to receive(:tar).and_return('blabla-tar')
expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found --exclude=./tmp -C /var/uploads -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], '']) expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found --exclude=./tmp -C /var/uploads -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], ''])

View File

@ -34,12 +34,28 @@ RSpec.describe Gitlab::ProjectAuthorizations do
.to include(owned_project.id, other_project.id, group_project.id) .to include(owned_project.id, other_project.id, group_project.id)
end end
it 'includes the correct access levels' do context 'when personal_project_owner_with_owner_access feature flag is enabled' do
mapping = map_access_levels(authorizations) it 'includes the correct access levels' do
mapping = map_access_levels(authorizations)
expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER) expect(mapping[owned_project.id]).to eq(Gitlab::Access::OWNER)
expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER) expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER) expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
end
end
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
before do
stub_feature_flags(personal_project_owner_with_owner_access: false)
end
it 'includes the correct access levels' do
mapping = map_access_levels(authorizations)
expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
end
end end
end end

View File

@ -51,4 +51,24 @@ RSpec.describe MergeRequestAssignee do
it { is_expected.to have_attributes(state: 'reviewed') } it { is_expected.to have_attributes(state: 'reviewed') }
end end
describe '#attention_requested_by' do
let(:current_user) { create(:user) }
before do
subject.update!(updated_state_by: current_user)
end
context 'attention requested' do
it { expect(subject.attention_requested_by).to eq(current_user) }
end
context 'attention requested' do
before do
subject.update!(state: :reviewed)
end
it { expect(subject.attention_requested_by).to eq(nil) }
end
end
end end

View File

@ -25,4 +25,24 @@ RSpec.describe MergeRequestReviewer do
it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') } it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
it { is_expected.to belong_to(:reviewer).class_name('User').inverse_of(:merge_request_reviewers) } it { is_expected.to belong_to(:reviewer).class_name('User').inverse_of(:merge_request_reviewers) }
end end
describe '#attention_requested_by' do
let(:current_user) { create(:user) }
before do
subject.update!(updated_state_by: current_user)
end
context 'attention requested' do
it { expect(subject.attention_requested_by).to eq(current_user) }
end
context 'attention requested' do
before do
subject.update!(state: :reviewed)
end
it { expect(subject.attention_requested_by).to eq(nil) }
end
end
end end

View File

@ -5109,4 +5109,34 @@ RSpec.describe MergeRequest, factory_default: :keep do
let!(:model) { create(:merge_request, head_pipeline: parent) } let!(:model) { create(:merge_request, head_pipeline: parent) }
end end
end end
describe '#merge_request_reviewers_with' do
let_it_be(:reviewer1) { create(:user) }
let_it_be(:reviewer2) { create(:user) }
before do
subject.update!(reviewers: [reviewer1, reviewer2])
end
it 'returns reviewers' do
reviewers = subject.merge_request_reviewers_with([reviewer1.id])
expect(reviewers).to match_array([subject.merge_request_reviewers[0]])
end
end
describe '#merge_request_assignees_with' do
let_it_be(:assignee1) { create(:user) }
let_it_be(:assignee2) { create(:user) }
before do
subject.update!(assignees: [assignee1, assignee2])
end
it 'returns assignees' do
assignees = subject.merge_request_assignees_with([assignee1.id])
expect(assignees).to match_array([subject.merge_request_assignees[0]])
end
end
end end

View File

@ -225,7 +225,7 @@ RSpec.describe ProjectTeam do
let_it_be(:maintainer) { create(:user) } let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) } let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project, namespace: maintainer.namespace) } let_it_be(:project) { create(:project, group: create(:group)) }
let_it_be(:access_levels) { [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER] } let_it_be(:access_levels) { [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER] }
subject(:members_with_access_levels) { project.team.members_with_access_levels(access_levels) } subject(:members_with_access_levels) { project.team.members_with_access_levels(access_levels) }

View File

@ -3717,7 +3717,7 @@ RSpec.describe User do
context 'with min_access_level' do context 'with min_access_level' do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:project) { create(:project, :private, namespace: user.namespace) } let!(:project) { create(:project, :private, group: create(:group)) }
before do before do
project.add_developer(user) project.add_developer(user)

View File

@ -675,13 +675,30 @@ RSpec.describe API::Members do
end end
context 'adding owner to project' do context 'adding owner to project' do
it 'returns 403' do context 'when personal_project_owner_with_owner_access feature flag is enabled' do
expect do it 'returns created status' do
post api("/projects/#{project.id}/members", maintainer), expect do
params: { user_id: stranger.id, access_level: Member::OWNER } post api("/projects/#{project.id}/members", maintainer),
params: { user_id: stranger.id, access_level: Member::OWNER }
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:created)
end.not_to change { project.members.count } end.to change { project.members.count }.by(1)
end
end
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
before do
stub_feature_flags(personal_project_owner_with_owner_access: false)
end
it 'returns created status' do
expect do
post api("/projects/#{project.id}/members", maintainer),
params: { user_id: stranger.id, access_level: Member::OWNER }
expect(response).to have_gitlab_http_status(:bad_request)
end.not_to change { project.members.count }
end
end end
end end

View File

@ -40,7 +40,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
it 'is called' do it 'is called' do
ProjectAuthorization.delete_all ProjectAuthorization.delete_all
expect(callback).to receive(:call).with(project.id, Gitlab::Access::MAINTAINER).once expect(callback).to receive(:call).with(project.id, Gitlab::Access::OWNER).once
service.execute service.execute
end end
@ -60,20 +60,20 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
to_be_removed = [project2.id] to_be_removed = [project2.id]
to_be_added = [ to_be_added = [
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER } { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
] ]
expect(service.execute).to eq([to_be_removed, to_be_added]) expect(service.execute).to eq([to_be_removed, to_be_added])
end end
it 'finds duplicate entries that has to be removed' do it 'finds duplicate entries that has to be removed' do
[Gitlab::Access::MAINTAINER, Gitlab::Access::REPORTER].each do |access_level| [Gitlab::Access::OWNER, Gitlab::Access::REPORTER].each do |access_level|
user.project_authorizations.create!(project: project, access_level: access_level) user.project_authorizations.create!(project: project, access_level: access_level)
end end
to_be_removed = [project.id] to_be_removed = [project.id]
to_be_added = [ to_be_added = [
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER } { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
] ]
expect(service.execute).to eq([to_be_removed, to_be_added]) expect(service.execute).to eq([to_be_removed, to_be_added])
@ -85,7 +85,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
to_be_removed = [project.id] to_be_removed = [project.id]
to_be_added = [ to_be_added = [
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER } { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
] ]
expect(service.execute).to eq([to_be_removed, to_be_added]) expect(service.execute).to eq([to_be_removed, to_be_added])
@ -143,16 +143,16 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
end end
it 'sets the keys to the project IDs' do it 'sets the keys to the project IDs' do
expect(hash.keys).to eq([project.id]) expect(hash.keys).to match_array([project.id])
end end
it 'sets the values to the access levels' do it 'sets the values to the access levels' do
expect(hash.values).to eq([Gitlab::Access::MAINTAINER]) expect(hash.values).to match_array([Gitlab::Access::OWNER])
end end
context 'personal projects' do context 'personal projects' do
it 'includes the project with the right access level' do it 'includes the project with the right access level' do
expect(hash[project.id]).to eq(Gitlab::Access::MAINTAINER) expect(hash[project.id]).to eq(Gitlab::Access::OWNER)
end end
end end
@ -242,7 +242,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
value = hash.values[0] value = hash.values[0]
expect(value.project_id).to eq(project.id) expect(value.project_id).to eq(project.id)
expect(value.access_level).to eq(Gitlab::Access::MAINTAINER) expect(value.access_level).to eq(Gitlab::Access::OWNER)
end end
end end
@ -267,7 +267,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
end end
it 'includes the access level for every row' do it 'includes the access level for every row' do
expect(row.access_level).to eq(Gitlab::Access::MAINTAINER) expect(row.access_level).to eq(Gitlab::Access::OWNER)
end end
end end
end end
@ -283,7 +283,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
rows = service.fresh_authorizations.to_a rows = service.fresh_authorizations.to_a
expect(rows.length).to eq(1) expect(rows.length).to eq(1)
expect(rows.first.access_level).to eq(Gitlab::Access::MAINTAINER) expect(rows.first.access_level).to eq(Gitlab::Access::OWNER)
end end
context 'every returned row' do context 'every returned row' do
@ -294,7 +294,7 @@ RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
end end
it 'includes the access level' do it 'includes the access level' do
expect(row.access_level).to eq(Gitlab::Access::MAINTAINER) expect(row.access_level).to eq(Gitlab::Access::OWNER)
end end
end end
end end

View File

@ -9,8 +9,8 @@ RSpec.describe Members::Projects::CreatorService do
end end
describe '.access_levels' do describe '.access_levels' do
it 'returns Gitlab::Access.sym_options' do it 'returns Gitlab::Access.sym_options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options) expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end end
end end
end end

View File

@ -95,6 +95,12 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do
execute execute
end end
it 'updates attention requested by of assignee' do
execute
expect(merge_request.find_assignee(assignee).updated_state_by).to eq(user)
end
it 'tracks users assigned event' do it 'tracks users assigned event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_users_assigned_to_mr).once.with(users: [assignee]) .to receive(:track_users_assigned_to_mr).once.with(users: [assignee])

View File

@ -59,6 +59,13 @@ RSpec.describe MergeRequests::ToggleAttentionRequestedService do
expect(reviewer.state).to eq 'attention_requested' expect(reviewer.state).to eq 'attention_requested'
end end
it 'adds who toggled attention' do
service.execute
reviewer.reload
expect(reviewer.updated_state_by).to eq current_user
end
it 'creates a new todo for the reviewer' do it 'creates a new todo for the reviewer' do
expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, user) expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, user)

View File

@ -215,6 +215,14 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
end end
it 'updates attention requested by of reviewer' do
opts[:reviewers] = [user2]
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
expect(merge_request.find_reviewer(user2).updated_state_by).to eq(user)
end
end end
context 'when reviewers did not change' do context 'when reviewers did not change' do

View File

@ -3312,7 +3312,7 @@ RSpec.describe NotificationService, :mailer do
describe "##{sym}" do describe "##{sym}" do
subject(:notify!) { notification.send(sym, domain) } subject(:notify!) { notification.send(sym, domain) }
it 'emails current watching maintainers' do it 'emails current watching maintainers and owners' do
expect(Notify).to receive(:"#{sym}_email").at_least(:once).and_call_original expect(Notify).to receive(:"#{sym}_email").at_least(:once).and_call_original
notify! notify!
@ -3410,7 +3410,7 @@ RSpec.describe NotificationService, :mailer do
reset_delivered_emails! reset_delivered_emails!
end end
it 'emails current watching maintainers' do it 'emails current watching maintainers and owners' do
notification.remote_mirror_update_failed(remote_mirror) notification.remote_mirror_update_failed(remote_mirror)
should_only_email(u_maintainer1, u_maintainer2, u_owner) should_only_email(u_maintainer1, u_maintainer2, u_owner)

View File

@ -116,14 +116,34 @@ RSpec.describe Projects::CreateService, '#execute' do
end end
context 'user namespace' do context 'user namespace' do
it 'creates a project in user namespace' do context 'when personal_project_owner_with_owner_access feature flag is enabled' do
project = create_project(user, opts) it 'creates a project in user namespace' do
project = create_project(user, opts)
expect(project).to be_valid expect(project).to be_valid
expect(project.first_owner).to eq(user) expect(project.first_owner).to eq(user)
expect(project.team.maintainers).to include(user) expect(project.team.maintainers).not_to include(user)
expect(project.namespace).to eq(user.namespace) expect(project.team.owners).to contain_exactly(user)
expect(project.project_namespace).to be_in_sync_with_project(project) expect(project.namespace).to eq(user.namespace)
expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
context 'when personal_project_owner_with_owner_access feature flag is disabled' do
before do
stub_feature_flags(personal_project_owner_with_owner_access: false)
end
it 'creates a project in user namespace' do
project = create_project(user, opts)
expect(project).to be_valid
expect(project.first_owner).to eq(user)
expect(project.team.maintainers).to contain_exactly(user)
expect(project.team.owners).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace)
expect(project.project_namespace).to be_in_sync_with_project(project)
end
end end
end end
@ -162,7 +182,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_persisted expect(project).to be_persisted
expect(project.owner).to eq(user) expect(project.owner).to eq(user)
expect(project.first_owner).to eq(user) expect(project.first_owner).to eq(user)
expect(project.team.maintainers).to contain_exactly(user) expect(project.team.owners).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace) expect(project.namespace).to eq(user.namespace)
expect(project.project_namespace).to be_in_sync_with_project(project) expect(project.project_namespace).to be_in_sync_with_project(project)
end end

View File

@ -52,7 +52,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
it 'is called' do it 'is called' do
ProjectAuthorization.delete_all ProjectAuthorization.delete_all
expect(callback).to receive(:call).with(project.id, Gitlab::Access::MAINTAINER).once expect(callback).to receive(:call).with(project.id, Gitlab::Access::OWNER).once
service.execute service.execute
end end
@ -73,7 +73,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
to_be_removed = [project_authorization.project_id] to_be_removed = [project_authorization.project_id]
to_be_added = [ to_be_added = [
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER } { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
] ]
expect(service).to receive(:update_authorizations) expect(service).to receive(:update_authorizations)
@ -83,14 +83,14 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
end end
it 'removes duplicate entries' do it 'removes duplicate entries' do
[Gitlab::Access::MAINTAINER, Gitlab::Access::REPORTER].each do |access_level| [Gitlab::Access::OWNER, Gitlab::Access::REPORTER].each do |access_level|
user.project_authorizations.create!(project: project, access_level: access_level) user.project_authorizations.create!(project: project, access_level: access_level)
end end
to_be_removed = [project.id] to_be_removed = [project.id]
to_be_added = [ to_be_added = [
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER } { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
] ]
expect(service).to( expect(service).to(
receive(:update_authorizations) receive(:update_authorizations)
@ -103,7 +103,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
project_authorization = ProjectAuthorization.where( project_authorization = ProjectAuthorization.where(
project_id: project.id, project_id: project.id,
user_id: user.id, user_id: user.id,
access_level: Gitlab::Access::MAINTAINER) access_level: Gitlab::Access::OWNER)
expect(project_authorization).to exist expect(project_authorization).to exist
end end
@ -116,7 +116,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
to_be_removed = [project_authorization.project_id] to_be_removed = [project_authorization.project_id]
to_be_added = [ to_be_added = [
{ user_id: user.id, project_id: project.id, access_level: Gitlab::Access::MAINTAINER } { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
] ]
expect(service).to receive(:update_authorizations) expect(service).to receive(:update_authorizations)

View File

@ -93,13 +93,12 @@ RSpec.shared_examples 'User views a wiki page' do
let(:path) { upload_file_to_wiki(wiki, user, 'dk.png') } let(:path) { upload_file_to_wiki(wiki, user, 'dk.png') }
it do it do
expect(page).to have_xpath("//img[@data-src='#{wiki.wiki_base_path}/#{path}']") expect(page).to have_xpath("//img[@src='#{wiki.wiki_base_path}/#{path}']")
expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}") expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}")
click_on('image') click_on('image')
expect(current_path).to match("wikis/#{path}") expect(current_path).to match("wikis/#{path}")
expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end end
end end

View File

@ -60,7 +60,7 @@ RSpec.shared_examples 'User views wiki pages' do
before do before do
page.within('.wiki-sort-dropdown') do page.within('.wiki-sort-dropdown') do
click_button('Title') click_button('Title')
click_link('Created date') click_button('Created date')
end end
end end

View File

@ -43,33 +43,4 @@ RSpec.describe 'shared/_global_alert.html.haml' do
expect(rendered).not_to have_selector('.gl-dismiss-btn') expect(rendered).not_to have_selector('.gl-dismiss-btn')
end end
end end
context 'fixed layout' do
before do
allow(view).to receive(:fluid_layout).and_return(false)
end
it 'adds container classes' do
render
expect(rendered).to have_selector('.container-fluid.container-limited')
end
it 'does not add container classes if is_contained is true' do
render partial: 'shared/global_alert', locals: { is_contained: true }
expect(rendered).not_to have_selector('.container-fluid.container-limited')
end
end
context 'fluid layout' do
before do
allow(view).to receive(:fluid_layout).and_return(true)
render
end
it 'does not add container classes' do
expect(rendered).not_to have_selector('.container-fluid.container-limited')
end
end
end end