Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-10 21:12:47 +00:00
parent 73fe31a692
commit 256c5ea115
91 changed files with 1856 additions and 378 deletions

View File

@ -463,7 +463,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/pages/ @ashrafkhamis
/doc/administration/polling.md @axil
/doc/administration/postgresql/ @aqualls
/doc/administration/postgresql/multiple_databases.md @jglassman1
/doc/administration/postgresql/multiple_databases.md @lciutacu
/doc/administration/raketasks/ @axil
/doc/administration/raketasks/ldap.md @jglassman1
/doc/administration/raketasks/praefect.md @eread
@ -710,7 +710,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/development/contributing/ @sselhorn
/doc/development/database/ @aqualls
/doc/development/database/filtering_by_label.md @msedlakjakubowski
/doc/development/database/multiple_databases.md @jglassman1
/doc/development/database/multiple_databases.md @lciutacu
/doc/development/database_review.md @aqualls
/doc/development/developing_with_solargraph.md @aqualls
/doc/development/development_processes.md @sselhorn

View File

@ -1,6 +1,10 @@
<!--
Merging into stable branches is reserved for GitLab patch releases
https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases
Merging into stable branches in canonical projects is reserved for
GitLab patch releases https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases
If you're backporting a security fix, please refer to the security merge request
template https://gitlab.com/gitlab-org/security/gitlab/blob/master/.gitlab/merge_request_templates/Security%20Release.md.
Security backport merge requests should not be opened on this project.
-->
## What does this MR do and why?

View File

@ -1 +1 @@
37fd51c54395a06dc39322606f9a3e4ba2dafa3d
ac7304c0ce981bf97dca39ac1017b31860c043b5

View File

@ -99,6 +99,7 @@ export default {
<gl-badge
v-gl-tooltip
variant="warning"
size="sm"
class="gl-mr-2"
:title="__('Pending comments are hidden until you submit your review.')"
>

View File

@ -114,6 +114,7 @@ export default {
defaultFetchOptions: {
exclude_internal: true,
active: true,
order_by: 'similarity',
},
};
</script>

View File

@ -1,5 +1,5 @@
<script>
import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle, GlLink } from '@gitlab/ui';
import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle, GlLink, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import { mapState, mapActions } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
@ -17,6 +17,7 @@ export default {
GlModal,
GlToggle,
GlLink,
GlAlert,
},
directives: {
SafeHtml,
@ -27,6 +28,7 @@ export default {
data() {
return {
modalId: 'delete-self-monitor-modal',
showDeprecationNotice: true,
};
},
computed: {
@ -49,6 +51,20 @@ export default {
selfMonitorProjectFullUrl() {
return `${getBaseURL()}/${this.projectPath}`;
},
selfMonitoringDeprecationNotice() {
return sprintf(
s__(
'SelfMonitoring|Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}.',
),
{
deprecation: `<a href="${this.deprecationPath}">`,
removal: `<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909">`,
opstrace: `<a href="https://gitlab.com/groups/gitlab-org/-/epics/6976">`,
link_end: `</a>`,
},
false,
);
},
selfMonitoringFormText() {
if (this.projectCreated) {
return sprintf(
@ -70,6 +86,9 @@ export default {
helpDocsPath() {
return helpPagePath('administration/monitoring/gitlab_self_monitoring_project/index');
},
deprecationPath() {
return helpPagePath('update/deprecations.md', { anchor: 'gitlab-self-monitoring-project' });
},
},
watch: {
selfMonitorEnabled() {
@ -123,6 +142,9 @@ export default {
viewSelfMonitorProject() {
visitUrl(this.selfMonitorProjectFullUrl);
},
hideDeprecationNotice() {
this.showDeprecationNotice = false;
},
},
};
</script>
@ -140,6 +162,16 @@ export default {
<gl-link :href="helpDocsPath">{{ __('Learn more.') }}</gl-link>
</p>
</div>
<gl-alert
v-if="showDeprecationNotice"
class="gl-mb-3"
:title="s__('SelfMonitoring|Deprecation notice')"
:dismissible="true"
variant="danger"
@dismiss="hideDeprecationNotice"
>
<div v-safe-html="selfMonitoringDeprecationNotice"></div>
</gl-alert>
<div class="settings-content">
<form name="self-monitoring-form">
<p ref="selfMonitoringFormText" v-safe-html="selfMonitoringFormText"></p>

View File

@ -23,6 +23,9 @@ module Mutations
def resolve(project_path:, target_branch:)
project = authorized_find!(project_path)
return respond(nil, ['Feature flag is disabled']) unless Feature.enabled?(:synchronize_fork, project)
details_resolver = Resolvers::Projects::ForkDetailsResolver.new(object: project, context: context, field: nil)
details = details_resolver.resolve(ref: target_branch)

View File

@ -97,7 +97,7 @@ module NavHelper
def super_sidebar_supported?
return true if @nav.nil?
%w(your_work project group).include?(@nav)
%w(your_work project group profile).include?(@nav)
end
def get_header_links

View File

@ -99,6 +99,9 @@ module SidebarsHelper
when 'group'
context = group_sidebar_context(group, user, **context_adds)
Sidebars::Groups::SuperSidebarPanel.new(context)
when 'profile'
context = Sidebars::Context.new(current_user: user, container: user, **context_adds)
Sidebars::UserSettings::Panel.new(context)
else
context = your_work_sidebar_context(user, **context_adds)
Sidebars::YourWork::Panel.new(context)

View File

@ -1,169 +1 @@
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(current_user), 'aria-label': _('User settings') }
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
= render Pajamas::AvatarComponent.new(current_user, size: 32, alt: current_user.name, class: 'gl-mr-3 js-sidebar-user-avatar', avatar_options: { data: { testid: 'sidebar-user-avatar' } })
%span.sidebar-context-title= _('User Settings')
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path do
.nav-icon-container
= sprite_icon('profile')
%span.nav-item-name
= _('Profile')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" }) do
= link_to profile_path do
%strong.fly-out-top-item-name
= _('Profile')
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, data: { qa_selector: 'profile_account_link' } do
.nav-icon-container
= sprite_icon('account')
%span.nav-item-name
= _('Account')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" }) do
= link_to profile_account_path do
%strong.fly-out-top-item-name
= _('Account')
= render_if_exists 'layouts/nav/sidebar/profile_billing_link'
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path do
.nav-icon-container
= sprite_icon('applications')
%span.nav-item-name
= _('Applications')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" }) do
= link_to applications_profile_path do
%strong.fly-out-top-item-name
= _('Applications')
= nav_link(controller: :chat_names) do
= link_to profile_chat_names_path do
.nav-icon-container
= sprite_icon('comment')
%span.nav-item-name
= _('Chat')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" }) do
= link_to profile_chat_names_path do
%strong.fly-out-top-item-name
= _('Chat')
- unless Gitlab::CurrentSettings.personal_access_tokens_disabled?
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path do
.nav-icon-container
= sprite_icon('token')
%span.nav-item-name
= _('Access Tokens')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" }) do
= link_to profile_personal_access_tokens_path do
%strong.fly-out-top-item-name
= _('Access Tokens')
= nav_link(controller: :emails) do
= link_to profile_emails_path, data: { qa_selector: 'profile_emails_link' } do
.nav-icon-container
= sprite_icon('mail')
%span.nav-item-name
= _('Emails')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :emails, html_options: { class: "fly-out-top-item" }) do
= link_to profile_emails_path do
%strong.fly-out-top-item-name
= _('Emails')
- if current_user.allow_password_authentication?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path , data: { qa_selector: 'profile_password_link' } do
.nav-icon-container
= sprite_icon('lock')
%span.nav-item-name
= _('Password')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" }) do
= link_to edit_profile_password_path do
%strong.fly-out-top-item-name
= _('Password')
= nav_link(controller: :notifications) do
= link_to profile_notifications_path do
.nav-icon-container
= sprite_icon('notifications')
%span.nav-item-name
= _('Notifications')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" }) do
= link_to profile_notifications_path do
%strong.fly-out-top-item-name
= _('Notifications')
= nav_link(controller: :keys) do
= link_to profile_keys_path do
.nav-icon-container
= sprite_icon('key')
%span.nav-item-name
= _('SSH Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :keys, html_options: { class: "fly-out-top-item" }) do
= link_to profile_keys_path do
%strong.fly-out-top-item-name
= _('SSH Keys')
= nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path do
.nav-icon-container
= sprite_icon('key')
%span.nav-item-name
= _('GPG Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" }) do
= link_to profile_gpg_keys_path do
%strong.fly-out-top-item-name
= _('GPG Keys')
= nav_link(controller: :preferences) do
= link_to profile_preferences_path do
.nav-icon-container
= sprite_icon('preferences')
%span.nav-item-name
= _('Preferences')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" }) do
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
= _('Preferences')
- if saved_replies_enabled?
= nav_link(controller: :saved_replies) do
= link_to profile_saved_replies_path do
.nav-icon-container
= sprite_icon('symlink')
%span.nav-item-name
= _('Saved Replies')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :saved_replies, html_options: { class: "fly-out-top-item" }) do
= link_to profile_saved_replies_path do
%strong.fly-out-top-item-name
= _('Saved Replies')
= nav_link(controller: :active_sessions) do
= link_to profile_active_sessions_path do
.nav-icon-container
= sprite_icon('monitor-lines')
%span.nav-item-name
= _('Active Sessions')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" }) do
= link_to profile_active_sessions_path do
%strong.fly-out-top-item-name
= _('Active Sessions')
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path do
.nav-icon-container
= sprite_icon('log')
%span.nav-item-name
= _('Authentication log')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" }) do
= link_to audit_log_profile_path do
%strong.fly-out-top-item-name
= _('Authentication Log')
= render_if_exists 'layouts/nav/sidebar/profile_usage_quotas_link'
= render 'shared/sidebar_toggle_button'
= render partial: 'shared/nav/sidebar', object: Sidebars::UserSettings::Panel.new(Sidebars::Context.new(current_user: current_user, container: current_user))

View File

@ -0,0 +1,4 @@
%li.context-header
= link_to profile_path, title: _('User Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
= render Pajamas::AvatarComponent.new(current_user, size: 32, alt: current_user.name, class: 'gl-mr-3 js-sidebar-user-avatar', avatar_options: { data: { testid: 'sidebar-user-avatar' } })
%span.sidebar-context-title= _('User Settings')

View File

@ -0,0 +1,8 @@
---
name: reduce_sub_batch_size_on_timeouts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109354
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393556
milestone: '15.10'
type: development
group: group::database
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: synchronize_fork
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114299
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395793
milestone: '15.10'
type: development
group: group::source code
default_enabled: false

View File

@ -192,7 +192,7 @@ accordingly, while also consulting the
- GitLab 12: `12.0.12` > [`12.1.17`](#1210) > [`12.10.14`](#12100)
- GitLab 13: `13.0.14` > [`13.1.11`](#1310) > [`13.8.8`](#1388) > [`13.12.15`](#13120)
- GitLab 14: [`14.0.12`](#1400) > [`14.3.6`](#1430) > [`14.9.5`](#1490) > [`14.10.5`](#14100)
- GitLab 15: [`15.0.5`](#1500) > [`15.1.6`](#1510) (for GitLab instances with multiple web nodes) > [`15.4.6`](#1540) > [`15.6.x, 15.7.x or 15.8.x`](#user-profile-data-loss-bug-in-159x) > [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
- GitLab 15: [`15.0.5`](#1500) > [`15.1.6`](#1510) (for GitLab instances with multiple web nodes) > [`15.4.6`](#1540) > [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
NOTE:
When not explicitly specified, upgrade GitLab to the latest available patch
@ -266,7 +266,9 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
### 15.9.0
- There is a [database migration bug in GitLab 15.9.x](#user-profile-data-loss-bug-in-159x) that can cause data to be lost from the user profile fields. This bug affects all currently available 15.9.x releases. Until a bug fix is released, you should upgrade to 15.6.x, 15.7.x, or 15.8.x first.
- **Upgrade to patch release 15.9.3 or later**. This provides fixes for two database migration bugs:
- Patch releases 15.9.0, 15.9.1, 15.9.2 have [a bug that can cause data loss](#user-profile-data-loss-bug-in-159x) from the user profile fields.
- The second [bug fix](https://gitlab.com/gitlab-org/gitlab/-/issues/394760) ensures it is possible to upgrade directly from 15.4.x.
- As part of the [CI Partitioning effort](../architecture/blueprints/ci_data_decay/pipeline_partitioning.md), a [new Foreign Key](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107547) was added to `ci_builds_needs`. On GitLab instances with large CI tables, adding this constraint can take longer than usual. Make sure that this migration is finished before upgrading to 15.9.
- Praefect's metadata verifier's [invalid metadata deletion behavior](../administration/gitaly/praefect.md#enable-deletions) is now enabled by default.
@ -298,7 +300,6 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
### 15.8.0
- Due to a bug in GitLab 15.9.x that can cause data to be lost from certain user profile fields, 15.6, 15.7, or 15.8 is temporarily a required stop on the upgrade path. This requirement will be removed when a 15.9.x bug fix is released. [Read more about this issue](#user-profile-data-loss-bug-in-159x).
- Git 2.38.0 and later is required by Gitaly. For installations from source, you should use the [Git version provided by Gitaly](../install/installation.md#git).
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
- Geo: We discovered an issue where [replication and verification of projects and wikis was not keeping up](https://gitlab.com/gitlab-org/gitlab/-/issues/387980) on small number of Geo installations. Your installation may be affected if you see some projects and/or wikis persistently in the "Queued" state for verification. This can lead to data loss after a failover.
@ -351,7 +352,6 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
### 15.7.0
- Due to a bug in GitLab 15.9.x that can cause data to be lost from certain user profile fields, 15.6, 15.7, or 15.8 is temporarily a required stop on the upgrade path. This requirement will be removed when a 15.9.x bug fix is released. [Read more about this issue](#user-profile-data-loss-bug-in-159x).
- This version validates a `NOT NULL DB` constraint on the `issues.work_item_type_id` column.
To upgrade to this version, no records with a `NULL` `work_item_type_id` should exist on the `issues` table.
There are multiple `BackfillWorkItemTypeIdForIssues` background migrations that will be finalized with
@ -461,7 +461,6 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
### 15.6.0
- Due to a bug in GitLab 15.9.x that can cause data to be lost from certain user profile fields, 15.6, 15.7, or 15.8 is temporarily a required stop on the upgrade path. This requirement will be removed when a 15.9.x bug fix is released. [Read more about this issue](#user-profile-data-loss-bug-in-159x).
- You should use one of the [officially supported PostgreSQL versions](../administration/package_information/postgresql_versions.md). Some database migrations can cause stability and performance issues with older PostgreSQL versions.
- Git 2.37.0 and later is required by Gitaly. For installations from source, we recommend you use the [Git version provided by Gitaly](../install/installation.md#git).
- A database change to modify the behavior of four indexes fails on instances

View File

@ -8,6 +8,10 @@ module API
before { authenticate_non_get! }
rescue_from ActiveRecord::QueryCanceled do |_e|
render_api_error!({ error: 'Request timed out' }, 408)
end
helpers Helpers::MergeRequestsHelpers
# These endpoints are defined in `TimeTrackingEndpoints` and is shared by

View File

@ -7,6 +7,8 @@ module Gitlab
#
# Job arguments needed must be defined explicitly,
# see https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#job-arguments.
# rubocop:disable Metrics/ClassLength
# rubocop:disable Metrics/ParameterLists
class BatchedMigrationJob
include Gitlab::Database::DynamicModelHelpers
include Gitlab::ClassAttributes
@ -60,7 +62,8 @@ module Gitlab
end
def initialize(
start_id:, end_id:, batch_table:, batch_column:, sub_batch_size:, pause_ms:, job_arguments: [], connection:
start_id:, end_id:, batch_table:, batch_column:, sub_batch_size:, pause_ms:, job_arguments: [], connection:,
sub_batch_exception: nil
)
@start_id = start_id
@ -71,6 +74,7 @@ module Gitlab
@pause_ms = pause_ms
@job_arguments = job_arguments
@connection = connection
@sub_batch_exception = sub_batch_exception
end
def filter_batch(relation)
@ -87,7 +91,8 @@ module Gitlab
private
attr_reader :start_id, :end_id, :batch_table, :batch_column, :sub_batch_size, :pause_ms, :connection
attr_reader :start_id, :end_id, :batch_table, :batch_column, :sub_batch_size,
:pause_ms, :connection, :sub_batch_exception
def each_sub_batch(batching_arguments: {}, batching_scope: nil)
all_batching_arguments = { column: batch_column, of: sub_batch_size }.merge(batching_arguments)
@ -98,6 +103,10 @@ module Gitlab
sub_batch_relation.each_batch(**all_batching_arguments) do |relation|
batch_metrics.instrument_operation(operation_name) do
yield relation
rescue *Gitlab::Database::BackgroundMigration::BatchedJob::TIMEOUT_EXCEPTIONS => exception
exception_class = sub_batch_exception || exception.class
raise exception_class, exception
end
sleep([pause_ms, 0].max * 0.001)
@ -137,3 +146,5 @@ module Gitlab
end
end
end
# rubocop:enable Metrics/ClassLength
# rubocop:enable Metrics/ParameterLists

View File

@ -4,6 +4,7 @@ module Gitlab
module Database
module BackgroundMigration
SplitAndRetryError = Class.new(StandardError)
ReduceSubBatchSizeError = Class.new(StandardError)
class BatchedJob < SharedModel
include EachBatch
@ -12,6 +13,9 @@ module Gitlab
self.table_name = :batched_background_migration_jobs
MAX_ATTEMPTS = 3
MIN_BATCH_SIZE = 1
SUB_BATCH_SIZE_REDUCE_FACTOR = 0.75
SUB_BATCH_SIZE_THRESHOLD = 65
STUCK_JOBS_TIMEOUT = 1.hour.freeze
TIMEOUT_EXCEPTIONS = [ActiveRecord::StatementTimeout, ActiveRecord::ConnectionTimeoutError,
ActiveRecord::AdapterTimeout, ActiveRecord::LockWaitTimeout,
@ -59,12 +63,12 @@ module Gitlab
end
after_transition any => :failed do |job, transition|
error_hash = transition.args.find { |arg| arg[:error].present? }
exception, from_sub_batch = job.class.extract_transition_options(transition.args)
exception = error_hash&.fetch(:error)
job.reduce_sub_batch_size! if from_sub_batch && job.can_reduce_sub_batch_size?
job.split_and_retry! if job.can_split?(exception)
rescue SplitAndRetryError => error
rescue SplitAndRetryError, ReduceSubBatchSizeError => error
Gitlab::AppLogger.error(
message: error.message,
batched_job_id: job.id,
@ -75,9 +79,7 @@ module Gitlab
end
after_transition do |job, transition|
error_hash = transition.args.find { |arg| arg[:error].present? }
exception = error_hash&.fetch(:error)
exception, _ = job.class.extract_transition_options(transition.args)
job.batched_job_transition_logs.create(previous_status: transition.from, next_status: transition.to, exception_class: exception&.class, exception_message: exception&.message)
@ -100,6 +102,17 @@ module Gitlab
delegate :job_class, :table_name, :column_name, :job_arguments, :job_class_name,
to: :batched_migration, prefix: :migration
def self.extract_transition_options(args)
error_hash = args.find { |arg| arg[:error].present? }
return [] unless error_hash
exception = error_hash.fetch(:error)
from_sub_batch = error_hash[:from_sub_batch]
[exception, from_sub_batch]
end
def time_efficiency
return unless succeeded?
return unless finished_at && started_at
@ -111,10 +124,15 @@ module Gitlab
end
def can_split?(exception)
attempts >= MAX_ATTEMPTS &&
exception&.class&.in?(TIMEOUT_EXCEPTIONS) &&
batch_size > sub_batch_size &&
batch_size > 1
return if still_retryable?
exception.class.in?(TIMEOUT_EXCEPTIONS) && within_batch_size_boundaries?
end
def can_reduce_sub_batch_size?
return false unless Feature.enabled?(:reduce_sub_batch_size_on_timeouts)
still_retryable? && within_batch_size_boundaries?
end
def split_and_retry!
@ -163,6 +181,51 @@ module Gitlab
end
end
end
# It reduces the size of +sub_batch_size+ by 25%
def reduce_sub_batch_size!
raise ReduceSubBatchSizeError, 'Only sub_batch_size of failed jobs can be reduced' unless failed?
return if sub_batch_exceeds_threshold?
with_lock do
actual_sub_batch_size = sub_batch_size
reduced_sub_batch_size = (sub_batch_size * SUB_BATCH_SIZE_REDUCE_FACTOR).to_i.clamp(1, batch_size)
update!(sub_batch_size: reduced_sub_batch_size)
Gitlab::AppLogger.warn(
message: 'Sub batch size reduced due to timeout',
batched_job_id: id,
sub_batch_size: actual_sub_batch_size,
reduced_sub_batch_size: reduced_sub_batch_size,
attempts: attempts,
batched_migration_id: batched_migration.id,
job_class_name: migration_job_class_name,
job_arguments: migration_job_arguments
)
end
end
def still_retryable?
attempts < MAX_ATTEMPTS
end
def within_batch_size_boundaries?
batch_size > MIN_BATCH_SIZE && batch_size > sub_batch_size
end
# It doesn't allow sub-batch size to be reduced lower than the threshold
#
# @info It will prevent the next iteration to reduce the +sub_batch_size+ lower
# than the +SUB_BATCH_SIZE_THRESHOLD+ or 65% of its original size.
def sub_batch_exceeds_threshold?
initial_sub_batch_size = batched_migration.sub_batch_size
reduced_sub_batch_size = (sub_batch_size * SUB_BATCH_SIZE_REDUCE_FACTOR).to_i
diff = initial_sub_batch_size - reduced_sub_batch_size
(1.0 * diff / initial_sub_batch_size * 100).round(2) > SUB_BATCH_SIZE_THRESHOLD
end
end
end
end

View File

@ -15,13 +15,21 @@ module Gitlab
# when starting and finishing execution, and optionally saves batch_metrics
# the migration provides, if any are given.
#
# The job's batch_metrics are serialized to JSON for storage.
# @info The job's batch_metrics are serialized to JSON for storage.
#
# @info Track exceptions that could happen when processing sub-batches
# through +Gitlab::BackgroundMigration::SubBatchTimeoutException+
def perform(batch_tracking_record)
start_tracking_execution(batch_tracking_record)
execute_batch(batch_tracking_record)
batch_tracking_record.succeed!
rescue SubBatchTimeoutError => exception
caused_by = exception.caused_by
batch_tracking_record.failure!(error: caused_by, from_sub_batch: true)
raise caused_by
rescue Exception => error # rubocop:disable Lint/RescueException
batch_tracking_record.failure!(error: error)
@ -67,7 +75,8 @@ module Gitlab
sub_batch_size: tracking_record.sub_batch_size,
pause_ms: tracking_record.pause_ms,
job_arguments: tracking_record.migration_job_arguments,
connection: connection)
connection: connection,
sub_batch_exception: ::Gitlab::Database::BackgroundMigration::SubBatchTimeoutError)
job_instance.perform

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Gitlab
module Database
module BackgroundMigration
class SubBatchTimeoutError < StandardError
def initialize(caused_by)
@caused_by = caused_by
super(caused_by)
end
attr_reader :caused_by
end
end
end
end

View File

@ -4,6 +4,8 @@ module Gitlab
module Database
module SchemaValidation
class Database
STATIC_PARTITIONS_SCHEMA = 'gitlab_partitions_static'
def initialize(connection)
@connection = connection
end
@ -12,33 +14,69 @@ module Gitlab
index_map[index_name]
end
def indexes
index_map.values
def fetch_trigger_by_name(trigger_name)
trigger_map[trigger_name]
end
def index_exists?(index_name)
index_map[index_name].present?
end
def trigger_exists?(trigger_name)
trigger_map[trigger_name].present?
end
def indexes
index_map.values
end
def triggers
trigger_map.values
end
private
attr_reader :connection
def schemas
@schemas ||= [STATIC_PARTITIONS_SCHEMA, connection.current_schema]
end
def index_map
@index_map ||=
fetch_indexes.transform_values! do |index_stmt|
Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt)
SchemaObjects::Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt)
end
end
attr_reader :connection
def trigger_map
@trigger_map ||=
fetch_triggers.transform_values! do |trigger_stmt|
SchemaObjects::Trigger.new(PgQuery.parse(trigger_stmt).tree.stmts.first.stmt.create_trig_stmt)
end
end
def fetch_indexes
sql = <<~SQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ('public', 'gitlab_partitions_static');
WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ($1, $2);
SQL
@fetch_indexes ||= connection.exec_query(sql).rows.to_h
connection.select_rows(sql, nil, schemas).to_h
end
def fetch_triggers
sql = <<~SQL
SELECT triggers.tgname, pg_get_triggerdef(triggers.oid)
FROM pg_catalog.pg_trigger triggers
INNER JOIN pg_catalog.pg_class rel ON triggers.tgrelid = rel.oid
INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = rel.relnamespace
WHERE triggers.tgisinternal IS FALSE
AND nsp.nspname IN ($1, $2)
SQL
connection.select_rows(sql, nil, schemas).to_h
end
end
end

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
class Index
def initialize(parsed_stmt)
@parsed_stmt = parsed_stmt
end
def name
parsed_stmt.idxname
end
def statement
@statement ||= PgQuery.deparse_stmt(parsed_stmt)
end
private
attr_reader :parsed_stmt
end
end
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
class Indexes
def initialize(structure_sql, database)
@structure_sql = structure_sql
@database = database
end
def missing_indexes
structure_sql.indexes.map(&:name) - database.indexes.map(&:name)
end
def extra_indexes
database.indexes.map(&:name) - structure_sql.indexes.map(&:name)
end
def wrong_indexes
structure_sql.indexes.filter_map do |structure_sql_index|
database_index = database.fetch_index_by_name(structure_sql_index.name)
next if database_index.nil?
next if database_index.statement == structure_sql_index.statement
structure_sql_index.name
end
end
private
attr_reader :structure_sql, :database
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module SchemaObjects
class Base
def initialize(parsed_stmt)
@parsed_stmt = parsed_stmt
end
def name
raise NoMethodError, "subclasses of #{self.class.name} must implement #{__method__}"
end
def statement
@statement ||= PgQuery.deparse_stmt(parsed_stmt)
end
private
attr_reader :parsed_stmt
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module SchemaObjects
class Index < Base
def name
parsed_stmt.idxname
end
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module SchemaObjects
class Trigger < Base
def name
parsed_stmt.trigname
end
end
end
end
end
end

View File

@ -4,33 +4,56 @@ module Gitlab
module Database
module SchemaValidation
class StructureSql
def initialize(structure_file_path)
DEFAULT_SCHEMA = 'public'
def initialize(structure_file_path, schema_name = DEFAULT_SCHEMA)
@structure_file_path = structure_file_path
@schema_name = schema_name
end
def index_exists?(index_name)
indexes.find { |index| index.name == index_name }.present?
end
def indexes
@indexes ||= index_statements.map do |index_statement|
index_statement.relation.schemaname = "public" if index_statement.relation.schemaname == ''
def trigger_exists?(trigger_name)
triggers.find { |trigger| trigger.name == trigger_name }.present?
end
Index.new(index_statement)
end
def indexes
@indexes ||= map_with_default_schema(index_statements, SchemaObjects::Index)
end
def triggers
@triggers ||= map_with_default_schema(trigger_statements, SchemaObjects::Trigger)
end
private
attr_reader :structure_file_path
attr_reader :structure_file_path, :schema_name
def index_statements
parsed_structure_file.tree.stmts.filter_map { |s| s.stmt.index_stmt }
statements.filter_map { |s| s.stmt.index_stmt }
end
def trigger_statements
statements.filter_map { |s| s.stmt.create_trig_stmt }
end
def statements
@statements ||= parsed_structure_file.tree.stmts
end
def parsed_structure_file
PgQuery.parse(File.read(structure_file_path))
end
def map_with_default_schema(statements, validation_class)
statements.map do |statement|
statement.relation.schemaname = schema_name if statement.relation.schemaname == ''
validation_class.new(statement)
end
end
end
end
end

View File

@ -15,8 +15,11 @@ module Gitlab
def self.all_validators
[
ExtraIndexes,
ExtraTriggers,
MissingIndexes,
DifferentDefinitionIndexes
MissingTriggers,
DifferentDefinitionIndexes,
DifferentDefinitionTriggers
]
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module Validators
class DifferentDefinitionTriggers < BaseValidator
def execute
structure_sql.triggers.filter_map do |structure_sql_trigger|
database_trigger = database.fetch_trigger_by_name(structure_sql_trigger.name)
next if database_trigger.nil?
next if database_trigger.statement == structure_sql_trigger.statement
build_inconsistency(self.class, structure_sql_trigger)
end
end
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module Validators
class ExtraTriggers < BaseValidator
def execute
database.triggers.filter_map do |trigger|
next if structure_sql.trigger_exists?(trigger.name)
build_inconsistency(self.class, trigger)
end
end
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module Validators
class MissingTriggers < BaseValidator
def execute
structure_sql.triggers.filter_map do |index|
next if database.trigger_exists?(index.name)
build_inconsistency(self.class, index)
end
end
end
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Sidebars
module Concerns
module RenderIfLoggedIn
def render?
!!context.current_user
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class AccessTokensMenu < ::Sidebars::Menu
override :link
def link
profile_personal_access_tokens_path
end
override :title
def title
_('Access Tokens')
end
override :sprite_icon
def sprite_icon
'token'
end
override :render?
def render?
!!context.current_user && !Gitlab::CurrentSettings.personal_access_tokens_disabled?
end
override :active_routes
def active_routes
{ controller: :personal_access_tokens }
end
override :extra_container_html_options
def extra_container_html_options
{ 'data-qa-selector': 'access_token_link' }
end
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class AccountMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_account_path
end
override :title
def title
_('Account')
end
override :sprite_icon
def sprite_icon
'account'
end
override :active_routes
def active_routes
{ controller: [:accounts, :two_factor_auths] }
end
override :extra_container_html_options
def extra_container_html_options
{ 'data-qa-selector': 'profile_account_link' }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class ActiveSessionsMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_active_sessions_path
end
override :title
def title
_('Active Sessions')
end
override :sprite_icon
def sprite_icon
'monitor-lines'
end
override :active_routes
def active_routes
{ controller: :active_sessions }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class ApplicationsMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
applications_profile_path
end
override :title
def title
_('Applications')
end
override :sprite_icon
def sprite_icon
'applications'
end
override :active_routes
def active_routes
{ controller: 'oauth/applications' }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class AuthenticationLogMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
audit_log_profile_path
end
override :title
def title
_('Authentication Log')
end
override :sprite_icon
def sprite_icon
'log'
end
override :active_routes
def active_routes
{ path: 'profiles#audit_log' }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class ChatMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_chat_names_path
end
override :title
def title
_('Chat')
end
override :sprite_icon
def sprite_icon
'comment'
end
override :active_routes
def active_routes
{ controller: :chat_names }
end
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class EmailsMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_emails_path
end
override :title
def title
_('Emails')
end
override :sprite_icon
def sprite_icon
'mail'
end
override :active_routes
def active_routes
{ controller: :emails }
end
override :extra_container_html_options
def extra_container_html_options
{ 'data-qa-selector': 'profile_emails_link' }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class GpgKeysMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_gpg_keys_path
end
override :title
def title
_('GPG Keys')
end
override :sprite_icon
def sprite_icon
'key'
end
override :active_routes
def active_routes
{ controller: :gpg_keys }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class NotificationsMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_notifications_path
end
override :title
def title
_('Notifications')
end
override :sprite_icon
def sprite_icon
'notifications'
end
override :active_routes
def active_routes
{ controller: :notifications }
end
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class PasswordMenu < ::Sidebars::Menu
override :link
def link
edit_profile_password_path
end
override :title
def title
_('Password')
end
override :sprite_icon
def sprite_icon
'lock'
end
override :render?
def render?
!!context.current_user&.allow_password_authentication?
end
override :active_routes
def active_routes
{ controller: :passwords }
end
override :extra_container_html_options
def extra_container_html_options
{ 'data-qa-selector': 'profile_password_link' }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class PreferencesMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_preferences_path
end
override :title
def title
_('Preferences')
end
override :sprite_icon
def sprite_icon
'preferences'
end
override :active_routes
def active_routes
{ controller: :preferences }
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class ProfileMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_path
end
override :title
def title
_('Profile')
end
override :sprite_icon
def sprite_icon
'profile'
end
override :active_routes
def active_routes
{ path: 'profiles#show' }
end
end
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class SavedRepliesMenu < ::Sidebars::Menu
include UsersHelper
override :link
def link
profile_saved_replies_path
end
override :title
def title
_('Saved Replies')
end
override :sprite_icon
def sprite_icon
'symlink'
end
override :render?
def render?
!!context.current_user && saved_replies_enabled?
end
override :active_routes
def active_routes
{ controller: :saved_replies }
end
private
def current_user
context.current_user
end
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
module Menus
class SshKeysMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::RenderIfLoggedIn
override :link
def link
profile_keys_path
end
override :title
def title
_('SSH Keys')
end
override :sprite_icon
def sprite_icon
'key'
end
override :active_routes
def active_routes
{ controller: :keys }
end
override :extra_container_html_options
def extra_container_html_options
{ 'data-qa-selector': 'ssh_keys_link' }
end
end
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
module Sidebars
module UserSettings
class Panel < ::Sidebars::Panel
override :configure_menus
def configure_menus
add_menus
end
override :aria_label
def aria_label
_('User settings')
end
override :render_raw_scope_menu_partial
def render_raw_scope_menu_partial
"shared/nav/user_settings_scope_header"
end
override :super_sidebar_context_header
def super_sidebar_context_header
@super_sidebar_context_header ||= {
title: aria_label,
avatar: context.current_user.avatar_url
}
end
private
def add_menus
add_menu(Sidebars::UserSettings::Menus::ProfileMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::AccountMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::ApplicationsMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::ChatMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::AccessTokensMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::EmailsMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::PasswordMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::NotificationsMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::SshKeysMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::GpgKeysMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::PreferencesMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::SavedRepliesMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::ActiveSessionsMenu.new(context))
add_menu(Sidebars::UserSettings::Menus::AuthenticationLogMenu.new(context))
end
end
end
end
Sidebars::UserSettings::Panel.prepend_mod_with('Sidebars::UserSettings::Panel')

View File

@ -64,11 +64,11 @@ namespace :tw do
CodeOwnerRule.new('Respond', '@msedlakjakubowski'),
CodeOwnerRule.new('Runner', '@fneill'),
CodeOwnerRule.new('Runner SaaS', '@fneill'),
CodeOwnerRule.new('Pods', '@jglassman1'),
CodeOwnerRule.new('Security Policies', '@dianalogan'),
CodeOwnerRule.new('Source Code', '@aqualls'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Tenant Scale', '@lciutacu'),
CodeOwnerRule.new('Testing', '@eread'),
CodeOwnerRule.new('Threat Insights', '@rdickenson'),
CodeOwnerRule.new('Tutorials', '@kpaizee'),

View File

@ -33046,9 +33046,6 @@ msgstr ""
msgid "Profile"
msgstr ""
msgid "Profile Settings"
msgstr ""
msgid "Profile failed to delete"
msgstr ""
@ -39821,6 +39818,9 @@ msgstr ""
msgid "SelfMonitoring|Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project?"
msgstr ""
msgid "SelfMonitoring|Deprecation notice"
msgstr ""
msgid "SelfMonitoring|Self-monitoring"
msgstr ""
@ -39833,6 +39833,9 @@ msgstr ""
msgid "SelfMonitoring|Self-monitoring project successfully deleted."
msgstr ""
msgid "SelfMonitoring|Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}."
msgstr ""
msgid "Send"
msgstr ""

View File

@ -8,25 +8,35 @@ module QA
# since tablets have the regular top navigation bar but still close the left nav
prepend QA::Mobile::Page::SubMenus::Common if QA::Runtime::Env.remote_mobile_device_name
view 'app/views/layouts/nav/sidebar/_profile.html.haml' do
element :access_token_link, 'link_to profile_personal_access_tokens_path' # rubocop:disable QA/ElementWithPattern
element :access_token_title, 'Access Tokens' # rubocop:disable QA/ElementWithPattern
element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern
element :ssh_keys, 'SSH Keys' # rubocop:disable QA/ElementWithPattern
view 'lib/sidebars/user_settings/menus/access_tokens_menu.rb' do
element :access_token_link
end
view 'lib/sidebars/user_settings/menus/ssh_keys_menu.rb' do
element :ssh_keys_link
end
view 'lib/sidebars/user_settings/menus/emails_menu.rb' do
element :profile_emails_link
end
view 'lib/sidebars/user_settings/menus/password_menu.rb' do
element :profile_password_link
end
view 'lib/sidebars/user_settings/menus/account_menu.rb' do
element :profile_account_link
end
def click_access_tokens
within_sidebar do
click_link('Access Tokens')
click_element(:access_token_link)
end
end
def click_ssh_keys
within_sidebar do
click_link('SSH Keys')
click_element(:ssh_keys_link)
end
end

View File

@ -13,7 +13,7 @@ RSpec.describe 'User visits the authentication log', feature_category: :user_pro
it 'shows correct menu item' do
visit(audit_log_profile_path)
expect(page).to have_active_navigation('Authentication log')
expect(page).to have_active_navigation('Authentication Log')
end
end

View File

@ -139,7 +139,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
end
end
it "verified and the gpg user's profile doesn't exist anymore" do
it "verified and the gpg user's profile doesn't exist anymore", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/395802' do
user_1_key
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)

View File

@ -18,3 +18,11 @@ CREATE TABLE ci_project_mirrors (
project_id integer NOT NULL,
namespace_id integer NOT NULL
);
CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1();
CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION my_function();
CREATE TRIGGER missing_trigger_1 BEFORE INSERT OR UPDATE ON public.t3 FOR EACH ROW EXECUTE FUNCTION t3();
CREATE TRIGGER projects_loose_fk_trigger AFTER DELETE ON projects REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();

View File

@ -61,6 +61,7 @@ describe('GroupSelect', () => {
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
exclude_internal: true,
active: true,
order_by: 'similarity',
});
});

View File

@ -40,6 +40,41 @@ exports[`self-monitor component When the self-monitor project has not been creat
</p>
</div>
<gl-alert-stub
class="gl-mb-3"
dismissible="true"
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
showicon="true"
title="Deprecation notice"
variant="danger"
>
<div>
Self-monitoring was
<a
href="/help/update/deprecations.md#gitlab-self-monitoring-project"
>
deprecated
</a>
in GitLab 14.9, and is
<a
href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909"
>
scheduled for removal
</a>
in GitLab 16.0. For information on a possible replacement,
<a
href="https://gitlab.com/groups/gitlab-org/-/epics/6976"
>
learn more about Opstrace
</a>
.
</div>
</gl-alert-stub>
<div
class="settings-content"
>

View File

@ -214,6 +214,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
expect(helper.super_sidebar_nav_panel(nav: 'group')).to be_a(Sidebars::Groups::SuperSidebarPanel)
end
it 'returns User Settings Panel for profile nav' do
expect(helper.super_sidebar_nav_panel(nav: 'profile')).to be_a(Sidebars::UserSettings::Panel)
end
it 'returns "Your Work" Panel for your_work nav', :use_clean_rails_memory_store_caching do
expect(helper.super_sidebar_nav_panel(nav: 'your_work', user: user)).to be_a(Sidebars::YourWork::Panel)
end

View File

@ -301,6 +301,28 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
perform_job
end
context 'when using a sub batch exception for timeouts' do
let(:job_class) do
Class.new(described_class) do
operation_name :update
def perform(*_)
each_sub_batch { raise ActiveRecord::StatementTimeout } # rubocop:disable Lint/UnreachableLoop
end
end
end
let(:job_instance) do
job_class.new(start_id: 1, end_id: 10, batch_table: '_test_table', batch_column: 'id',
sub_batch_size: 2, pause_ms: 1000, connection: connection,
sub_batch_exception: StandardError)
end
it 'raises the expected error type' do
expect { job_instance.perform }.to raise_error(StandardError)
end
end
context 'when batching_arguments are given' do
it 'forwards them for batching' do
expect(job_instance).to receive(:base_relation).and_return(test_table)

View File

@ -184,6 +184,35 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
expect(transition_log.exception_message).to eq('RuntimeError')
end
end
context 'when job fails during sub batch processing' do
let(:args) { { error: ActiveRecord::StatementTimeout.new, from_sub_batch: true } }
let(:attempts) { 0 }
let(:failure) { job.failure!(**args) }
let(:job) do
create(:batched_background_migration_job, :running, batch_size: 20, sub_batch_size: 10, attempts: attempts)
end
context 'when sub batch size can be reduced in 25%' do
it { expect { failure }.to change { job.sub_batch_size }.to 7 }
end
context 'when retries exceeds 2 attempts' do
let(:attempts) { 3 }
before do
allow(job).to receive(:split_and_retry!)
end
it 'calls split_and_retry! once sub_batch_size cannot be decreased anymore' do
failure
expect(job).to have_received(:split_and_retry!).once
end
it { expect { failure }.not_to change { job.sub_batch_size } }
end
end
end
describe 'scopes' do
@ -271,6 +300,24 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
describe '.extract_transition_options' do
let(:perform) { subject.class.extract_transition_options(args) }
where(:args, :expected_result) do
[
[[], []],
[[{ error: StandardError }], [StandardError, nil]],
[[{ error: StandardError, from_sub_batch: true }], [StandardError, true]]
]
end
with_them do
it 'matches expected keys and result' do
expect(perform).to match_array(expected_result)
end
end
end
describe '#can_split?' do
subject { job.can_split?(exception) }
@ -327,6 +374,48 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
describe '#can_reduce_sub_batch_size?' do
let(:attempts) { 0 }
let(:batch_size) { 10 }
let(:sub_batch_size) { 6 }
let(:feature_flag) { :reduce_sub_batch_size_on_timeouts }
let(:job) do
create(:batched_background_migration_job, attempts: attempts,
batch_size: batch_size, sub_batch_size: sub_batch_size)
end
where(:feature_flag_state, :within_boundaries, :outside_boundaries, :limit_reached) do
[
[true, true, false, false],
[false, false, false, false]
]
end
with_them do
before do
stub_feature_flags(feature_flag => feature_flag_state)
end
context 'when the number of attempts is lower than the limit and batch size are within boundaries' do
let(:attempts) { 1 }
it { expect(job.can_reduce_sub_batch_size?).to be(within_boundaries) }
end
context 'when the number of attempts is lower than the limit and batch size are outside boundaries' do
let(:batch_size) { 1 }
it { expect(job.can_reduce_sub_batch_size?).to be(outside_boundaries) }
end
context 'when the number of attempts is greater than the limit and batch size are within boundaries' do
let(:attempts) { 3 }
it { expect(job.can_reduce_sub_batch_size?).to be(limit_reached) }
end
end
end
describe '#time_efficiency' do
subject { job.time_efficiency }
@ -465,4 +554,80 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
end
describe '#reduce_sub_batch_size!' do
let(:migration_batch_size) { 20 }
let(:migration_sub_batch_size) { 10 }
let(:job_batch_size) { 20 }
let(:job_sub_batch_size) { 10 }
let(:status) { :failed }
let(:migration) do
create(:batched_background_migration, :active, batch_size: migration_batch_size,
sub_batch_size: migration_sub_batch_size)
end
let(:job) do
create(:batched_background_migration_job, status, sub_batch_size: job_sub_batch_size,
batch_size: job_batch_size, batched_migration: migration)
end
context 'when the job sub batch size can be reduced' do
let(:expected_sub_batch_size) { 7 }
it 'reduces sub batch size in 25%' do
expect { job.reduce_sub_batch_size! }.to change { job.sub_batch_size }.to(expected_sub_batch_size)
end
it 'log the changes' do
expect(Gitlab::AppLogger).to receive(:warn).with(
message: 'Sub batch size reduced due to timeout',
batched_job_id: job.id,
sub_batch_size: job_sub_batch_size,
reduced_sub_batch_size: expected_sub_batch_size,
attempts: job.attempts,
batched_migration_id: migration.id,
job_class_name: job.migration_job_class_name,
job_arguments: job.migration_job_arguments
)
job.reduce_sub_batch_size!
end
end
context 'when reduced sub_batch_size is greater than sub_batch' do
let(:job_batch_size) { 5 }
it "doesn't allow sub_batch_size to greater than sub_batch" do
expect { job.reduce_sub_batch_size! }.to change { job.sub_batch_size }.to 5
end
end
context 'when sub_batch_size is already 1' do
let(:job_sub_batch_size) { 1 }
it "updates sub_batch_size to it's minimum value" do
expect { job.reduce_sub_batch_size! }.not_to change { job.sub_batch_size }
end
end
context 'when job has not failed' do
let(:status) { :succeeded }
let(:error) { Gitlab::Database::BackgroundMigration::ReduceSubBatchSizeError }
it 'raises an exception' do
expect { job.reduce_sub_batch_size! }.to raise_error(error)
end
end
context 'when the amount to be reduced exceeds the threshold' do
let(:migration_batch_size) { 150 }
let(:migration_sub_batch_size) { 100 }
let(:job_sub_batch_size) { 30 }
it 'prevents sub batch size to be reduced' do
expect { job.reduce_sub_batch_size! }.not_to change { job.sub_batch_size }
end
end
end
end

View File

@ -8,6 +8,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
let(:metrics_tracker) { instance_double('::Gitlab::Database::BackgroundMigration::PrometheusMetrics', track: nil) }
let(:job_class) { Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) }
let(:sub_batch_exception) { Gitlab::Database::BackgroundMigration::SubBatchTimeoutError }
let_it_be(:pause_ms) { 250 }
let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) }
@ -39,7 +40,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
sub_batch_size: 1,
pause_ms: pause_ms,
job_arguments: active_migration.job_arguments,
connection: connection)
connection: connection,
sub_batch_exception: sub_batch_exception)
.and_return(job_instance)
expect(job_instance).to receive(:perform).with(no_args)
@ -119,12 +121,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
end
context 'when the migration job raises an error' do
shared_examples 'an error is raised' do |error_class|
shared_examples 'an error is raised' do |error_class, cause|
let(:expected_to_raise) { cause || error_class }
it 'marks the tracking record as failed' do
expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class)
freeze_time do
expect { perform }.to raise_error(error_class)
expect { perform }.to raise_error(expected_to_raise)
reloaded_job_record = job_record.reload
@ -137,13 +141,16 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class)
expect(metrics_tracker).to receive(:track).with(job_record)
expect { perform }.to raise_error(error_class)
expect { perform }.to raise_error(expected_to_raise)
end
end
it_behaves_like 'an error is raised', RuntimeError.new('Something broke!')
it_behaves_like 'an error is raised', SignalException.new('SIGTERM')
it_behaves_like 'an error is raised', ActiveRecord::StatementTimeout.new('Timeout!')
error = StandardError.new
it_behaves_like('an error is raised', Gitlab::Database::BackgroundMigration::SubBatchTimeoutError.new(error), error)
end
context 'when the batched background migration does not inherit from BatchedMigrationJob' do

View File

@ -3,61 +3,108 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do
let(:database_name) { 'main' }
let(:database_indexes) do
[['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
end
let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
let(:database_model) { Gitlab::Database.database_base_models[database_name] }
let(:connection) { database_model.connection }
subject(:database) { described_class.new(connection) }
before do
allow(connection).to receive(:exec_query).and_return(query_result)
end
let(:database_model) { Gitlab::Database.database_base_models['main'] }
let(:connection) { database_model.connection }
describe '#fetch_index_by_name' do
context 'when index does not exist' do
it 'returns nil' do
index = database.fetch_index_by_name('non_existing_index')
context 'when having indexes' do
let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index }
let(:results) do
[['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
end
expect(index).to be_nil
before do
allow(connection).to receive(:select_rows).and_return(results)
end
describe '#fetch_index_by_name' do
context 'when index does not exist' do
it 'returns nil' do
index = database.fetch_index_by_name('non_existing_index')
expect(index).to be_nil
end
end
it 'returns index by name' do
index = database.fetch_index_by_name('index')
expect(index.name).to eq('index')
end
end
it 'returns index by name' do
index = database.fetch_index_by_name('index')
describe '#index_exists?' do
context 'when index exists' do
it 'returns true' do
index_exists = database.index_exists?('index')
expect(index.name).to eq('index')
end
end
expect(index_exists).to be_truthy
end
end
describe '#index_exists?' do
context 'when index exists' do
it 'returns true' do
index_exists = database.index_exists?('index')
context 'when index does not exist' do
it 'returns false' do
index_exists = database.index_exists?('non_existing_index')
expect(index_exists).to be_truthy
expect(index_exists).to be_falsey
end
end
end
context 'when index does not exist' do
it 'returns false' do
index_exists = database.index_exists?('non_existing_index')
describe '#indexes' do
it 'returns indexes' do
indexes = database.indexes
expect(index_exists).to be_falsey
expect(indexes).to all(be_a(schema_object))
expect(indexes.map(&:name)).to eq(['index'])
end
end
end
describe '#indexes' do
it 'returns indexes' do
indexes = database.indexes
context 'when having triggers' do
let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Trigger }
let(:results) do
{ 'my_trigger' => 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' }
end
expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
expect(indexes.map(&:name)).to eq(['index'])
before do
allow(database).to receive(:fetch_triggers).and_return(results)
end
describe '#fetch_trigger_by_name' do
context 'when trigger does not exist' do
it 'returns nil' do
expect(database.fetch_trigger_by_name('non_existing_trigger')).to be_nil
end
end
it 'returns trigger by name' do
expect(database.fetch_trigger_by_name('my_trigger').name).to eq('my_trigger')
end
end
describe '#trigger_exists?' do
context 'when trigger exists' do
it 'returns true' do
expect(database.trigger_exists?('my_trigger')).to be_truthy
end
end
context 'when trigger does not exist' do
it 'returns false' do
expect(database.trigger_exists?('non_existing_trigger')).to be_falsey
end
end
end
describe '#triggers' do
it 'returns triggers' do
triggers = database.triggers
expect(triggers).to all(be_a(schema_object))
expect(triggers.map(&:name)).to eq(['my_trigger'])
end
end
end
end

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::Index, feature_category: :database do
let(:index_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
let(:stmt) { PgQuery.parse(index_statement).tree.stmts.first.stmt.index_stmt }
let(:index) { described_class.new(stmt) }
describe '#name' do
it 'returns index name' do
expect(index.name).to eq('index_name')
end
end
describe '#statement' do
it 'returns index statement' do
expect(index.statement).to eq(index_statement)
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe Gitlab::Database::SchemaValidation::Runner, feature_category: :da
let(:connection) { ActiveRecord::Base.connection }
let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
let(:structure_sql) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
let(:structure_sql) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, 'public') }
describe '#execute' do
subject(:inconsistencies) { described_class.new(structure_sql, database).execute }

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Index, feature_category: :database do
let(:statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
let(:name) { 'index_name' }
include_examples 'schema objects assertions for', 'index_stmt'
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Trigger, feature_category: :database do
let(:statement) { 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' }
let(:name) { 'my_trigger' }
include_examples 'schema objects assertions for', 'create_trig_stmt'
end

View File

@ -4,44 +4,79 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::StructureSql, feature_category: :database do
let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
let(:schema_name) { 'public' }
subject(:structure_sql) { described_class.new(structure_file_path) }
subject(:structure_sql) { described_class.new(structure_file_path, schema_name) }
describe '#index_exists?' do
subject(:index_exists) { structure_sql.index_exists?(index_name) }
context 'when having indexes' do
describe '#index_exists?' do
subject(:index_exists) { structure_sql.index_exists?(index_name) }
context 'when the index does not exist' do
let(:index_name) { 'non-existent-index' }
context 'when the index does not exist' do
let(:index_name) { 'non-existent-index' }
it 'returns false' do
expect(index_exists).to be_falsey
it 'returns false' do
expect(index_exists).to be_falsey
end
end
context 'when the index exists' do
let(:index_name) { 'index' }
it 'returns true' do
expect(index_exists).to be_truthy
end
end
end
context 'when the index exists' do
let(:index_name) { 'index' }
describe '#indexes' do
it 'returns indexes' do
indexes = structure_sql.indexes
it 'returns true' do
expect(index_exists).to be_truthy
expected_indexes = %w[
missing_index
wrong_index
index
index_namespaces_public_groups_name_id
index_on_deploy_keys_id_and_type_and_public
index_users_on_public_email_excluding_null_and_empty
]
expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::SchemaObjects::Index))
expect(indexes.map(&:name)).to eq(expected_indexes)
end
end
end
describe '#indexes' do
it 'returns indexes' do
indexes = structure_sql.indexes
context 'when having triggers' do
describe '#trigger_exists?' do
subject(:trigger_exists) { structure_sql.trigger_exists?(name) }
expected_indexes = %w[
missing_index
wrong_index
index
index_namespaces_public_groups_name_id
index_on_deploy_keys_id_and_type_and_public
index_users_on_public_email_excluding_null_and_empty
]
context 'when the trigger does not exist' do
let(:name) { 'non-existent-trigger' }
expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
expect(indexes.map(&:name)).to eq(expected_indexes)
it 'returns false' do
expect(trigger_exists).to be_falsey
end
end
context 'when the trigger exists' do
let(:name) { 'trigger' }
it 'returns true' do
expect(trigger_exists).to be_truthy
end
end
end
describe '#triggers' do
it 'returns triggers' do
triggers = structure_sql.triggers
expected_triggers = %w[trigger wrong_trigger missing_trigger_1 projects_loose_fk_trigger]
expect(triggers).to all(be_a(Gitlab::Database::SchemaValidation::SchemaObjects::Trigger))
expect(triggers.map(&:name)).to eq(expected_triggers)
end
end
end
end

View File

@ -9,8 +9,11 @@ RSpec.describe Gitlab::Database::SchemaValidation::Validators::BaseValidator, fe
it 'returns an array of all validators' do
expect(all_validators).to eq([
Gitlab::Database::SchemaValidation::Validators::ExtraIndexes,
Gitlab::Database::SchemaValidation::Validators::ExtraTriggers,
Gitlab::Database::SchemaValidation::Validators::MissingIndexes,
Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes
Gitlab::Database::SchemaValidation::Validators::MissingTriggers,
Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes,
Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers
])
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers,
feature_category: :database do
include_examples 'trigger validators', described_class, ['wrong_trigger']
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraTriggers, feature_category: :database do
include_examples 'trigger validators', described_class, ['extra_trigger']
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingTriggers, feature_category: :database do
missing_triggers = %w[missing_trigger_1 projects_loose_fk_trigger]
include_examples 'trigger validators', described_class, missing_triggers
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::AccessTokensMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/personal_access_tokens',
title: _('Access Tokens'),
icon: 'token',
active_routes: { controller: :personal_access_tokens }
describe '#render?' do
subject { described_class.new(context) }
let_it_be(:user) { build(:user) }
context 'when personal access tokens are disabled' do
before do
allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: true)
end
context 'when user is logged in' do
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
it 'does not render' do
expect(subject.render?).to be false
end
end
context 'when user is not logged in' do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
subject { described_class.new(context) }
it 'does not render' do
expect(subject.render?).to be false
end
end
end
context 'when personal access tokens are enabled' do
before do
allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: false)
end
context 'when user is logged in' do
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
it 'renders' do
expect(subject.render?).to be true
end
end
context 'when user is not logged in' do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
subject { described_class.new(context) }
it 'does not render' do
expect(subject.render?).to be false
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::AccountMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/account',
title: _('Account'),
icon: 'account',
active_routes: { controller: [:accounts, :two_factor_auths] }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::ActiveSessionsMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/active_sessions',
title: _('Active Sessions'),
icon: 'monitor-lines',
active_routes: { controller: :active_sessions }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::ApplicationsMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/applications',
title: _('Applications'),
icon: 'applications',
active_routes: { controller: 'oauth/applications' }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::AuthenticationLogMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/audit_log',
title: _('Authentication Log'),
icon: 'log',
active_routes: { path: 'profiles#audit_log' }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::ChatMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/chat',
title: _('Chat'),
icon: 'comment',
active_routes: { controller: :chat_names }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::EmailsMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/emails',
title: _('Emails'),
icon: 'mail',
active_routes: { controller: :emails }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::GpgKeysMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/gpg_keys',
title: _('GPG Keys'),
icon: 'key',
active_routes: { controller: :gpg_keys }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::NotificationsMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/notifications',
title: _('Notifications'),
icon: 'notifications',
active_routes: { controller: :notifications }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::PasswordMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/password',
title: _('Password'),
icon: 'lock',
active_routes: { controller: :passwords }
describe '#render?' do
subject { described_class.new(context) }
let_it_be(:user) { build(:user) }
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
context 'when password authentication is enabled' do
before do
allow(user).to receive(:allow_password_authentication?).and_return(true)
end
it 'renders' do
expect(subject.render?).to be true
end
end
context 'when password authentication is disabled' do
before do
allow(user).to receive(:allow_password_authentication?).and_return(false)
end
it 'renders' do
expect(subject.render?).to be false
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::PreferencesMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/preferences',
title: _('Preferences'),
icon: 'preferences',
active_routes: { controller: :preferences }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::ProfileMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile',
title: _('Profile'),
icon: 'profile',
active_routes: { path: 'profiles#show' }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::SavedRepliesMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/saved_replies',
title: _('Saved Replies'),
icon: 'symlink',
active_routes: { controller: :saved_replies }
describe '#render?' do
subject { described_class.new(context) }
let_it_be(:user) { build(:user) }
context 'when saved replies are enabled' do
before do
allow(subject).to receive(:saved_replies_enabled?).and_return(true)
end
context 'when user is logged in' do
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
it 'does not render' do
expect(subject.render?).to be true
end
end
context 'when user is not logged in' do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
subject { described_class.new(context) }
it 'does not render' do
expect(subject.render?).to be false
end
end
end
context 'when saved replies are disabled' do
before do
allow(subject).to receive(:saved_replies_enabled?).and_return(false)
end
context 'when user is logged in' do
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
it 'renders' do
expect(subject.render?).to be false
end
end
context 'when user is not logged in' do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
subject { described_class.new(context) }
it 'does not render' do
expect(subject.render?).to be false
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::SshKeysMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
link: '/-/profile/keys',
title: _('SSH Keys'),
icon: 'key',
active_routes: { controller: :keys }
it_behaves_like 'User settings menu #render? method'
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Panel, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
subject { described_class.new(context) }
it 'implements #super_sidebar_context_header' do
expect(subject.super_sidebar_context_header).to eq({ title: _('User settings'), avatar: user.avatar_url })
end
end

View File

@ -28,7 +28,7 @@ RSpec.describe EnsureTimelogsNoteIdBigintBackfillIsFinishedForGitlabDotCom, feat
expect(described_class).send(
expectation,
ensure_bacthed_background_migration_is_finished_for(migration_arguments)
ensure_batched_background_migration_is_finished_for(migration_arguments)
)
migrate!

View File

@ -32,6 +32,24 @@ RSpec.describe "Sync project fork", feature_category: :source_code_management do
source_project.change_head('feature')
end
context 'when synchronize_fork feature flag is disabled' do
before do
stub_feature_flags(synchronize_fork: false)
end
it 'does not call the sync service' do
expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_mutation_response(:project_sync_fork)).to eq(
{
'details' => nil,
'errors' => ['Feature flag is disabled']
})
end
end
context 'when the user does not have permission' do
let_it_be(:current_user) { create(:user) }

View File

@ -168,6 +168,17 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
end
context 'when DB timeouts occur' do
it 'returns a :request_timeout status' do
allow(MergeRequestsFinder).to receive(:new).and_raise(ActiveRecord::QueryCanceled)
path = endpoint_path + '?view=simple'
get api(path, user)
expect(response).to have_gitlab_http_status(:request_timeout)
end
end
it 'returns an array of all merge_requests using simple mode' do
path = endpoint_path + '?view=simple'

View File

@ -101,7 +101,7 @@ RSpec::Matchers.define :be_finalize_background_migration_of do |migration|
end
end
RSpec::Matchers.define :ensure_bacthed_background_migration_is_finished_for do |migration_arguments|
RSpec::Matchers.define :ensure_batched_background_migration_is_finished_for do |migration_arguments|
define_method :matches? do |klass|
expect_next_instance_of(klass) do |instance|
expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)

View File

@ -20,15 +20,15 @@ RSpec.shared_examples "index validators" do |validator, expected_result|
let(:connection) { database_model.connection }
let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
let(:schema) { connection.current_schema }
let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
subject(:result) { validator.new(structure_file, database).execute }
before do
allow(connection).to receive(:exec_query).and_return(query_result)
allow(connection).to receive(:select_rows).and_return(database_indexes)
end
it 'returns index inconsistencies' do

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples "schema objects assertions for" do |stmt_name|
let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt }
let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) }
describe '#name' do
it 'returns schema object name' do
expect(schema_object.name).to eq(name)
end
end
describe '#statement' do
it 'returns schema object statement' do
expect(schema_object.statement).to eq(statement)
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'trigger validators' do |validator, expected_result|
subject(:result) { validator.new(structure_file, database).execute }
let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
let(:inconsistency_type) { validator.name.demodulize.underscore }
let(:database_name) { 'main' }
let(:schema) { 'public' }
let(:database_model) { Gitlab::Database.database_base_models[database_name] }
let(:connection) { database_model.connection }
let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
let(:database_triggers) do
[
['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'],
['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'],
['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()']
]
end
before do
allow(connection).to receive(:select_rows).and_return(database_triggers)
end
it 'returns trigger inconsistencies' do
expect(result.map(&:object_name)).to match_array(expected_result)
expect(result.map(&:type)).to all(eql inconsistency_type)
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
RSpec.shared_examples 'User settings menu' do |link:, title:, icon:, active_routes:|
let_it_be(:user) { create(:user) }
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
subject { described_class.new(context) }
it 'does not contain any sub menu' do
expect(subject.has_items?).to be false
end
it 'renders the correct link' do
expect(subject.link).to match link
end
it 'renders the correct title' do
expect(subject.title).to eq title
end
it 'renders the correct icon' do
expect(subject.sprite_icon).to be icon
end
it 'defines correct active route' do
expect(subject.active_routes).to eq active_routes
end
end
RSpec.shared_examples 'User settings menu #render? method' do
describe '#render?' do
subject { described_class.new(context) }
context 'when user is logged in' do
let_it_be(:user) { build(:user) }
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
it 'renders' do
expect(subject.render?).to be true
end
end
context 'when user is not logged in' do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
it 'does not render' do
expect(subject.render?).to be false
end
end
end
end