Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-19 15:07:55 +00:00
parent 881435f2a3
commit 3c55affa66
167 changed files with 2454 additions and 873 deletions

View File

@ -3,71 +3,6 @@
Style/PercentLiteralDelimiters:
Exclude:
- 'metrics_server/metrics_server.rb'
- 'spec/lib/gitlab/alert_management/payload/base_spec.rb'
- 'spec/lib/gitlab/asset_proxy_spec.rb'
- 'spec/lib/gitlab/auth/ldap/auth_hash_spec.rb'
- 'spec/lib/gitlab/auth/ldap/config_spec.rb'
- 'spec/lib/gitlab/auth/ldap/person_spec.rb'
- 'spec/lib/gitlab/auth/o_auth/user_spec.rb'
- 'spec/lib/gitlab/auth/saml/auth_hash_spec.rb'
- 'spec/lib/gitlab/auth/saml/user_spec.rb'
- 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- 'spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- 'spec/lib/gitlab/batch_worker_context_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/cache_spec.rb'
- 'spec/lib/gitlab/ci/ansi2html_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/bridge_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/commands_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/environment_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/image_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/root_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/service_spec.rb'
- 'spec/lib/gitlab/ci/config/extendable/entry_spec.rb'
- 'spec/lib/gitlab/ci/config/external/file/base_spec.rb'
- 'spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/seed/build_spec.rb'
- 'spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb'
- 'spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
- 'spec/lib/gitlab/ci/reports/test_suite_spec.rb'
- 'spec/lib/gitlab/ci/status/composite_spec.rb'
- 'spec/lib/gitlab/ci/status/stage/factory_spec.rb'
- 'spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb'
- 'spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb'
- 'spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb'
- 'spec/lib/gitlab/ci/variables/collection/item_spec.rb'
- 'spec/lib/gitlab/ci/yaml_processor/dag_spec.rb'
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- 'spec/lib/gitlab/config/entry/factory_spec.rb'
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/data_builder/build_spec.rb'
- 'spec/lib/gitlab/data_builder/pipeline_spec.rb'
- 'spec/lib/gitlab/data_builder/push_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_job_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb'
- 'spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb'
- 'spec/lib/gitlab/database/migration_helpers_spec.rb'
- 'spec/lib/gitlab/database/postgres_index_spec.rb'
- 'spec/lib/gitlab/database/reindexing_spec.rb'
- 'spec/lib/gitlab/database/transaction/observer_spec.rb'
- 'spec/lib/gitlab/dependency_linker/base_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb'
- 'spec/lib/gitlab/diff/highlight_spec.rb'
- 'spec/lib/gitlab/diff/inline_diff_marker_spec.rb'
- 'spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
- 'spec/lib/gitlab/email/handler_spec.rb'
- 'spec/lib/gitlab/email/receiver_spec.rb'

View File

@ -1,5 +1,4 @@
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
import { persistCache } from 'apollo3-cache-persist';
import ActionCableLink from '~/actioncable_link';
@ -116,18 +115,14 @@ Object.defineProperty(window, 'pendingApolloRequests', {
function createApolloClient(resolvers = {}, config = {}) {
const {
baseUrl,
batchMax = 10,
cacheConfig = { typePolicies: {}, possibleTypes: {} },
fetchPolicy = fetchPolicies.CACHE_FIRST,
typeDefs,
httpHeaders = {},
fetchCredentials = 'same-origin',
path = '/api/graphql',
useGet = false,
} = config;
const shouldUnbatch = gon.features?.unbatchGraphqlQueries;
let ac = null;
let uri = `${gon.relative_url_root || ''}${path}`;
@ -146,7 +141,6 @@ function createApolloClient(resolvers = {}, config = {}) {
// We set to `same-origin` which is default value in modern browsers.
// See https://github.com/whatwg/fetch/pull/585 for more information.
credentials: fetchCredentials,
batchMax,
};
/*
@ -165,14 +159,10 @@ function createApolloClient(resolvers = {}, config = {}) {
return fetch(stripWhitespaceFromQuery(url, uri), options);
};
const requestLink = ApolloLink.split(
() => useGet || shouldUnbatch,
new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
new BatchHttpLink(httpOptions),
);
const requestLink = new HttpLink({ ...httpOptions, fetch: fetchIntervention });
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
(operation) => operation.getContext().hasUpload,
createUploadLink(httpOptions),
);

View File

@ -1,7 +1,6 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
import { initReportAbuse } from '~/users/profile';
import { initProfileTabs } from '~/profile';
import UserTabs from './user_tabs';
@ -25,4 +24,3 @@ const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
initReportAbuse();

View File

@ -1,58 +0,0 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
export default {
name: 'ReportAbuseButton',
components: {
GlButton,
AbuseCategorySelector,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['reportedUserId', 'reportedFromUrl'],
i18n: {
reportAbuse: s__('ReportAbuse|Report abuse to administrator'),
},
data() {
return {
open: false,
};
},
computed: {
buttonTooltipText() {
return this.$options.i18n.reportAbuse;
},
},
methods: {
toggleDrawer(open) {
this.open = open;
},
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
},
};
</script>
<template>
<span>
<gl-button
v-gl-tooltip="buttonTooltipText"
category="primary"
:aria-label="buttonTooltipText"
icon="error"
@click="toggleDrawer(true)"
@mouseout="hideTooltips"
/>
<abuse-category-selector
:reported-user-id="reportedUserId"
:reported-from-url="reportedFromUrl"
:show-drawer="open"
@close-drawer="toggleDrawer(false)"
/>
</span>
</template>

View File

@ -1,23 +0,0 @@
import Vue from 'vue';
import ReportAbuseButton from './components/report_abuse_button.vue';
export const initReportAbuse = () => {
const el = document.getElementById('js-report-abuse');
if (!el) return false;
const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset;
return new Vue({
el,
name: 'ReportAbuseButtonRoot',
provide: {
reportAbusePath,
reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
reportedFromUrl,
},
render(createElement) {
return createElement(ReportAbuseButton);
},
});
};

View File

@ -4,7 +4,6 @@ class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include ProductAnalyticsTracking
include ProductAnalyticsTracking
include SearchRateLimitable
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze
@ -16,6 +15,12 @@ class SearchController < ApplicationController
action: 'executed',
destinations: [:redis_hll, :snowplow]
track_event :autocomplete,
name: 'i_search_total',
label: 'redis_hll_counters.search.search_total_unique_counts_monthly',
action: 'autocomplete',
destinations: [:redis_hll, :snowplow]
def self.search_rate_limited_endpoints
%i[show count autocomplete]
end
@ -39,10 +44,6 @@ class SearchController < ApplicationController
push_frontend_feature_flag(:search_notes_hide_archived_projects, current_user)
end
before_action only: :show do
push_frontend_feature_flag(:search_issues_hide_archived_projects, current_user)
end
before_action only: :show do
push_frontend_feature_flag(:search_merge_requests_hide_archived_projects, current_user)
end

View File

@ -20,6 +20,7 @@ module Ci
filter_by_upgrade_status!
filter_by_runner_type!
filter_by_tag_list!
filter_by_creator_id!
sort!
request_tag_list!
@ -113,6 +114,11 @@ module Ci
end
end
def filter_by_creator_id!
creator_id = @params[:creator_id]
@runners = @runners.with_creator_id(creator_id) if creator_id.present?
end
def sort!
@runners = @runners.order_by(sort_key)
end

View File

@ -41,6 +41,10 @@ module Resolvers
required: false,
description: 'Filter by upgrade status.'
argument :creator_id, ::Types::GlobalIDType[::User].as('UserID'),
required: false,
description: 'Filter runners by creator ID.'
def resolve_with_lookahead(**args)
apply_lookahead(
::Ci::RunnersFinder
@ -68,6 +72,8 @@ module Resolvers
upgrade_status: params[:upgrade_status],
search: params[:search],
sort: params[:sort]&.to_s,
creator_id:
params[:creator_id] ? ::GitlabSchema.parse_gid(params[:creator_id], expected_type: ::User).model_id : nil,
preload: false # we'll handle preloading ourselves
}.compact
.merge(parent_param)

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module ActivityPub
def self.table_name_prefix
"activity_pub_"
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module ActivityPub
class ReleasesSubscription < ApplicationRecord
belongs_to :project, optional: false
enum :status, [:requested, :accepted], default: :requested
attribute :payload, Gitlab::Database::Type::JsonPgSafe.new
validates :payload, json_schema: { filename: 'activity_pub_follow_payload' }, allow_blank: true
validates :subscriber_url, presence: true, uniqueness: { case_sensitive: false, scope: :project_id },
public_url: true
validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id },
public_url: { allow_nil: true }
validates :shared_inbox_url, public_url: { allow_nil: true }
def self.find_by_subscriber_url(subscriber_url)
find_by('LOWER(subscriber_url) = ?', subscriber_url.downcase)
end
end
end

View File

@ -123,6 +123,8 @@ module Ci
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_id })
}
scope :with_creator_id, -> (value) { where(creator_id: value) }
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)

View File

@ -383,7 +383,11 @@ class Note < ApplicationRecord
end
def for_project_noteable?
!(for_personal_snippet? || for_abuse_report?)
!(for_personal_snippet? || for_abuse_report? || group_level_issue?)
end
def group_level_issue?
(for_issue? || for_work_item?) && noteable&.project_id.blank?
end
def for_design?

View File

@ -13,8 +13,6 @@ module Import
GIT_PROTOCOL_PKT_LEN = 4
GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement"
INVALID_BODY_MESSAGE = 'Not a git repository: Invalid response body'
INVALID_CONTENT_TYPE_MESSAGE = 'Not a git repository: Invalid content-type'
def initialize(params)
@params = params
@ -32,35 +30,32 @@ module Import
uri.fragment = nil
url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}")
response, response_body = http_get_and_extract_first_chunks(url)
response_body = ''
result = nil
Gitlab::HTTP.try_get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |fragment|
response_body += fragment
next if response_body.length < GIT_MINIMUM_RESPONSE_LENGTH
validate(uri, response, response_body)
rescue *Gitlab::HTTP::HTTP_ERRORS => err
error_result("HTTP #{err.class.name.underscore} error: #{err.message}")
rescue StandardError => err
ServiceResponse.error(
message: "Internal #{err.class.name.underscore} error: #{err.message}",
reason: 500
)
result = if status_code_is_valid(fragment) && content_type_is_valid(fragment) && response_body_is_valid(response_body)
:success
else
:error
end
# We are interested only in the first chunks of the response
# So we're using stream_body: true and breaking when receive enough body
break
end
if result == :success
ServiceResponse.success
else
ServiceResponse.error(message: "#{uri} is not a valid HTTP Git repository")
end
end
private
def http_get_and_extract_first_chunks(url)
# We are interested only in the first chunks of the response
# So we're using stream_body: true and breaking when receive enough body
response = nil
response_body = ''
Gitlab::HTTP.get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |response_chunk|
response = response_chunk
response_body += response_chunk
break if GIT_MINIMUM_RESPONSE_LENGTH <= response_body.length
end
[response, response_body]
end
def auth
unless @params[:user].to_s.blank?
{
@ -70,38 +65,16 @@ module Import
end
end
def validate(uri, response, response_body)
return status_code_error(uri, response) unless status_code_is_valid?(response)
return error_result(INVALID_CONTENT_TYPE_MESSAGE) unless content_type_is_valid?(response)
return error_result(INVALID_BODY_MESSAGE) unless response_body_is_valid?(response_body)
ServiceResponse.success
def status_code_is_valid(fragment)
fragment.http_response.code == '200'
end
def status_code_error(uri, response)
http_code = response.http_response.code.to_i
message = response.http_response.message || Rack::Utils::HTTP_STATUS_CODES[http_code]
error_result(
"#{uri} endpoint error: #{http_code}#{message.presence&.prepend(' ')}",
http_code
)
def content_type_is_valid(fragment)
fragment.http_response['content-type'] == EXPECTED_CONTENT_TYPE
end
def error_result(message, reason = nil)
ServiceResponse.error(message: message, reason: reason)
end
def status_code_is_valid?(response)
response.http_response.code == '200'
end
def content_type_is_valid?(response)
response.http_response['content-type'] == EXPECTED_CONTENT_TYPE
end
def response_body_is_valid?(response_body)
response_body.length <= GIT_MINIMUM_RESPONSE_LENGTH && response_body.match?(GIT_BODY_MESSAGE_REGEXP)
def response_body_is_valid(response_body)
response_body.match?(GIT_BODY_MESSAGE_REGEXP)
end
end
end

View File

@ -0,0 +1,53 @@
{
"description": "ActivityPub Follow activity payload",
"type": "object",
"required": [
"@context",
"id",
"type",
"actor",
"object"
],
"properties": {
"@context": {
"oneOf": [
{
"type": "string"
},
{
"type": "array"
}
]
},
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"actor": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"id"
],
"id": {
"type": "string"
},
"inbox": {
"type": "string"
},
"additionalProperties": true
}
]
},
"object": {
"type": "string"
},
"additionalProperties": true
}
}

View File

@ -1,2 +0,0 @@
.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
= yield

View File

@ -2,9 +2,5 @@
= render 'middle_dot_divider', stacking: true do
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
- unless Feature.enabled?(:user_profile_overflow_menu_vue)
= render 'middle_dot_divider', stacking: true do
= s_('UserProfile|User ID: %{id}') % { id: @user.id }
= clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
= render 'middle_dot_divider', stacking: true do
= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }

View File

@ -17,32 +17,16 @@
.user-profile
.cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1
%div{ class: container_class }
- if Feature.enabled?(:user_profile_overflow_menu_vue)
.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
= render 'users/follow_user'
-# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- if @user == current_user
= render Pajamas::ButtonComponent.new(href: profile_path,
button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
= s_("UserProfile|Edit profile")
= render 'users/view_gpg_keys'
= render 'users/view_user_in_admin_area'
.js-user-profile-actions{ data: user_profile_actions_data(@user) }
- else
= render layout: 'users/cover_controls' do
- if @user == current_user
= render Pajamas::ButtonComponent.new(href: profile_path,
icon: 'pencil',
button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- elsif current_user
#js-report-abuse{ data: { report_abuse_path: add_category_abuse_reports_path, reported_user_id: @user.id, reported_from_url: user_url(@user) } }
= render 'users/view_gpg_keys'
- if can?(current_user, :read_user_profile, @user)
= render Pajamas::ButtonComponent.new(href: user_path(@user, rss_url_options),
icon: 'rss',
button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
= render 'users/view_user_in_admin_area'
= render 'users/follow_user'
.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
= render 'users/follow_user'
-# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- if @user == current_user
= render Pajamas::ButtonComponent.new(href: profile_path,
button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
= s_("UserProfile|Edit profile")
= render 'users/view_gpg_keys'
= render 'users/view_user_in_admin_area'
.js-user-profile-actions{ data: user_profile_actions_data(@user) }
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?), ('gl-mb-4!' if show_super_sidebar?)] }
.gl-display-inline-block.gl-mx-8.gl-vertical-align-top

View File

@ -1,8 +0,0 @@
---
name: search_issues_hide_archived_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124846
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416483
milestone: '16.2'
type: development
group: group::global search
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: unbatch_graphql_queries
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117407
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/406765
milestone: '16.0'
type: development
group: group::project management
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: user_profile_overflow_menu_vue
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414773
milestone: '16.1'
type: development
group: group::tenant scale
default_enabled: false

View File

@ -9,7 +9,10 @@ Gitlab::Database::QueryAnalyzer.instance.tap do |query_analyzer|
analyzers.append(::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification)
analyzers.append(::Gitlab::Database::QueryAnalyzers::Ci::PartitioningRoutingAnalyzer)
analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection) if Gitlab.dev_or_test_env?
if Gitlab.dev_or_test_env?
analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection)
analyzers.append(::Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch)
end
end
end

View File

@ -0,0 +1,10 @@
---
table_name: activity_pub_releases_subscriptions
classes:
- ActivityPub::ReleasesSubscription
feature_categories:
- release_orchestration
description: Stores subscriptions from external users through ActivityPub for project
releases
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132889
gitlab_schema: gitlab_main

View File

@ -8,8 +8,10 @@ class RecreateBillableIndex < Gitlab::Database::Migration[2.1]
def up
remove_concurrent_index_by_name :users, INDEX_NAME
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, name: INDEX_NAME,
where: "state = 'active' AND (user_type IN (0, 6, 4, 13)) AND (user_type IN (0, 4, 5))"
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -8,9 +8,11 @@ class RecreatedActivityIndex < Gitlab::Database::Migration[2.1]
def up
remove_concurrent_index_by_name :users, INDEX_NAME
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:id, :last_activity_on],
name: INDEX_NAME,
where: "state = 'active' AND user_type IN (0, 4)"
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -6,9 +6,11 @@ class AddUnconfirmedCreatedAtIndexToUsers < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_users_on_unconfirmed_and_created_at_for_active_humans'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:created_at, :id],
name: INDEX_NAME,
where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0)"
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class CreateActivityPubReleasesSubscriptions < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def up
create_table :activity_pub_releases_subscriptions do |t|
t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
t.timestamps_with_timezone null: false
t.integer :status, null: false, limit: 2, default: 1
t.text :shared_inbox_url, limit: 1024
t.text :subscriber_inbox_url, limit: 1024
t.text :subscriber_url, limit: 1024, null: false
t.jsonb :payload, null: true
t.index 'project_id, LOWER(subscriber_url)', name: :index_activity_pub_releases_sub_on_project_id_sub_url,
unique: true
t.index 'project_id, LOWER(subscriber_inbox_url)',
name: :index_activity_pub_releases_sub_on_project_id_inbox_url, unique: true
end
end
def down
drop_table :activity_pub_releases_subscriptions
end
end

View File

@ -6,10 +6,12 @@ class AddUniqueIndexOnProjectsOnRunnersToken < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_uniq_projects_on_runners_token'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects,
:runners_token,
name: INDEX_NAME,
unique: true
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -6,10 +6,12 @@ class AddUniqueIndexOnProjectsOnRunnersTokenEncrypted < Gitlab::Database::Migrat
INDEX_NAME = 'index_uniq_projects_on_runners_token_encrypted'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects,
:runners_token_encrypted,
name: INDEX_NAME,
unique: true
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -7,7 +7,9 @@ class TiebreakUserTypeIndex < Gitlab::Database::Migration[2.0]
OLD_INDEX_NAME = 'index_users_on_user_type'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:user_type, :id], name: NEW_INDEX_NAME
# rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name :users, OLD_INDEX_NAME
end

View File

@ -6,6 +6,7 @@ class AddTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, name: INDEX_NAME, where: <<~QUERY
(COALESCE(linkedin, '') IS DISTINCT FROM '')
OR (COALESCE(twitter, '') IS DISTINCT FROM '')
@ -14,6 +15,7 @@ class AddTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
OR (COALESCE(location, '') IS DISTINCT FROM '')
OR (COALESCE(organization, '') IS DISTINCT FROM '')
QUERY
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -16,7 +16,9 @@ class UpdateBillableUsersIndex < Gitlab::Database::Migration[2.1]
QUERY
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index(:users, :id, where: NEW_INDEX_CONDITION, name: NEW_INDEX)
# rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name(:users, OLD_INDEX)
end

View File

@ -16,7 +16,9 @@ class UpdateBillableUsersIndexForServiceAccounts < Gitlab::Database::Migration[2
QUERY
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index(:users, :id, where: NEW_INDEX_CONDITION, name: NEW_INDEX)
# rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name(:users, OLD_INDEX)
end

View File

@ -6,6 +6,7 @@ class AddUserTypeMigrationIndexes < Gitlab::Database::Migration[2.1]
BILLABLE_INDEX = 'index_users_for_active_billable_users_migration'
LAST_ACTIVITY_INDEX = 'i_users_on_last_activity_for_active_human_service_migration'
# rubocop:disable Migration/PreventIndexCreation
def up
# Temporary indexes to migrate human user_type. See https://gitlab.com/gitlab-org/gitlab/-/issues/386474
add_concurrent_index :users, :id, name: BILLABLE_INDEX,
@ -14,6 +15,7 @@ class AddUserTypeMigrationIndexes < Gitlab::Database::Migration[2.1]
add_concurrent_index :users, [:id, :last_activity_on], name: LAST_ACTIVITY_INDEX,
where: "((state)::text = 'active'::text) AND ((user_type IS NULL OR user_type = 0) OR (user_type = 4))"
end
# rubocop:enable Migration/PreventIndexCreation
def down
remove_concurrent_index_by_name :users, BILLABLE_INDEX

View File

@ -8,9 +8,11 @@ class RecreateUserTypeMigrationIndexes < Gitlab::Database::Migration[2.1]
def up
# Temporary index to migrate human user_type. See https://gitlab.com/gitlab-org/gitlab/-/issues/386474
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, name: BILLABLE_INDEX,
where: "state = 'active' AND ((user_type IS NULL OR user_type = 0) OR (user_type = ANY (ARRAY[0, 6, 4, 13]))) " \
"AND ((user_type IS NULL OR user_type = 0) OR (user_type = ANY (ARRAY[0, 4, 5])))"
# rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name :users, INCORRECT_BILLABLE_INDEX
end

View File

@ -7,9 +7,11 @@ class ChangeUnconfirmedCreatedAtIndexOnUsers < Gitlab::Database::Migration[2.1]
NEW_INDEX_NAME = 'index_users_on_unconfirmed_created_at_active_type_sign_in_count'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:created_at, :id],
name: NEW_INDEX_NAME,
where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0) AND sign_in_count = 0"
# rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name :users, OLD_INDEX_NAME
end

View File

@ -9,7 +9,9 @@ class IndexProjectsOnNamespaceIdAndRepositorySizeLimit < Gitlab::Database::Migra
disable_ddl_transaction!
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects, [:namespace_id, :repository_size_limit], name: INDEX_NAME
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -5,7 +5,9 @@ class AddAuditorIndexToUsersTable < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, where: 'auditor IS true', name: INDEX_NAME
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -6,7 +6,9 @@ class IndexOrgIdOnProjects < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_projects_on_organization_id'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects, :organization_id, name: INDEX_NAME
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -6,7 +6,9 @@ class AddBuildTimeoutIndex < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_projects_on_id_where_build_timeout_geq_than_2629746'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects, :id, where: 'build_timeout >= 2629746', name: INDEX_NAME
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -6,7 +6,9 @@ class IndexUsersOnEmailDomainAndId < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_users_on_email_domain_and_id'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index(:users, "lower(split_part(email, '@', 2)), id", name: INDEX_NAME)
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -6,10 +6,12 @@ class AddIndexOnProjectsForAdjournedDeletion < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_projects_id_for_aimed_for_deletion'
def up
# rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects,
[:id, :marked_for_deletion_at],
where: 'marked_for_deletion_at IS NOT NULL AND pending_delete = false',
name: INDEX_NAME
# rubocop:enable Migration/PreventIndexCreation
end
def down

View File

@ -0,0 +1 @@
730b861c660b96556969054402a7776f622d42ed98055b0f7099c940ecf03c32

View File

@ -10880,6 +10880,30 @@ CREATE SEQUENCE achievements_id_seq
ALTER SEQUENCE achievements_id_seq OWNED BY achievements.id;
CREATE TABLE activity_pub_releases_subscriptions (
id bigint NOT NULL,
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
status smallint DEFAULT 1 NOT NULL,
shared_inbox_url text,
subscriber_inbox_url text,
subscriber_url text NOT NULL,
payload jsonb,
CONSTRAINT check_0ebf38bcaa CHECK ((char_length(subscriber_inbox_url) <= 1024)),
CONSTRAINT check_2afd35ba17 CHECK ((char_length(subscriber_url) <= 1024)),
CONSTRAINT check_61b77ced49 CHECK ((char_length(shared_inbox_url) <= 1024))
);
CREATE SEQUENCE activity_pub_releases_subscriptions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE activity_pub_releases_subscriptions_id_seq OWNED BY activity_pub_releases_subscriptions.id;
CREATE TABLE agent_activity_events (
id bigint NOT NULL,
agent_id bigint NOT NULL,
@ -25864,6 +25888,8 @@ ALTER TABLE ONLY abuse_trust_scores ALTER COLUMN id SET DEFAULT nextval('abuse_t
ALTER TABLE ONLY achievements ALTER COLUMN id SET DEFAULT nextval('achievements_id_seq'::regclass);
ALTER TABLE ONLY activity_pub_releases_subscriptions ALTER COLUMN id SET DEFAULT nextval('activity_pub_releases_subscriptions_id_seq'::regclass);
ALTER TABLE ONLY agent_activity_events ALTER COLUMN id SET DEFAULT nextval('agent_activity_events_id_seq'::regclass);
ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_group_authorizations_id_seq'::regclass);
@ -27632,6 +27658,9 @@ ALTER TABLE ONLY abuse_trust_scores
ALTER TABLE ONLY achievements
ADD CONSTRAINT achievements_pkey PRIMARY KEY (id);
ALTER TABLE ONLY activity_pub_releases_subscriptions
ADD CONSTRAINT activity_pub_releases_subscriptions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY agent_activity_events
ADD CONSTRAINT agent_activity_events_pkey PRIMARY KEY (id);
@ -31229,6 +31258,10 @@ CREATE INDEX index_abuse_trust_scores_on_user_id_and_source_and_created_at ON ab
CREATE UNIQUE INDEX "index_achievements_on_namespace_id_LOWER_name" ON achievements USING btree (namespace_id, lower(name));
CREATE UNIQUE INDEX index_activity_pub_releases_sub_on_project_id_inbox_url ON activity_pub_releases_subscriptions USING btree (project_id, lower(subscriber_inbox_url));
CREATE UNIQUE INDEX index_activity_pub_releases_sub_on_project_id_sub_url ON activity_pub_releases_subscriptions USING btree (project_id, lower(subscriber_url));
CREATE INDEX index_agent_activity_events_on_agent_id_and_recorded_at_and_id ON agent_activity_events USING btree (agent_id, recorded_at, id);
CREATE INDEX index_agent_activity_events_on_agent_token_id ON agent_activity_events USING btree (agent_token_id) WHERE (agent_token_id IS NOT NULL);
@ -38269,6 +38302,9 @@ ALTER TABLE ONLY batched_background_migration_jobs
ALTER TABLE ONLY operations_strategies_user_lists
ADD CONSTRAINT fk_rails_43241e8d29 FOREIGN KEY (strategy_id) REFERENCES operations_strategies(id) ON DELETE CASCADE;
ALTER TABLE ONLY activity_pub_releases_subscriptions
ADD CONSTRAINT fk_rails_4337598314 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_value_stream_settings
ADD CONSTRAINT fk_rails_4360d37256 FOREIGN KEY (value_stream_id) REFERENCES analytics_cycle_analytics_group_value_streams(id) ON DELETE CASCADE;

View File

@ -810,8 +810,8 @@ GraphQL queries are recorded in the file. For example:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133371) in GitLab 16.5.
The `clickhouse.log` file logs information related to
Clickhouse database client within GitLab.
The `clickhouse.log` file logs information related to the
ClickHouse database client in GitLab.
## `migrations.log`

View File

@ -63,6 +63,7 @@ In the following table, you can see:
| [Issue analytics](../../user/group/issues_analytics/index.md) | GitLab 16.5 and later |
| [Custom Text in Emails](../../administration/settings/email.md#custom-additional-text) | GitLab 16.5 and later |
| [Contribution analytics](../../user/group/contribution_analytics/index.md) | GitLab 16.5 and later |
| [Group file templates](../../user/group/manage.md#group-file-templates) | GitLab 16.6 and later |
### Enable registration features

View File

@ -702,6 +702,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryrunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
| <a id="queryrunnerscreatorid"></a>`creatorId` | [`UserID`](#userid) | Filter runners by creator ID. |
| <a id="queryrunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="queryrunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
| <a id="queryrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
@ -18595,6 +18596,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="grouprunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
| <a id="grouprunnerscreatorid"></a>`creatorId` | [`UserID`](#userid) | Filter runners by creator ID. |
| <a id="grouprunnersmembership"></a>`membership` | [`CiRunnerMembershipFilter`](#cirunnermembershipfilter) | Control which runners to include in the results. |
| <a id="grouprunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="grouprunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
@ -23604,6 +23606,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectrunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
| <a id="projectrunnerscreatorid"></a>`creatorId` | [`UserID`](#userid) | Filter runners by creator ID. |
| <a id="projectrunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="projectrunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
| <a id="projectrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |

View File

@ -16,6 +16,7 @@ To make the application work within the GitLab Cells architecture, we need to fi
Here is the suggested approach:
1. Pick a workflow to fix.
1. Firstly, we need to find out the tables that are affected while performing the chosen workflow. As an example, in [this note](https://gitlab.com/gitlab-org/gitlab/-/issues/428600#note_1610331742) we have described how to figure out the list of all tables that are affected when a project is created in a group.
1. For each table affected for the chosen workflow, choose the approriate
[GitLab schema](../database/multiple_databases.md#gitlab-schema).
1. Identify all cross-joins, cross-transactions, and cross-database foreign keys for

View File

@ -356,3 +356,21 @@ If you see this message after trying to invite a user to a group:
1. Ensure the user is a [member of the top-level group](../index.md#search-a-group).
Additionally, see [troubleshooting users receiving a 404 after sign in](#users-receive-a-404).
## Message: The SAML response did not contain an email address. Either the SAML identity provider is not configured to send the attribute, or the identity provider directory does not have an email address value for your user
This error appears when the SAML response does not contain the user's email address in an **email** or **mail** attribute as shown in the following example:
```xml
<Attribute Name="email">
<AttributeValue>user@domain.com/AttributeValue>
</Attribute>
```
Attribute names starting with phrases such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/` like in the following example are not supported. Remove this type of attribute name from the SAML response on the IDP side.
```xml
<Attribute Name="http://schemas.microsoft.com/ws/2008/06/identity/claims/email">
<AttributeValue>user@domain.com/AttributeValue>
</Attribute>
```

View File

@ -32,7 +32,7 @@ Product analytics uses several tools:
- [**Snowplow**](https://docs.snowplow.io/docs) - A developer-first engine for collecting behavioral data, and passing it through to ClickHouse.
- [**ClickHouse**](https://clickhouse.com/docs) - A database suited to store, query, and retrieve analytical data.
- [**Cube**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in Clickhouse.
- [**Cube**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in ClickHouse.
The following diagram illustrates the product analytics flow:
@ -46,7 +46,7 @@ flowchart TB
B --Pass data through--> C[Snowplow Enricher]
end
subgraph Data warehouse
C --Transform and enrich data--> D([Clickhouse])
C --Transform and enrich data--> D([ClickHouse])
end
subgraph Data visualization with dashboards
E([Dashboards]) --Generated from the YAML definition--> F[Panels/Visualizations]

View File

@ -60,7 +60,7 @@ To create a project access token:
1. Enter a name. The token name is visible to any user with permissions to view the project.
1. Enter an expiry date for the token.
- The token expires on that date at midnight UTC.
- If you do not enter an expiry date, the expiry date is automatically set to 365 days later than the current date.
- If you do not enter an expiry date, the expiry date is automatically set to 30 days later than the current date.
- By default, this date can be a maximum of 365 days later than the current date.
- An instance-wide [maximum lifetime](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) setting can limit the maximum allowable lifetime in self-managed instances.
1. Select a role for the token.

View File

@ -26,17 +26,12 @@ You can report a user through their:
> - Report abuse from overflow menu [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414773) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `user_profile_overflow_menu_vue`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/414773) in GitLab 16.4.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `user_profile_overflow_menu_vue`.
On GitLab.com, this feature is available.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/414773) in GitLab 16.6. Feature flag `user_profile_overflow_menu_vue` removed.
To report abuse from a user's profile page:
1. Anywhere in GitLab, select the name of the user.
1. In the upper-right corner of the user's profile, if the `user_profile_overflow_menu_vue` feature flag is:
- Enabled, select the vertical ellipsis (**{ellipsis_v}**), then **Report abuse to administrator**.
- Disabled, select **Report abuse to administrator** (**{information-o}**).
1. In the upper-right corner of the user's profile select the vertical ellipsis (**{ellipsis_v}**), then **Report abuse to administrator**.
1. Select a reason for reporting the user.
1. Complete an abuse report.
1. Select **Send report**.

View File

@ -112,19 +112,16 @@ To include archived projects:
1. On the project search page, on the left sidebar, select the **Include archived** checkbox.
1. On the left sidebar, select **Apply**.
## Exclude issues in archived projects from search results
### Include issues in archived projects
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124846) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `search_issues_hide_archived_projects`. Disabled by default.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124846) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `search_issues_hide_archived_projects`. Disabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/416483) in GitLab 16.6. Feature flag `search_issues_hide_archived_projects` removed.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
an administrator can [enable the feature flag](../../administration/feature_flags.md) named `search_issues_hide_archived_projects`. On GitLab.com, this feature is not available.
By default, issues in archived projects are excluded from search results.
To include issues in archived projects:
By default, issues in archived projects are included in search results.
To exclude issues in archived projects, ensure the `search_issues_hide_archived_projects` flag is enabled.
To include issues in archived projects with `search_issues_hide_archived_projects` enabled,
you must add the parameter `include_archived=true` to the URL.
1. On the project search page, on the left sidebar, select the **Include archived** checkbox.
1. On the left sidebar, select **Apply**.
## Search for code

View File

@ -25,6 +25,8 @@ module Gitlab
validates :name, type: Symbol
validates :name, length: { maximum: 255 }
validates :config, mutually_exclusive_keys: %i[script trigger]
validates :config, disallowed_keys: {
in: %i[only except start_in],
message: 'key may not be used with `rules`',

View File

@ -56,8 +56,7 @@ module Gitlab
mutually_exclusive_keys = value.try(:keys).to_a & options[:in]
if mutually_exclusive_keys.length > 1
record.errors.add(attribute, "please use only one of the following keys: " +
mutually_exclusive_keys.join(', '))
record.errors.add(attribute, "these keys cannot be used together: #{mutually_exclusive_keys.join(', ')}")
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch < Base
SetOperatorStarError = Class.new(QueryAnalyzerError)
DETECT_REGEX = /.*SELECT.+(UNION|EXCEPT|INTERSECT)/i
class << self
def enabled?
::Feature::FlipperFeature.table_exists? &&
Feature.enabled?(:query_analyzer_gitlab_schema_metrics, type: :ops)
end
def analyze(parsed)
return unless requires_detection?(parsed.sql)
# Only handle SELECT queries.
parsed.pg.tree.stmts.each do |stmt|
select_stmt = next_select_stmt(stmt)
next unless select_stmt
types = SelectStmt.new(select_stmt).types
raise SetOperatorStarError if types.any?(Type::INVALID)
end
end
private
def next_select_stmt(node)
return unless node.stmt.respond_to?(:select_stmt)
node.stmt.select_stmt
end
# This not entirely correct and will run true on `SELECT union_station, ...`
def requires_detection?(sql)
sql.match DETECT_REGEX
end
end
end
end
end
end

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
# Columns refer to table columns produced by queries and parts of queries.
# If we have `SELECT namespaces.id` then `id` is a column. But, we can also have
# `WHERE namespaces.id > 10` and `id` is also a column.
#
# In static analysis of a SQL query a column source can be ambiguous.
# Such as in `SELECT id FROM users, namespaces. In such cases we assume `id` could come from either `users` or
# `namespaces`.
class Columns
class << self
# Determine the type of each column in the select statement.
# Returns a Set object containing a Types enum.
# When an error is found parsing will return immediately.
def types(select_stmt)
# Forward through any errors when the column refers to a part of the SQL query that is known to include
# errors. For example, the column may refer to a column from a CTE that was invalid.
return Set.new([Type::INVALID]) if References.errors?(select_stmt.all_references)
types = Set.new
# Resolve the type of reference for each target in the select statement.
target_list = select_stmt.node.target_list
targets = target_list.map(&:res_target)
targets.each do |target|
target_type = get_target_type(target, select_stmt)
# A NULL target is of the form:
# SELECT NULL::namespaces FROM namespaces
types += if Targets.null?(target)
# Maintain any errors but otherwise ignore this target.
target_type & [Type::INVALID]
else
target_type
end
end
types
end
private
def get_target_type(target, select_stmt)
target_ref_names = Targets.reference_names(target, select_stmt)
resolved_refs = References.resolved(select_stmt.all_references)
# Cross reference column references with resolved references.
# A resolved reference is part of a SQL query that we were able to analyze already.
# A CTE or sub-query would be such a case. The only non-resolvable reference is a table.
all_resolved = (target_ref_names - resolved_refs.keys).empty?
# Is this target `*` such as `SELECT *`.
a_star = Targets.a_star?(target)
if all_resolved
# Defer to the reference source types.
col_refs = resolved_refs.slice(*target_ref_names)
.values
.reduce(:union) || Set.new
if a_star
# When * the target forwards through the types of the references.
col_refs
else
# When not * the column is static, but we also forward through any nested errors.
(col_refs.to_a & [Type::INVALID]) << Type::STATIC
end
elsif a_star
# This is a * on a table. The * lookup occurs dynamically during query runtime and will
# change when the table schema changes.
[Type::DYNAMIC]
else
# This references a column on a table or intermediate result set such as:
# SELECT namespaces.id FROM namespaces
#
# or:
# WITH some_cte AS ( ... ) SELECT some_cte.id FROM some_cte
[Type::STATIC]
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
# The CTE in a SELECT can reference CTEs defined by the current scope, but also CTEs defined by earlier scopes.
# With the following query as an example:
#
# WITH some_cte AS (select 1)
# SELECT *
# FROM (SELECT * FROM some_cte) subquery
#
# The CTE some_cte is visible from within the subquery scope.
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
class CommonTableExpressions
class << self
# Convert CTEs available within this SELECT statement into a set of References.
#
# @param [PgQuery::Node] node The PgQuery SELECT statement node containing the CTEs.
# @param [References] cte_refs Inherited CTEs from scopes that wrap this SELECT statement.
def references(node, cte_refs)
return cte_refs if node&.with_clause.nil?
refs = cte_refs.dup
node.with_clause.ctes.each do |cte|
cte_name = name(cte)
cte_select_stmt = select_stmt(cte)
# Resolve the CTE type to dynamic/static/error.
refs[cte_name] = if node.with_clause.recursive
# Recursive CTEs need special handling to avoid infinite loops.
recursive_refs(cte_refs, cte_name, cte_select_stmt)
else
SelectStmt.new(cte_select_stmt, cte_refs).types
end
end
refs
end
private
def name(cte)
cte.common_table_expr.ctename
end
def select_stmt(cte)
cte.common_table_expr.ctequery.select_stmt
end
# Return whether the recursive CTE is dynamic/static/error.
def recursive_refs(cte_refs, cte_name, select_stmt)
# Resolve the non-recursive term before the recursive term.
larg_select_stmt = SelectStmt.new(select_stmt.larg, cte_refs)
larg_type = larg_select_stmt.types
new_cte_refs = cte_refs.merge({ cte_name => larg_type })
# Now we can resolve the recursive side.
rarg_type = SelectStmt.new(select_stmt.rarg, new_cte_refs).types
final_type = larg_type | rarg_type
if final_type.count > 1
final_type | [Type::INVALID]
else
final_type
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
class Froms
class << self
# Parse the FROM part of the SELECT. Construct a mapping of FROM names to their PgQuery node. Recurse any
# sub-queries and resolve to a Set of dynamic/static/error.
#
# Whenever a node is aliased, use the alias name as it's reference and ignore it's original name.
#
# For example, given:
#
# SELECT id
# FROM namespaces ns
#
# Return a Hash of { 'ns' => NodeObject }
#
# @param [PgQuery::Node] node The PgQuery SELECT statement node containing the CTEs.
# @param [References] cte_refs Inherited CTEs from scopes that wrap this SELECT statement.
#
# @return [Hash] name of from references mapped to the node that defines their value, or Set if already
# resolved.
def references(node, cte_refs)
refs = {}
return refs unless node
node.from_clause.each do |from|
range_var = Node.dig(from, :range_var)
range_sq = Node.dig(from, :range_subselect)
if range_var
# FROM some_table
# FROM some_table some_alias
refs.merge!(range_var_reference(range_var, cte_refs))
elsif Node.dig(from, :join_expr)
# FROM some_table INNER JOIN other_table
range_vars = Node.locate_descendants(from, :range_var)
range_vars.each do |range_var|
refs.merge!(range_var_reference(range_var, cte_refs))
end
elsif range_sq
# FROM (SELECT ...) some_alias
select_stmt = Node.dig(range_sq, :subquery, :select_stmt)
refs[range_sq.alias.aliasname] = SelectStmt.new(select_stmt, cte_refs).types
end
end
refs
end
private
def range_var_reference(range_var, cte_refs)
relname = Node.dig(range_var, :alias, :aliasname) || range_var.relname
reference = cte_refs[range_var.relname] || range_var
{ relname => reference }
end
end
end
end
end
end
end

View File

@ -0,0 +1,118 @@
# frozen_string_literal: true
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
# The Node class allows us to traverse PgQuery nodes with tree like semantics.
#
# This class balances convenience and performance. The PgQuery nodes are Google::Protobuf::MessageExts which
# contain a dynamic set of attributes known as fields. Accessing these fields can cause performance problems
# due to the large volume of iterable fields.
#
# When possible use #dig over the *descendant* methods.
#
# The filter available to each method reduces the traversed attributes. The default filter only traverses nodes
# required to parse for set operator mismatches.
class Node
class << self
include Gitlab::Utils::StrongMemoize
# The default nodes help speed up traversal. Traversal of other nodes can greatly affect performance.
DEFAULT_NODES = %i[
a_star
alias
args
column_ref
fields
func_call
join_expr
larg
range_subselect
range_var
rarg
res_target
subquery
val
].freeze
DEFAULT_FIELD_FILTER = ->(field) { field.is_a?(Integer) || DEFAULT_NODES.include?(field) }.freeze
# Recurse through children.
# The block will yield the child node and the name of that node.
# Calling without a block will return an Enumerator.
def descendants(node, filter: DEFAULT_FIELD_FILTER, &blk)
if blk
children(node, filter: filter) do |child_node, child_field|
yield(child_node, child_field)
descendants(child_node, filter: filter, &blk)
end
nil
else
enum_for(:descendants, node, filter: filter, &blk)
end
end
# Return the first node that matches the field.
def locate_descendant(node, field, filter: DEFAULT_FIELD_FILTER)
descendants(node, filter: filter).find { |_, child_field| child_field == field }&.first
end
# Return all nodes that match the field.
def locate_descendants(node, field, filter: DEFAULT_FIELD_FILTER)
descendants(node, filter: filter).select { |_, child_field| child_field == field }.map(&:first)
end
# Like Hash#dig, traverse attributes in sequential order and return the final value.
# Return nil if any of the fields are not available.
def dig(node, *attrs)
obj = node
attrs.each do |attr|
if obj.respond_to?(attr)
obj = obj.public_send(attr) # rubocop:disable GitlabSecurity/PublicSend
else
obj = nil
break
end
end
obj
end
private
# Interface with a PgQuery result as though it was a tree node.
# All elements in a PgQuery result are ancestors of Google::Protobuf::AbstractMessage
#
# Based off PgQuery's treewalker https://github.com/pganalyze/pg_query/blob/main/lib/pg_query/treewalker.rb
def children(node, filter: DEFAULT_FIELD_FILTER, &_blk)
attributes = case node
when Google::Protobuf::MessageExts
descriptor_fields(node.class.descriptor)
when Google::Protobuf::RepeatedField
node.count.times.to_a
end
attributes.select(&filter).each do |attr|
attr_key = attr.is_a?(Symbol) ? attr.to_s : attr
child = node[attr_key]
next if child.nil?
yield(child, attr)
end
end
def descriptor_fields(descriptor)
strong_memoize_with(:descriptor_fields, descriptor) do
keys = []
descriptor.each do |field|
keys << field.name.to_sym
end
keys
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
# References form the base data structure of the PreventSetOperatorMismatch query analyzer.
#
# A reference refers to a table, CTE, or other named entity in a SQL query. References are a set of mappings between the
# name of the reference and the PgQuery node that represents that reference in the parsed tree.
#
# Given the SQL:
#
# WITH some_cte AS (SELECT 1)
# SELECT *
# FROM some_cte, users, namespace ns
#
# The reference names would be `some_cte`, `users`, `ns`. The reference values are the nodes in the parse tree that
# represent that reference:
# - some_cte: the common table expression node
# - users: nil, being a table
# - ns: nil, being a table, but importantly we use the alias name
#
# A reference can be "resolved". A resolved reference value is a Set of Types. The reference value was a select
# statement that has since been parsed.
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
class References
class << self
# All references that have already been parsed to determine static/dynamic/error state.
# @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
def resolved(refs)
refs.select { |_name, ref| ref.is_a?(Set) }
end
# All references that have not been parsed to determine static/dynamic/error state.
# @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
def unresolved(refs)
refs.select { |_name, ref| unresolved?(ref) }
end
# Whether any currently resolved references have resulted in an error state.
# @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
def errors?(refs)
resolved(refs).any? { |_, values| values.include?(Type::INVALID) }
end
private
def resolved?(ref)
ref.is_a?(Set)
end
def unresolved?(ref)
!resolved?(ref) && table?(ref)
end
def table?(ref)
!ref.is_a?(PgQuery::RangeVar)
end
end
end
end
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
class SelectStmt
include Gitlab::Utils::StrongMemoize
attr_reader :node, :cte_references, :all_references
# @param [PgQuery::SelectStmt] node The PgQuery node of the select statement.
# @param [Hash] inherited_cte_references CTE References available to the select statement.
def initialize(node, inherited_cte_references = {})
@node = node
@cte_references = CommonTableExpressions.references(node, inherited_cte_references)
from_references = Froms.references(node, cte_references)
@all_references = from_references.merge(cte_references)
end
# returns Set of Types.
#
# STATIC - queries that don't require a database schema lookup. E.g. `SELECT users.id FROM users`
# DYNAMIC - queries that require a database schema lookup. E.g. `SELECT users.* FROM users`
# INVALID - set operator queries that mix static and dynamic queries.
def types
if set_operator?
resolve_set_operator_select_types
else
resolve_normal_select_types
end
end
private
# Standard SELECT, not a set operator (UNION/INTERSECT/EXCEPT)
def resolve_normal_select_types
# Cross reference resolved sources with what is requested by the SELECT.
types = Columns.types(self)
# Mixed dynamic and static queries can be normalized to simply dynamic queries for the purposes of
# detecting mismatched set operator parts.
types.delete(Type::STATIC) if types.include?(Type::DYNAMIC)
types
end
# Set operator (UNION/INTERSECT/EXCEPT)
def resolve_set_operator_select_types
types = Set.new
# Recurse each set operator part as a SELECT statement.
# select statement part => type
set_operator_parts do |part|
types += SelectStmt.new(part, cte_references).types
end
types << Type::INVALID if types.count > 1
types
end
def set_operator?
!(node.respond_to?(:op) && node.op == :SETOP_NONE)
end
SET_OPERATOR_PART_LOCATIONS = %i[larg rarg].freeze
private_constant :SET_OPERATOR_PART_LOCATIONS
def set_operator_parts(&_blk)
return unless node
yield node if node.op == :SETOP_NONE
yield node.larg if node.larg && node.larg.op == :SETOP_NONE
yield node.rarg if node.rarg && node.rarg.op == :SETOP_NONE
end
end
end
end
end
end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
# Targets refer to SELECT columns but also JOIN fields, etc.
# A target can have a qualifying reference to some other entity like a table or CTE.
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
class Targets
class << self
# Return the reference names used by the given target.
#
# For example:
# `SELECT users.id` would return ['users']
# `SELECT * FROM users, namespaces` would return ['users', 'namespaces']
def reference_names(target, select_stmt)
# Parse all targets to determine what is referenced.
fields = fields(target)
case fields.count
when 0
literal_ref_names(target, select_stmt)
when 1
unqualified_ref_names(fields, select_stmt)
else
# The target is qualified such as SELECT reference.id
field_ref = fields[fields.count - 2]
[field_ref.string.sval]
end
end
# True when `SELECT *`
def a_star?(target)
Node.locate_descendant(target, :a_star)
end
# Null targets are used to produce "polymorphic" query result sets that can be aggregated through a UNION
# without having to worry about mismatched columns.
#
# A null target would be something like:
# SELECT NULL::namespaces FROM namespaces
def null?(target)
target&.val&.type_cast&.arg&.a_const&.isnull
end
private
def literal_ref_names(target, select_stmt)
# The target is unqualified and is not part of a column_ref, such as in `SELECT 1`.
# These include targets like literals, functions, and subselects.
sub_select_stmt = subselect_select_stmt(target)
if sub_select_stmt
name = (target.name.presence || "loc_#{target.location}")
# The select is anonymous, so we provide a name.
k = "#{name}_subselect"
# Force parsing of the select.
# We don't care about the static/dynamic nature in this case, but we do need to parse for
# any nested error states.
sub_select = SelectStmt.new(sub_select_stmt, select_stmt.cte_references)
select_stmt.all_references[k] = sub_select.types
[k]
else
# TODO we need to parse function references. Assuming no sources for now.
# https://gitlab.com/gitlab-org/gitlab/-/issues/428102
[]
end
end
def unqualified_ref_names(fields, select_stmt)
# The target is unqualified, but is part of a column_ref.
# E.g. `SELECT id FROM namespaces` or `SELECT namespaces FROM namespaces`
# Otherwise, check all FROM/JOIN/CTE entries.
field = fields[0]
field_sval = field&.string&.sval
if field_sval && select_stmt.all_references.key?(field_sval)
# SELECT some_table_name
[field.string.sval]
else
# SELECT *
# SELECT some_column
select_stmt.all_references.keys
end
end
def fields(target)
Node.locate_descendants(target, :fields).flatten
end
def subselect_select_stmt(target)
Node.dig(target, :val, :sub_link, :subselect, :select_stmt)
end
end
end
end
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Gitlab
module Database
module QueryAnalyzers
class PreventSetOperatorMismatch
# An enumerated set of constants that represent the state of the parse.
module Type
STATIC = :static
DYNAMIC = :dynamic
INVALID = :invalid
end
end
end
end
end

View File

@ -75,7 +75,6 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:vscode_web_ide, current_user)
push_frontend_feature_flag(:unbatch_graphql_queries, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
push_frontend_feature_flag(:custom_emoji)

View File

@ -191,9 +191,7 @@ module Gitlab
unless default_project_filter
project_ids = project_ids_relation
if Feature.enabled?(:search_issues_hide_archived_projects, current_user) && !filters[:include_archived]
project_ids = project_ids.non_archived
end
project_ids = project_ids.non_archived unless filters[:include_archived]
issues = issues.in_projects(project_ids)
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/420046')

View File

@ -51733,9 +51733,6 @@ msgstr ""
msgid "UserProfile|Contributed projects"
msgstr ""
msgid "UserProfile|Copy user ID"
msgstr ""
msgid "UserProfile|Copy user ID: %{id}"
msgstr ""
@ -51838,9 +51835,6 @@ msgstr ""
msgid "UserProfile|User ID copied to clipboard"
msgstr ""
msgid "UserProfile|User ID: %{id}"
msgstr ""
msgid "UserProfile|User profile navigation"
msgstr ""

View File

@ -61,7 +61,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.66.0",
"@gitlab/ui": "66.33.0",
"@gitlab/ui": "66.34.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20231004090414",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",

View File

@ -8,11 +8,11 @@ module RuboCop
class PreventIndexCreation < RuboCop::Cop::Base
include MigrationHelpers
FORBIDDEN_TABLES = %i[ci_builds namespaces].freeze
FORBIDDEN_TABLES = %i[ci_builds namespaces projects users].freeze
MSG = "Adding new index to #{FORBIDDEN_TABLES.join(", ")} is forbidden. " \
"For `ci_builds` see https://gitlab.com/gitlab-org/gitlab/-/issues/332886, " \
"for `namespaces` see https://gitlab.com/groups/gitlab-org/-/epics/11543".freeze
"for `namespaces`, `projects`, and `users` see https://gitlab.com/groups/gitlab-org/-/epics/11543".freeze
def on_new_investigation
super

View File

@ -537,6 +537,34 @@ RSpec.describe SearchController, feature_category: :global_search do
expect(response.headers['Cache-Control']).to eq('max-age=60, private')
expect(response.headers['Pragma']).to be_nil
end
context 'unique users tracking' do
before do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
end
it_behaves_like 'tracking unique hll events' do
subject(:request) { get :autocomplete, params: { term: 'term' } }
let(:target_event) { 'i_search_total' }
let(:expected_value) { instance_of(String) }
end
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject { get :autocomplete, params: { group_id: namespace.id, term: 'term' } }
let(:project) { nil }
let(:category) { described_class.to_s }
let(:action) { 'autocomplete' }
let(:label) { 'redis_hll_counters.search.search_total_unique_counts_monthly' }
let(:property) { 'i_search_total' }
let(:context) do
[Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
end
let(:namespace) { create(:group) }
end
end
describe '#append_info_to_payload' do

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
FactoryBot.define do
factory :activity_pub_releases_subscription, class: 'ActivityPub::ReleasesSubscription' do
project
subscriber_url { 'https://example.com/actor' }
status { :requested }
payload do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://example.com/actor#follow/1',
type: 'Follow',
actor: 'https://example.com/actor',
object: 'http://localhost/user/project/-/releases'
}
end
trait :inbox do
subscriber_inbox_url { 'https://example.com/actor/inbox' }
end
trait :shared_inbox do
shared_inbox_url { 'https://example.com/shared-inbox' }
end
end
end

View File

@ -55,103 +55,53 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
it_behaves_like 'reports the user with an abuse category'
end
describe 'when user_profile_overflow_menu FF turned on' do
context 'when reporting a user profile for abuse' do
let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
context 'when reporting a user profile for abuse' do
let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
before do
visit user_path(abusive_user)
find_by_testid('base-dropdown-toggle').click
end
it_behaves_like 'reports the user with an abuse category'
it 'allows the reporter to report the same user for different abuse categories' do
visit user_path(abusive_user)
find_by_testid('base-dropdown-toggle').click
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
visit user_path(abusive_user)
find_by_testid('base-dropdown-toggle').click
fill_and_submit_abuse_category_form("They're being offensive or abusive.")
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it 'allows multiple users to report the same user' do
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
gitlab_sign_out
gitlab_sign_in(reporter2)
visit user_path(abusive_user)
find_by_testid('base-dropdown-toggle').click
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it_behaves_like 'cancel report'
before do
visit user_path(abusive_user)
find_by_testid('base-dropdown-toggle').click
end
end
describe 'when user_profile_overflow_menu FF turned off' do
context 'when reporting a user profile for abuse' do
let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
it_behaves_like 'reports the user with an abuse category'
before do
stub_feature_flags(user_profile_overflow_menu_vue: false)
visit user_path(abusive_user)
end
it 'allows the reporter to report the same user for different abuse categories' do
visit user_path(abusive_user)
it_behaves_like 'reports the user with an abuse category'
find_by_testid('base-dropdown-toggle').click
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
it 'allows the reporter to report the same user for different abuse categories' do
visit user_path(abusive_user)
expect(page).to have_content 'Thank you for your report'
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
visit user_path(abusive_user)
expect(page).to have_content 'Thank you for your report'
find_by_testid('base-dropdown-toggle').click
fill_and_submit_abuse_category_form("They're being offensive or abusive.")
fill_and_submit_report_abuse_form
visit user_path(abusive_user)
fill_and_submit_abuse_category_form("They're being offensive or abusive.")
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it 'allows multiple users to report the same user' do
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
gitlab_sign_out
gitlab_sign_in(reporter2)
visit user_path(abusive_user)
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it_behaves_like 'cancel report'
expect(page).to have_content 'Thank you for your report'
end
it 'allows multiple users to report the same user' do
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
gitlab_sign_out
gitlab_sign_in(reporter2)
visit user_path(abusive_user)
find_by_testid('base-dropdown-toggle').click
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it_behaves_like 'cancel report'
end
context 'when reporting an merge request for abuse' do
@ -180,10 +130,6 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
end
end
# TODO: implement tests before the FF "user_profile_overflow_menu_vue" is turned on
# See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
# Related Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/416983
private
def fill_and_submit_abuse_category_form(category = "They're posting spam.")

View File

@ -3,23 +3,28 @@
require 'spec_helper'
RSpec.describe 'User reverts a merge request', :js, feature_category: :code_review_workflow do
include Spec::Support::Helpers::ModalHelpers
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
before do
stub_feature_flags(unbatch_graphql_queries: false)
project.add_developer(user)
sign_in(user)
set_cookie('new-actions-popover-viewed', 'true')
visit(merge_request_path(merge_request))
page.within('.mr-state-widget') do
click_button 'Merge'
end
wait_for_requests
wait_for_all_requests
page.refresh
wait_for_requests
# do not reload the page by visiting, let javascript update the page as it will validate we have loaded the modal
# code correctly on page update that adds the `revert` button
end
@ -55,11 +60,11 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi
end
def revert_commit(create_merge_request: false)
click_button('Revert')
click_button 'Revert'
page.within('[data-testid="modal-commit"]') do
within_modal do
uncheck('create_merge_request') unless create_merge_request
click_button('Revert')
click_button 'Revert'
end
end
end

View File

@ -53,7 +53,6 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
let!(:deployment) { build.deployment }
before do
stub_feature_flags(unbatch_graphql_queries: false)
merge_request.update!(head_pipeline: pipeline)
deployment.update!(status: :success)
visit project_merge_request_path(project, merge_request)
@ -84,6 +83,8 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
wait_for_requests
page.refresh
click_button 'Cherry-pick'
page.within(modal_selector) do

View File

@ -12,6 +12,8 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
it 'shows the revert modal' do
click_button('Revert')
wait_for_requests
page.within('[data-testid="modal-commit"]') do
expect(page).to have_content 'Revert this merge request'
end
@ -19,7 +21,6 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
end
before do
stub_feature_flags(unbatch_graphql_queries: false)
sign_in(user)
visit(project_merge_request_path(project, merge_request))
@ -27,6 +28,10 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
click_button 'Merge'
end
wait_for_all_requests
page.refresh
wait_for_requests
end

View File

@ -6,55 +6,29 @@ RSpec.describe 'User RSS', feature_category: :user_profile do
let(:user) { create(:user, :no_super_sidebar) }
let(:path) { user_path(create(:user, :no_super_sidebar)) }
describe 'with "user_profile_overflow_menu_vue" feature flag off' do
context 'when signed in' do
before do
stub_feature_flags(user_profile_overflow_menu_vue: false)
sign_in(user)
visit path
end
context 'when signed in' do
before do
sign_in(user)
visit path
end
it 'shows the RSS link with overflow menu', :js do
find('[data-testid="base-dropdown-toggle"').click
it_behaves_like "it has an RSS button with current_user's feed token"
end
context 'when signed out' do
before do
stub_feature_flags(super_sidebar_logged_out: false)
visit path
end
it_behaves_like "it has an RSS button without a feed token"
expect(page).to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
end
describe 'with "user_profile_overflow_menu_vue" feature flag on', :js do
context 'when signed in' do
before do
sign_in(user)
visit path
end
it 'shows the RSS link with overflow menu' do
find('[data-testid="base-dropdown-toggle"').click
expect(page).to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
context 'when signed out' do
before do
stub_feature_flags(super_sidebar_logged_out: false)
visit path
end
context 'when signed out' do
before do
stub_feature_flags(super_sidebar_logged_out: false)
visit path
end
it 'has an RSS without a feed token', :js do
find('[data-testid="base-dropdown-toggle"').click
it 'has an RSS without a feed token' do
find('[data-testid="base-dropdown-toggle"').click
expect(page).not_to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
expect(page).not_to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
end
end

View File

@ -13,32 +13,12 @@ RSpec.describe 'User page', feature_category: :user_profile do
subject(:visit_profile) { visit(user_path(user)) }
context 'with "user_profile_overflow_menu_vue" feature flag enabled', :js do
it 'does not show the user id in the profile info' do
subject
it 'shows copy user id action in the dropdown', :js do
subject
expect(page).not_to have_content("User ID: #{user.id}")
end
find('[data-testid="base-dropdown-toggle"').click
it 'shows copy user id action in the dropdown' do
subject
find('[data-testid="base-dropdown-toggle"').click
expect(page).to have_content("Copy user ID: #{user.id}")
end
end
context 'with "user_profile_overflow_menu_vue" feature flag disabled', :js do
before do
stub_feature_flags(user_profile_overflow_menu_vue: false)
end
it 'shows user id' do
subject
expect(page).to have_content("User ID: #{user.id}")
end
expect(page).to have_content("Copy user ID: #{user.id}")
end
it 'shows name on breadcrumbs' do

View File

@ -148,6 +148,14 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
end
end
context 'by creator' do
it 'calls the corresponding scope on Ci::Runner' do
expect(Ci::Runner).to receive(:with_creator_id).with('1').and_call_original
described_class.new(current_user: admin, params: { creator_id: '1' }).execute
end
end
end
context 'sorting' do
@ -608,6 +616,16 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
expect(subject).to match_array([runner_project_active, runner_project_inactive])
end
end
context 'by creator' do
let_it_be(:runner_creator_1) { create(:ci_runner, creator_id: '1') }
let(:extra_params) { { creator_id: '1' } }
it 'returns correct runners' do
is_expected.to contain_exactly(runner_creator_1)
end
end
end
end

View File

@ -1,79 +0,0 @@
import { GlButton } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import ReportAbuseButton from '~/users/profile/components/report_abuse_button.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
describe('ReportAbuseButton', () => {
let wrapper;
const ACTION_PATH = '/abuse_reports/add_category';
const USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
wrapper = shallowMountExtended(ReportAbuseButton, {
propsData: {
...props,
},
provide: {
reportAbusePath: ACTION_PATH,
reportedUserId: USER_ID,
reportedFromUrl: REPORTED_FROM_URL,
},
});
};
beforeEach(() => {
createComponent();
});
const findReportAbuseButton = () => wrapper.findComponent(GlButton);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
it('renders report abuse button', () => {
expect(findReportAbuseButton().exists()).toBe(true);
expect(findReportAbuseButton().props()).toMatchObject({
category: 'primary',
icon: 'error',
});
expect(findReportAbuseButton().attributes('aria-label')).toBe(
ReportAbuseButton.i18n.reportAbuse,
);
});
it('renders abuse category selector with the drawer initially closed', () => {
expect(findAbuseCategorySelector().exists()).toBe(true);
expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
});
describe('when button is clicked', () => {
beforeEach(async () => {
await findReportAbuseButton().vm.$emit('click');
});
it('opens the abuse category selector', () => {
expect(findAbuseCategorySelector().props('showDrawer')).toBe(true);
});
it('closes the abuse category selector', async () => {
await findAbuseCategorySelector().vm.$emit('close-drawer');
expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
});
});
describe('when user hovers out of the button', () => {
it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
const rootWrapper = createWrapper(wrapper.vm.$root);
findReportAbuseButton().vm.$emit('mouseout');
expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)).toHaveLength(1);
});
});
});

View File

@ -2,10 +2,8 @@ import { graphqlQuery } from '../graphql';
export default (server) => {
server.post('/api/graphql', (schema, request) => {
const batches = JSON.parse(request.requestBody);
const { query, variables } = JSON.parse(request.requestBody);
return Promise.all(
batches.map(({ query, variables }) => graphqlQuery(query, variables, schema)),
);
return graphqlQuery(query, variables, schema);
});
};

View File

@ -85,7 +85,8 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
type: :instance_type,
tag_list: ['active_runner'],
search: 'abc',
sort: :contacted_asc
sort: :contacted_asc,
creator_id: 'gid://gitlab/User/1'
}
end
@ -98,7 +99,8 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
tag_name: ['active_runner'],
preload: false,
search: 'abc',
sort: 'contacted_asc'
sort: 'contacted_asc',
creator_id: '1'
}
end

View File

@ -32,7 +32,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
context 'with multiple paths provided' do
let(:payload_class) do
Class.new(described_class) do
attribute :test, paths: [['test'], %w(alt test)]
attribute :test, paths: [['test'], %w[alt test]]
end
end
@ -204,8 +204,8 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
end
context 'with too-long hosts array' do
let(:hosts) { %w(abc def ghij) }
let(:shortened_hosts) { %w(abc def ghi) }
let(:hosts) { %w[abc def ghij] }
let(:shortened_hosts) { %w[abc def ghi] }
before do
stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9)
@ -215,15 +215,15 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) }
context 'with host cut off between elements' do
let(:hosts) { %w(abcde fghij) }
let(:shortened_hosts) { %w(abcde fghi) }
let(:hosts) { %w[abcde fghij] }
let(:shortened_hosts) { %w[abcde fghi] }
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end
context 'with nested hosts' do
let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray
let(:shortened_hosts) { %w(abc de f g hi) }
let(:shortened_hosts) { %w[abc de f g hi] }
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end

View File

@ -17,7 +17,7 @@ RSpec.describe Gitlab::AssetProxy do
context 'when asset proxy is enabled' do
before do
stub_asset_proxy_setting(allowlist: %w(gitlab.com *.mydomain.com))
stub_asset_proxy_setting(allowlist: %w[gitlab.com *.mydomain.com])
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',

View File

@ -52,7 +52,7 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do
let(:attributes) do
{
'username' => %w(mail email),
'username' => %w[mail email],
'name' => 'fullName'
}
end

View File

@ -90,7 +90,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
end
it 'returns one provider' do
expect(described_class.available_providers).to match_array(%w(ldapmain))
expect(described_class.available_providers).to match_array(%w[ldapmain])
end
end
@ -552,15 +552,15 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
stub_ldap_config(
options: {
'attributes' => {
'username' => %w(sAMAccountName),
'email' => %w(userPrincipalName)
'username' => %w[sAMAccountName],
'email' => %w[userPrincipalName]
}
}
)
expect(config.attributes).to include({
'username' => %w(sAMAccountName),
'email' => %w(userPrincipalName),
'username' => %w[sAMAccountName],
'email' => %w[userPrincipalName],
'name' => 'cn'
})
end

View File

@ -13,13 +13,13 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
'uid' => 'uid',
'attributes' => {
'name' => 'cn',
'email' => %w(mail email userPrincipalName),
'email' => %w[mail email userPrincipalName],
'username' => username_attribute
}
}
)
end
let(:username_attribute) { %w(uid sAMAccountName userid) }
let(:username_attribute) { %w[uid sAMAccountName userid] }
describe '.normalize_dn' do
subject { described_class.normalize_dn(given) }
@ -57,7 +57,7 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
'attributes' => {
'name' => 'cn',
'email' => 'mail',
'username' => %w(uid mail),
'username' => %w[uid mail],
'first_name' => ''
}
}

View File

@ -369,7 +369,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "and at least one LDAP provider is defined" do
before do
stub_ldap_config(providers: %w(ldapmain))
stub_ldap_config(providers: %w[ldapmain])
end
context "and a corresponding LDAP person" do
@ -570,7 +570,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { 'johndoe@example.com' }
allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) }
allow(ldap_user).to receive(:email) { %w[johndoe@example.com john2@example.com] }
allow(ldap_user).to receive(:dn) { dn }
end
@ -605,7 +605,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "and at least one LDAP provider is defined" do
before do
stub_ldap_config(providers: %w(ldapmain))
stub_ldap_config(providers: %w[ldapmain])
end
context "and a corresponding LDAP person" do
@ -1055,7 +1055,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "update only requested info" do
before do
stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
stub_omniauth_setting(sync_profile_attributes: %w(name location))
stub_omniauth_setting(sync_profile_attributes: %w[name location])
end
it "updates the user name" do

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::AuthHash do
include LoginHelpers
let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } }
let(:raw_info_attr) { { 'groups' => %w[Developers Freelancers] } }
subject(:saml_auth_hash) { described_class.new(omniauth_auth_hash) }
let(:info_hash) do
@ -23,12 +23,12 @@ RSpec.describe Gitlab::Auth::Saml::AuthHash do
end
before do
stub_saml_group_config(%w(Developers Freelancers Designers))
stub_saml_group_config(%w[Developers Freelancers Designers])
end
describe '#groups' do
it 'returns array of groups' do
expect(saml_auth_hash.groups).to eq(%w(Developers Freelancers))
expect(saml_auth_hash.groups).to eq(%w[Developers Freelancers])
end
context 'raw info hash attributes empty' do

View File

@ -11,7 +11,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
let(:uid) { 'my-uid' }
let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'saml' }
let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
let(:raw_info_attr) { { 'groups' => %w[Developers Freelancers Designers] } }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
let(:info_hash) do
{
@ -47,12 +47,12 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'external groups' do
before do
stub_saml_group_config(%w(Interns))
stub_saml_group_config(%w[Interns])
end
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
stub_saml_group_config(%w[Freelancers])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@ -119,7 +119,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'external groups' do
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
stub_saml_group_config(%w[Freelancers])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@ -128,7 +128,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
stub_saml_group_config(%w(Interns))
stub_saml_group_config(%w[Interns])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
@ -151,7 +151,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'and at least one LDAP provider is defined' do
before do
stub_ldap_config(providers: %w(ldapmain))
stub_ldap_config(providers: %w[ldapmain])
end
context 'and a corresponding LDAP person' do
@ -160,7 +160,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:email) { %w[john@mail.com john2@example.com] }
allow(ldap_user).to receive(:dn) { dn }
allow(Gitlab::Auth::Ldap::Adapter).to receive(:new).and_return(adapter)
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
@ -190,14 +190,14 @@ RSpec.describe Gitlab::Auth::Saml::User do
info: info_hash,
extra: {
raw_info: OneLogin::RubySaml::Attributes.new(
{ 'groups' => %w(Developers Freelancers Designers) }
{ 'groups' => %w[Developers Freelancers Designers] }
)
}
}
end
let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) }
let(:uid_types) { %w(uid dn email) }
let(:uid_types) { %w[uid dn email] }
before do
create(:omniauth_user,
@ -410,7 +410,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
let(:raw_info_attr) { {} }
it 'does not mark user as external' do
stub_saml_group_config(%w(Freelancers))
stub_saml_group_config(%w[Freelancers])
expect(saml_user.find_user.external).to be_falsy
end

View File

@ -9,15 +9,15 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
it 'defines generic instance with only some of the attributes set' do
generic_instance = described_class.generic_instance(
batch_table: 'projects', batch_column: 'id',
job_arguments: %w(x y), connection: connection
job_arguments: %w[x y], connection: connection
)
expect(generic_instance.send(:batch_table)).to eq('projects')
expect(generic_instance.send(:batch_column)).to eq('id')
expect(generic_instance.instance_variable_get(:@job_arguments)).to eq(%w(x y))
expect(generic_instance.instance_variable_get(:@job_arguments)).to eq(%w[x y])
expect(generic_instance.send(:connection)).to eq(connection)
%i(start_id end_id sub_batch_size pause_ms).each do |attr|
%i[start_id end_id sub_batch_size pause_ms].each do |attr|
expect(generic_instance.send(attr)).to eq(0)
end
end
@ -36,7 +36,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
batch_column: 'id',
sub_batch_size: 2,
pause_ms: 1000,
job_arguments: %w(a b),
job_arguments: %w[a b],
connection: connection)
end
@ -66,7 +66,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
batch_column: 'id',
sub_batch_size: 2,
pause_ms: 1000,
job_arguments: %w(a b),
job_arguments: %w[a b],
connection: connection)
end
@ -129,7 +129,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
batch_column: 'id',
sub_batch_size: 2,
pause_ms: 1000,
job_arguments: %w(a b),
job_arguments: %w[a b],
connection: connection)
end

View File

@ -16,7 +16,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers)
end
let(:job_arguments) { %w(name name_convert_to_text) }
let(:job_arguments) { %w[name name_convert_to_text] }
let(:copy_job) do
described_class.new(start_id: 12,
end_id: 20,
@ -82,7 +82,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
end
context 'columns with NULLs' do
let(:job_arguments) { %w(name name_convert_to_text) }
let(:job_arguments) { %w[name name_convert_to_text] }
it 'copies all in range' do
expect { copy_job.perform }

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::BatchWorkerContext do
subject(:batch_context) do
described_class.new(
%w(hello world),
%w[hello world],
arguments_proc: -> (word) { word },
context_proc: -> (word) { { user: build_stubbed(:user, username: word) } }
)
@ -13,13 +13,13 @@ RSpec.describe Gitlab::BatchWorkerContext do
describe "#arguments" do
it "returns all the expected arguments in arrays" do
expect(batch_context.arguments).to eq([%w(hello), %w(world)])
expect(batch_context.arguments).to eq([%w[hello], %w[world]])
end
end
describe "#context_for" do
it "returns the correct application context for the arguments" do
context = batch_context.context_for(%w(world))
context = batch_context.context_for(%w[world])
expect(context).to be_a(Gitlab::ApplicationContext)
expect(context.to_lazy_hash[:user].call).to eq("world")

View File

@ -220,7 +220,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea
subject.execute
expect(subject.errors.count).to eq(1)
expect(subject.errors.first.keys).to match_array(%i(type iid errors))
expect(subject.errors.first.keys).to match_array(%i[type iid errors])
end
end

View File

@ -28,7 +28,7 @@ RSpec.describe Gitlab::Cache, :request_store do
end
describe '.delete' do
let(:key) { %w{a cache key} }
let(:key) { %w[a cache key] }
subject(:delete) { described_class.delete(key) }

View File

@ -227,7 +227,7 @@ RSpec.describe Gitlab::Ci::Ansi2html do
text = "#{section_start}Some text#{section_end}"
class_name_start = section_start.gsub("\033[0K", '').gsub('<', '&lt;')
class_name_end = section_end.gsub("\033[0K", '').gsub('<', '&lt;')
html = %{<span>#{class_name_start}Some text#{class_name_end}</span>}
html = %(<span>#{class_name_start}Some text#{class_name_end}</span>)
expect(convert_html(text)).to eq(html)
end
@ -238,9 +238,9 @@ RSpec.describe Gitlab::Ci::Ansi2html do
it 'prints light red' do
text = "#{section_start}\e[91mHello\e[0m\nLine 1\nLine 2\nLine 3\n#{section_end}"
header = %{<span class="term-fg-l-red section section-header js-s-#{class_name(section_name)}">Hello</span>}
line_break = %{<span class="section section-header js-s-#{class_name(section_name)}"><br/></span>}
output_line = %{<span class="section line js-s-#{class_name(section_name)}">Line 1<br/>Line 2<br/>Line 3<br/></span>}
header = %(<span class="term-fg-l-red section section-header js-s-#{class_name(section_name)}">Hello</span>)
line_break = %(<span class="section section-header js-s-#{class_name(section_name)}"><br/></span>)
output_line = %(<span class="section line js-s-#{class_name(section_name)}">Line 1<br/>Line 2<br/>Line 3<br/></span>)
html = "#{section_start_html}#{header}#{line_break}#{output_line}#{section_end_html}"
expect(convert_html(text)).to eq(html)

View File

@ -283,8 +283,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] },
parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) },
{ 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] },
parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w[monitoring app1] },
{ 'PROVIDER' => ['gcp'], 'STACK' => %w[data] }] },
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage

View File

@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
context 'when entry config value is an array of strings' do
let(:config) { %w(ls pwd) }
let(:config) { %w[ls pwd] }
describe '#value' do
it 'returns array of strings' do

View File

@ -93,7 +93,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
context 'when valid action is used' do
where(:action) do
%w(start stop prepare verify access)
%w[start stop prepare verify access]
end
with_them do

View File

@ -56,7 +56,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
end
context 'when configuration is a hash' do
let(:config) { { name: 'image:1.0', entrypoint: %w(/bin/sh run) } }
let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run] } }
describe '#value' do
it 'returns image hash' do
@ -84,13 +84,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
describe '#entrypoint' do
it "returns image's entrypoint" do
expect(entry.entrypoint).to eq %w(/bin/sh run)
expect(entry.entrypoint).to eq %w[/bin/sh run]
end
end
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { { name: 'image:1.0', entrypoint: %w(/bin/sh run), ports: ports } }
let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run], ports: ports } }
let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }

View File

@ -119,6 +119,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli
end
end
context 'when script: and trigger: are used together' do
let(:config) do
{
script: 'echo',
trigger: 'test-group/test-project'
}
end
it 'returns is invalid' do
expect(entry).not_to be_valid
expect(entry.errors).to include(/these keys cannot be used together: script, trigger/)
end
end
context 'when only: is used with rules:' do
let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }

View File

@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
context 'when top-level entries are defined' do
let(:hash) do
{
before_script: %w(ls pwd),
before_script: %w[ls pwd],
image: 'image:1.0',
default: {},
services: ['postgres:9.1', 'mysql:5.5'],
@ -41,7 +41,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
VAR3: { value: 'val3', options: %w[val3 val4 val5], description: 'this is var 3 and some options' }
},
after_script: ['make clean'],
stages: %w(build pages release),
stages: %w[build pages release],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: {}, script: 'spinach' },
@ -123,7 +123,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value[:rspec]).to eq(
{ name: :rspec,
script: %w[rspec ls],
before_script: %w(ls pwd),
before_script: %w[ls pwd],
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
@ -162,7 +162,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success',
unprotect: false, fallback_keys: [] }],
only: { refs: %w(branches tags) },
only: { refs: %w[branches tags] },
job_variables: { 'VAR' => { value: 'job' } },
root_variables_inheritance: true,
after_script: [],
@ -176,14 +176,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
context 'when a mix of top-level and default entries is used' do
let(:hash) do
{ before_script: %w(ls pwd),
{ before_script: %w[ls pwd],
after_script: ['make clean'],
default: {
image: 'image:1.0',
services: ['postgres:9.1', 'mysql:5.5']
},
variables: { VAR: 'root' },
stages: %w(build pages),
stages: %w[build pages],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: { VAR: 'job' }, script: 'spinach' } }
@ -205,7 +205,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
before_script: %w(ls pwd),
before_script: %w[ls pwd],
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',

Some files were not shown because too many files have changed in this diff Show More