Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
881435f2a3
commit
3c55affa66
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActivityPub
|
||||
def self.table_name_prefix
|
||||
"activity_pub_"
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
|
||||
= yield
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
730b861c660b96556969054402a7776f622d42ed98055b0f7099c940ecf03c32
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' => ''
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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('<', '<')
|
||||
class_name_end = section_end.gsub("\033[0K", '').gsub('<', '<')
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }] } }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue