Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-06 18:10:28 +00:00
parent b333706699
commit 958f41148d
93 changed files with 2324 additions and 405 deletions

View File

@ -35,10 +35,24 @@ stages:
RUN_WITH_BUNDLE: "true" # installs and runs gitlab-qa via bundler
QA_PATH: qa
.omnibus-env:
variables:
BUILD_ENV: build.env
script:
- |
SECURITY_SOURCES=$([[ ! "$CI_PROJECT_NAMESPACE" =~ ^gitlab-org\/security ]] || echo "true")
echo "SECURITY_SOURCES=${SECURITY_SOURCES:-false}" > $BUILD_ENV
echo "OMNIBUS_GITLAB_CACHE_UPDATE=${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" >> $BUILD_ENV
for version_file in *_VERSION; do echo "$version_file=$(cat $version_file)" >> $BUILD_ENV; done
echo "Built environment file for omnibus build:"
cat $BUILD_ENV
artifacts:
reports:
dotenv: $BUILD_ENV
.update-script:
script:
- export CURRENT_VERSION="$(cat ../VERSION)"
- export QA_COMMAND="bundle exec gitlab-qa Test::Omnibus::UpdateFromPrevious $RELEASE $CURRENT_VERSION $UPDATE_TYPE -- $QA_RSPEC_TAGS $RSPEC_REPORT_OPTS"
- export QA_COMMAND="bundle exec gitlab-qa Test::Omnibus::UpdateFromPrevious $RELEASE $GITLAB_VERSION $UPDATE_TYPE -- $QA_RSPEC_TAGS $RSPEC_REPORT_OPTS"
- echo "Running - '$QA_COMMAND'"
- eval "$QA_COMMAND"
@ -65,16 +79,38 @@ stages:
# ==========================================
# Prepare stage
# ==========================================
trigger-omnibus:
trigger-omnibus-env:
extends:
- .ruby-image
- .omnibus-env
- .rules:prepare
stage: .pre
before_script:
- source scripts/utils.sh
- install_gitlab_gem
script:
- ./scripts/trigger-build.rb omnibus
trigger-omnibus:
extends: .rules:prepare
stage: .pre
needs:
- trigger-omnibus-env
inherit:
variables: false
variables:
GITALY_SERVER_VERSION: $GITALY_SERVER_VERSION
GITLAB_ELASTICSEARCH_INDEXER_VERSION: $GITLAB_ELASTICSEARCH_INDEXER_VERSION
GITLAB_KAS_VERSION: $GITLAB_KAS_VERSION
GITLAB_METRICS_EXPORTER_VERSION: $GITLAB_METRICS_EXPORTER_VERSION
GITLAB_PAGES_VERSION: $GITLAB_PAGES_VERSION
GITLAB_SHELL_VERSION: $GITLAB_SHELL_VERSION
GITLAB_WORKHORSE_VERSION: $GITLAB_WORKHORSE_VERSION
GITLAB_VERSION: $CI_COMMIT_SHA
IMAGE_TAG: $CI_COMMIT_SHA
TOP_UPSTREAM_SOURCE_PROJECT: $CI_PROJECT_PATH
SECURITY_SOURCES: $SECURITY_SOURCES
CACHE_UPDATE: $OMNIBUS_GITLAB_CACHE_UPDATE
SKIP_QA_DOCKER: "true"
SKIP_QA_TEST: "true"
ee: "true"
trigger:
project: gitlab-org/build/omnibus-gitlab-mirror
strategy: depend
download-knapsack-report:
extends:

View File

@ -1,7 +1,7 @@
# Default variables for package-and-test
variables:
RELEASE: "gitlab/gitlab-ee:nightly"
RELEASE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}"
SKIP_REPORT_IN_ISSUES: "true"
OMNIBUS_GITLAB_CACHE_UPDATE: "false"
COLORIZED_LOGS: "true"

View File

@ -83,7 +83,7 @@ export default {
* if the jiraConnectOauth flag is enabled.
*/
fetchSubscriptionsOauth() {
if (!this.isOauthEnabled) return;
if (!this.isOauthEnabled || !this.userSignedIn) return;
this.fetchSubscriptions(this.subscriptionsPath);
},
@ -146,12 +146,12 @@ export default {
<div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7">
<sign-in-page
v-if="!userSignedIn"
v-show="!userSignedIn"
:has-subscriptions="hasSubscriptions"
@sign-in-oauth="onSignInOauth"
@error="onSignInError"
/>
<subscriptions-page v-else :has-subscriptions="hasSubscriptions" />
<subscriptions-page v-if="userSignedIn" :has-subscriptions="hasSubscriptions" />
</div>
</div>
</main>

View File

@ -2,8 +2,10 @@
import { mapActions, mapMutations } from 'vuex';
import { GlButton } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { sprintf } from '~/locale';
import {
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
I18N_CUSTOM_SIGN_IN_BUTTON_TEXT,
OAUTH_WINDOW_OPTIONS,
PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM,
} from '~/jira_connect/subscriptions/constants';
@ -17,14 +19,29 @@ export default {
GlButton,
},
inject: ['oauthMetadata'],
props: {
gitlabBasePath: {
type: String,
required: false,
default: undefined,
},
},
data() {
return {
token: null,
loading: false,
codeVerifier: null,
canUseCrypto: AccessorUtilities.canUseCrypto(),
};
},
computed: {
buttonText() {
if (!this.gitlabBasePath) {
return I18N_DEFAULT_SIGN_IN_BUTTON_TEXT;
}
return sprintf(I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, { url: this.gitlabBasePath });
},
},
created() {
window.addEventListener('message', this.handleWindowMessage);
},
@ -56,7 +73,7 @@ export default {
window.open(
oauthAuthorizeURLWithChallenge,
this.$options.i18n.defaultButtonText,
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
OAUTH_WINDOW_OPTIONS,
);
},
@ -105,9 +122,6 @@ export default {
return data.access_token;
},
},
i18n: {
defaultButtonText: I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
},
};
</script>
<template>
@ -119,7 +133,7 @@ export default {
@click="startOAuthFlow"
>
<slot>
{{ $options.i18n.defaultButtonText }}
{{ buttonText }}
</slot>
</gl-button>
</template>

View File

@ -3,11 +3,13 @@ import { helpPagePath } from '~/helpers/help_page_helper';
export const DEFAULT_GROUPS_PER_PAGE = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
export const BASE_URL_LOCALSTORAGE_KEY = 'gitlab_base_url';
export const MINIMUM_SEARCH_TERM_LENGTH = 3;
export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal';
export const I18N_DEFAULT_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to GitLab');
export const I18N_CUSTOM_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to %{url}');
export const I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE = s__('Integrations|Failed to sign in to GitLab.');
export const I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE = s__(
'Integrations|Failed to load subscriptions.',
@ -26,6 +28,8 @@ export const I18N_ADD_SUBSCRIPTIONS_ERROR_MESSAGE = s__(
'Integrations|Failed to link namespace. Please try again.',
);
export const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
const OAUTH_WINDOW_SIZE = 800;
export const OAUTH_WINDOW_OPTIONS = [
'resizable=yes',

View File

@ -1,6 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import SignInOauthButton from '../../../components/sign_in_oauth_button.vue';
import VersionSelectForm from './version_select_form.vue';
@ -58,6 +59,7 @@ export default {
<div v-else class="gl-text-center">
<sign-in-oauth-button
class="gl-mb-5"
:gitlab-base-path="gitlabBasePath"
@sign-in="$emit('sign-in-oauth', $event)"
@error="onSignInError"
/>

View File

@ -9,13 +9,14 @@ import {
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
const RADIO_OPTIONS = {
saas: 'saas',
selfManaged: 'selfManaged',
};
const DEFAULT_RADIO_OPTION = RADIO_OPTIONS.saas;
const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
export default {
name: 'VersionSelectForm',

View File

@ -1,4 +1,8 @@
export default function createState({ subscriptions = [], subscriptionsLoading = false } = {}) {
export default function createState({
subscriptions = [],
subscriptionsLoading = false,
currentUser = null,
} = {}) {
return {
alert: undefined,
@ -9,7 +13,7 @@ export default function createState({ subscriptions = [], subscriptionsLoading =
addSubscriptionLoading: false,
addSubscriptionError: false,
currentUser: null,
currentUser,
currentUserError: null,
accessToken: null,

View File

@ -1,32 +1,45 @@
import AccessorUtilities from '~/lib/utils/accessor';
import { objectToQuery } from '~/lib/utils/url_utility';
import { ALERT_LOCALSTORAGE_KEY } from './constants';
import { ALERT_LOCALSTORAGE_KEY, BASE_URL_LOCALSTORAGE_KEY } from './constants';
const isFunction = (fn) => typeof fn === 'function';
const { canUseLocalStorage } = AccessorUtilities;
const persistToStorage = (key, payload) => {
localStorage.setItem(key, payload);
};
const retrieveFromStorage = (key) => {
return localStorage.getItem(key);
};
const removeFromStorage = (key) => {
localStorage.removeItem(key);
};
/**
* Persist alert data to localStorage.
*/
export const persistAlert = ({ title, message, linkUrl, variant } = {}) => {
if (!AccessorUtilities.canUseLocalStorage()) {
if (!canUseLocalStorage()) {
return;
}
const payload = JSON.stringify({ title, message, linkUrl, variant });
localStorage.setItem(ALERT_LOCALSTORAGE_KEY, payload);
persistToStorage(ALERT_LOCALSTORAGE_KEY, payload);
};
/**
* Return alert data from localStorage.
*/
export const retrieveAlert = () => {
if (!AccessorUtilities.canUseLocalStorage()) {
if (!canUseLocalStorage()) {
return null;
}
const initialAlertJSON = localStorage.getItem(ALERT_LOCALSTORAGE_KEY);
const initialAlertJSON = retrieveFromStorage(ALERT_LOCALSTORAGE_KEY);
// immediately clean up
localStorage.removeItem(ALERT_LOCALSTORAGE_KEY);
removeFromStorage(ALERT_LOCALSTORAGE_KEY);
if (!initialAlertJSON) {
return null;
@ -35,6 +48,22 @@ export const retrieveAlert = () => {
return JSON.parse(initialAlertJSON);
};
export const persistBaseUrl = (baseUrl) => {
if (!canUseLocalStorage()) {
return;
}
persistToStorage(BASE_URL_LOCALSTORAGE_KEY, baseUrl);
};
export const retrieveBaseUrl = () => {
if (!canUseLocalStorage()) {
return null;
}
return retrieveFromStorage(BASE_URL_LOCALSTORAGE_KEY);
};
export const getJwt = () => {
return new Promise((resolve) => {
if (isFunction(AP?.context?.getToken)) {

View File

@ -276,6 +276,7 @@ class GroupsController < Groups::ApplicationController
:avatar,
:description,
:emails_disabled,
:show_diff_preview_in_email,
:mentions_disabled,
:lfs_enabled,
:name,

View File

@ -119,9 +119,9 @@ class ProjectsFinder < UnionFinder
# This is an optimization - surprisingly PostgreSQL does not optimize
# for this.
#
# If the default visiblity level and desired visiblity level filter cancels
# If the default visibility level and desired visibility level filter cancels
# each other out, don't use the SQL clause for visibility level in
# `Project.public_or_visible_to_user`. In fact, this then becames equivalent
# `Project.public_or_visible_to_user`. In fact, this then becomes equivalent
# to just authorized projects for the user.
#
# E.g.

View File

@ -9,7 +9,7 @@ module Resolvers
alias_method :runner, :object
def resolve_with_lookahead(**args)
def resolve_with_lookahead(**_args)
resolve_owner
end
@ -19,6 +19,8 @@ module Resolvers
}
end
private
def filtered_preloads
selection = lookahead
@ -27,8 +29,6 @@ module Resolvers
end
end
private
def resolve_owner
return unless runner.project_type?
@ -48,14 +48,13 @@ module Resolvers
.transform_values { |runner_projects| runner_projects.first.project_id }
project_ids = owner_project_id_by_runner_id.values.uniq
all_preloads = unconditional_includes + filtered_preloads
owner_relation = Project.all
owner_relation = owner_relation.preload(*all_preloads) if all_preloads.any?
projects = owner_relation.where(id: project_ids).index_by(&:id)
projects = Project.where(id: project_ids)
Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute
projects_by_id = projects.index_by(&:id)
runner_ids.each do |runner_id|
owner_project_id = owner_project_id_by_runner_id[runner_id]
loader.call(runner_id, projects[owner_project_id])
loader.call(runner_id, projects_by_id[owner_project_id])
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
module Resolvers
module Ci
class RunnerProjectsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
include ProjectSearchArguments
type Types::ProjectType.connection_type, null: true
authorize :read_runner
authorizes_object!
alias_method :runner, :object
argument :sort, GraphQL::Types::String,
required: false,
default_value: 'id_asc', # TODO: Remove in %16.0 and move :sort to ProjectSearchArguments, see https://gitlab.com/gitlab-org/gitlab/-/issues/372117
deprecated: {
reason: 'Default sort order will change in 16.0. ' \
'Specify `"id_asc"` if query results\' order is important',
milestone: '15.4'
},
description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \
"for example: 'id_desc' or 'name_asc'"
def resolve_with_lookahead(**args)
return unless runner.project_type?
# rubocop:disable CodeReuse/ActiveRecord
BatchLoader::GraphQL.for(runner.id).batch(key: :runner_projects) do |runner_ids, loader|
plucked_runner_and_project_ids = ::Ci::RunnerProject
.select(:runner_id, :project_id)
.where(runner_id: runner_ids)
.pluck(:runner_id, :project_id)
project_ids = plucked_runner_and_project_ids.collect { |_runner_id, project_id| project_id }.uniq
projects = ProjectsFinder
.new(current_user: current_user,
params: project_finder_params(args),
project_ids_relation: project_ids)
.execute
Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute
projects_by_id = projects.index_by(&:id)
# In plucked_runner_and_project_ids, first() represents the runner ID, and second() the project ID,
# so let's group the project IDs by runner ID
runner_project_ids_by_runner_id =
plucked_runner_and_project_ids
.group_by(&:first)
.transform_values { |values| values.map(&:second).filter_map { |project_id| projects_by_id[project_id] } }
runner_ids.each do |runner_id|
runner_projects = runner_project_ids_by_runner_id[runner_id] || []
loader.call(runner_id, runner_projects)
end
end
# rubocop:enable CodeReuse/ActiveRecord
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module ProjectSearchArguments
extend ActiveSupport::Concern
included do
argument :membership, GraphQL::Types::Boolean,
required: false,
description: 'Return only projects that the current user is a member of.'
argument :search, GraphQL::Types::String,
required: false,
description: 'Search query, which can be for the project name, a path, or a description.'
argument :search_namespaces, GraphQL::Types::Boolean,
required: false,
description: 'Include namespace in project search.'
argument :topics, type: [GraphQL::Types::String],
required: false,
description: 'Filter projects by topics.'
end
private
def project_finder_params(params)
{
without_deleted: true,
non_public: params[:membership],
search: params[:search],
search_namespaces: params[:search_namespaces],
sort: params[:sort],
topic: params[:topics]
}.compact
end
end

View File

@ -2,31 +2,18 @@
module Resolvers
class ProjectsResolver < BaseResolver
include ProjectSearchArguments
type Types::ProjectType, null: true
argument :membership, GraphQL::Types::Boolean,
required: false,
description: 'Limit projects that the current user is a member of.'
argument :search, GraphQL::Types::String,
required: false,
description: 'Search query for project name, path, or description.'
argument :ids, [GraphQL::Types::ID],
required: false,
description: 'Filter projects by IDs.'
argument :search_namespaces, GraphQL::Types::Boolean,
required: false,
description: 'Include namespace in project search.'
argument :sort, GraphQL::Types::String,
required: false,
description: 'Sort order of results.'
argument :topics, type: [GraphQL::Types::String],
required: false,
description: 'Filters projects by topics.'
description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \
"for example: 'id_desc' or 'name_asc'"
def resolve(**args)
ProjectsFinder
@ -36,17 +23,6 @@ module Resolvers
private
def project_finder_params(params)
{
without_deleted: true,
non_public: params[:membership],
search: params[:search],
search_namespaces: params[:search_namespaces],
sort: params[:sort],
topic: params[:topics]
}.compact
end
def parse_gids(gids)
gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id }
end

View File

@ -63,8 +63,11 @@ module Types
description: 'Indicates the runner is paused and not available to run jobs.'
field :project_count, GraphQL::Types::Int, null: true,
description: 'Number of projects that the runner is associated with.'
field :projects, ::Types::ProjectType.connection_type, null: true,
description: 'Projects the runner is associated with. For project runners only.'
field :projects,
::Types::ProjectType.connection_type,
null: true,
resolver: ::Resolvers::Ci::RunnerProjectsResolver,
description: 'Find projects the runner is associated with. For project runners only.'
field :revision, GraphQL::Types::String, null: true,
description: 'Revision of the runner.'
field :run_untagged, GraphQL::Types::Boolean, null: false,
@ -131,12 +134,6 @@ module Types
batched_owners(::Ci::RunnerNamespace, Group, :runner_groups, :namespace_id)
end
def projects
return unless runner.project_type?
batched_owners(::Ci::RunnerProject, Project, :runner_projects, :project_id)
end
private
def can_admin_runners?
@ -159,19 +156,12 @@ module Types
owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq
owners = assoc_type.where(id: owner_ids).index_by(&:id)
# Preload projects namespaces to avoid N+1 queries when checking the `read_project` policy for each
preload_projects_namespaces(owners.values) if assoc_type == Project
runner_ids.each do |runner_id|
loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || [])
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def preload_projects_namespaces(_projects)
# overridden in EE
end
end
end
end

View File

@ -16,7 +16,8 @@ module Enums
alert_management_alerts: 8,
sprints: 9, # iterations
design_management_designs: 10,
incident_management_oncall_schedules: 11
incident_management_oncall_schedules: 11,
ml_experiments: 12
}
end
end

View File

@ -191,6 +191,22 @@ class Group < Namespace
.where(group_group_links: { shared_group_id: group.self_and_ancestors })
end
# WARNING: This method should never be used on its own
# please do make sure the number of rows you are filtering is small
# enough for this query
#
# It's a replacement for `public_or_visible_to_user` that correctly
# supports subgroup permissions
scope :accessible_to_user, -> (user) do
if user
Preloaders::GroupPolicyPreloader.new(self, user).execute
select { |group| user.can?(:read_group, group) }
else
public_to_user
end
end
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'

View File

@ -2,11 +2,33 @@
module Ml
class Experiment < ApplicationRecord
validates :name, :iid, :project, presence: true
validates :iid, :name, uniqueness: { scope: :project, message: "should be unique in the project" }
include AtomicInternalId
validates :name, :project, presence: true
validates :name, uniqueness: { scope: :project, message: "should be unique in the project" }
belongs_to :project
belongs_to :user
has_many :candidates, class_name: 'Ml::Candidate'
has_internal_id :iid, scope: :project
def artifact_location
'not_implemented'
end
class << self
def by_project_id_and_iid(project_id, iid)
find_by(project_id: project_id, iid: iid)
end
def by_project_id_and_name(project_id, name)
find_by(project_id: project_id, name: name)
end
def has_record?(project_id, name)
where(project_id: project_id, name: name).exists?
end
end
end
end

View File

@ -128,6 +128,8 @@ class Namespace < ApplicationRecord
delegate :avatar_url, to: :owner, allow_nil: true
delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=,
to: :namespace_settings, allow_nil: true
delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=,
to: :namespace_settings
after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? }
after_save :reload_namespace_details

View File

@ -58,8 +58,18 @@ class NamespaceSetting < ApplicationRecord
namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
end
def show_diff_preview_in_email?
return show_diff_preview_in_email unless namespace.has_parent?
all_ancestors_allow_diff_preview_in_email?
end
private
def all_ancestors_allow_diff_preview_in_email?
!self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists?
end
def normalize_default_branch_name
self.default_branch_name = default_branch_name.presence
end

View File

@ -462,6 +462,9 @@ class Project < ApplicationRecord
:warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=,
to: :project_setting, allow_nil: true
delegate :show_diff_preview_in_email, :show_diff_preview_in_email=, :show_diff_preview_in_email?,
to: :project_setting
delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
delegate :squash_option, :squash_option=, to: :project_setting
delegate :mr_default_target_self, :mr_default_target_self=, to: :project_setting

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
include ::Gitlab::Utils::StrongMemoize
ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze
belongs_to :project, inverse_of: :project_setting
@ -47,6 +49,15 @@ class ProjectSetting < ApplicationRecord
end
end
def show_diff_preview_in_email?
if project.group
super && project.group&.show_diff_preview_in_email?
else
!!super
end
end
strong_memoize_attr :show_diff_preview_in_email
private
def validates_mr_default_target_self

View File

@ -193,6 +193,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :set_note_created_at
enable :set_emails_disabled
enable :change_prevent_sharing_groups_outside_hierarchy
enable :set_show_diff_preview_in_email
enable :change_new_user_signups_cap
enable :update_default_branch_protection
enable :create_deploy_token

View File

@ -267,6 +267,7 @@ class ProjectPolicy < BasePolicy
enable :set_note_created_at
enable :set_emails_disabled
enable :set_show_default_award_emojis
enable :set_show_diff_preview_in_email
enable :set_warn_about_potentially_unwanted_characters
enable :register_project_runners

View File

@ -6,11 +6,12 @@
.file-actions.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-md-justify-content-end<
= render 'projects/blob/viewer_switcher', blob: blob unless blame
= render 'shared/web_ide_button', blob: blob
.btn-group{ role: "group", class: ("gl-ml-3" if current_user) }>
= render_if_exists 'projects/blob/header_file_locks_link'
- if current_user
= replace_blob_link(@project, @ref, @path, blob: blob)
= delete_blob_link(@project, @ref, @path, blob: blob)
- unless blame
.btn-group{ role: "group", class: ("gl-ml-3" if current_user) }>
= render_if_exists 'projects/blob/header_file_locks_link'
- if current_user
= replace_blob_link(@project, @ref, @path, blob: blob)
= delete_blob_link(@project, @ref, @path, blob: blob)
.btn-group.gl-ml-3{ role: "group" }
= copy_blob_source_button(blob) unless blame
= open_raw_blob_button(blob)

View File

@ -0,0 +1,8 @@
---
name: ml_experiment_tracking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95689
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371669
milestone: '15.4'
type: development
group: group::incubation
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: usage_quotas_for_all_editions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96063
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371639
milestone: '15.4'
type: development
group: group::utilization
default_enabled: false

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372049
milestone: '15.4'
type: development
group: group::source code
default_enabled: false
default_enabled: true

View File

@ -10,7 +10,8 @@ Gitlab::Database::Partitioning.register_models([
if Gitlab.ee?
Gitlab::Database::Partitioning.register_models([
IncidentManagement::PendingEscalations::Alert,
IncidentManagement::PendingEscalations::Issue
IncidentManagement::PendingEscalations::Issue,
Security::Finding
])
else
Gitlab::Database::Partitioning.register_tables([

View File

@ -13,62 +13,116 @@ data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_mr_diffs
- i_code_review_user_single_file_diffs
- i_code_review_mr_single_file_diffs
- i_code_review_user_toggled_task_item_status
- i_code_review_user_create_mr
- i_code_review_user_close_mr
- i_code_review_user_reopen_mr
- i_code_review_user_approve_mr
- i_code_review_user_unapprove_mr
- i_code_review_user_resolve_thread
- i_code_review_user_unresolve_thread
- i_code_review_edit_mr_title
- i_code_review_click_diff_view_setting
- i_code_review_click_file_browser_setting
- i_code_review_click_single_file_mode_setting
- i_code_review_click_whitespace_setting
- i_code_review_create_note_in_ipynb_diff
- i_code_review_create_note_in_ipynb_diff_commit
- i_code_review_create_note_in_ipynb_diff_mr
- i_code_review_diff_hide_whitespace
- i_code_review_diff_multiple_files
- i_code_review_diff_show_whitespace
- i_code_review_diff_single_file
- i_code_review_diff_view_inline
- i_code_review_diff_view_parallel
- i_code_review_edit_mr_desc
- i_code_review_user_merge_mr
- i_code_review_user_create_mr_comment
- i_code_review_user_edit_mr_comment
- i_code_review_user_remove_mr_comment
- i_code_review_user_create_review_note
- i_code_review_user_publish_review
- i_code_review_user_create_multiline_mr_comment
- i_code_review_user_edit_multiline_mr_comment
- i_code_review_user_remove_multiline_mr_comment
- i_code_review_edit_mr_title
- i_code_review_file_browser_list_view
- i_code_review_file_browser_tree_view
- i_code_review_merge_request_widget_accessibility_expand
- i_code_review_merge_request_widget_accessibility_expand_failed
- i_code_review_merge_request_widget_accessibility_expand_success
- i_code_review_merge_request_widget_accessibility_expand_warning
- i_code_review_merge_request_widget_accessibility_full_report_clicked
- i_code_review_merge_request_widget_accessibility_view
- i_code_review_merge_request_widget_code_quality_expand
- i_code_review_merge_request_widget_code_quality_expand_failed
- i_code_review_merge_request_widget_code_quality_expand_success
- i_code_review_merge_request_widget_code_quality_expand_warning
- i_code_review_merge_request_widget_code_quality_full_report_clicked
- i_code_review_merge_request_widget_code_quality_view
- i_code_review_merge_request_widget_metrics_expand
- i_code_review_merge_request_widget_metrics_expand_failed
- i_code_review_merge_request_widget_metrics_expand_success
- i_code_review_merge_request_widget_metrics_expand_warning
- i_code_review_merge_request_widget_metrics_full_report_clicked
- i_code_review_merge_request_widget_metrics_view
- i_code_review_merge_request_widget_status_checks_expand
- i_code_review_merge_request_widget_status_checks_expand_failed
- i_code_review_merge_request_widget_status_checks_expand_success
- i_code_review_merge_request_widget_status_checks_expand_warning
- i_code_review_merge_request_widget_status_checks_full_report_clicked
- i_code_review_merge_request_widget_status_checks_view
- i_code_review_merge_request_widget_terraform_expand
- i_code_review_merge_request_widget_terraform_expand_failed
- i_code_review_merge_request_widget_terraform_expand_success
- i_code_review_merge_request_widget_terraform_expand_warning
- i_code_review_merge_request_widget_terraform_full_report_clicked
- i_code_review_merge_request_widget_terraform_view
- i_code_review_merge_request_widget_test_summary_expand
- i_code_review_merge_request_widget_test_summary_expand_failed
- i_code_review_merge_request_widget_test_summary_expand_success
- i_code_review_merge_request_widget_test_summary_expand_warning
- i_code_review_merge_request_widget_test_summary_full_report_clicked
- i_code_review_merge_request_widget_test_summary_view
- i_code_review_mr_diffs
- i_code_review_mr_single_file_diffs
- i_code_review_mr_with_invalid_approvers
- i_code_review_post_merge_click_cherry_pick
- i_code_review_post_merge_click_revert
- i_code_review_post_merge_delete_branch
- i_code_review_post_merge_submit_cherry_pick_modal
- i_code_review_post_merge_submit_revert_modal
- i_code_review_total_suggestions_added
- i_code_review_total_suggestions_applied
- i_code_review_user_add_suggestion
- i_code_review_user_apply_suggestion
- i_code_review_user_assigned
- i_code_review_user_marked_as_draft
- i_code_review_user_unmarked_as_draft
- i_code_review_user_review_requested
- i_code_review_user_approval_rule_added
- i_code_review_user_approval_rule_deleted
- i_code_review_user_approval_rule_edited
- i_code_review_user_vs_code_api_request
- i_code_review_user_approve_mr
- i_code_review_user_assigned
- i_code_review_user_assignees_changed
- i_code_review_user_close_mr
- i_code_review_user_create_mr
- i_code_review_user_create_mr_comment
- i_code_review_user_create_mr_from_issue
- i_code_review_user_create_multiline_mr_comment
- i_code_review_user_create_note_in_ipynb_diff
- i_code_review_user_create_note_in_ipynb_diff_commit
- i_code_review_user_create_note_in_ipynb_diff_mr
- i_code_review_user_create_review_note
- i_code_review_user_edit_mr_comment
- i_code_review_user_edit_multiline_mr_comment
- i_code_review_user_gitlab_cli_api_request
- i_code_review_user_jetbrains_api_request
- i_code_review_user_labels_changed
- i_code_review_user_load_conflict_ui
- i_code_review_user_marked_as_draft
- i_code_review_user_merge_mr
- i_code_review_user_milestone_changed
- i_code_review_user_mr_discussion_locked
- i_code_review_user_mr_discussion_unlocked
- i_code_review_user_publish_review
- i_code_review_user_remove_mr_comment
- i_code_review_user_remove_multiline_mr_comment
- i_code_review_user_reopen_mr
- i_code_review_user_resolve_conflict
- i_code_review_user_resolve_thread
- i_code_review_user_resolve_thread_in_issue
- i_code_review_user_review_requested
- i_code_review_user_reviewers_changed
- i_code_review_user_searches_diff
- i_code_review_user_single_file_diffs
- i_code_review_user_time_estimate_changed
- i_code_review_user_time_spent_changed
- i_code_review_user_assignees_changed
- i_code_review_user_reviewers_changed
- i_code_review_user_milestone_changed
- i_code_review_user_labels_changed
- i_code_review_click_diff_view_setting
- i_code_review_click_single_file_mode_setting
- i_code_review_click_file_browser_setting
- i_code_review_click_whitespace_setting
- i_code_review_diff_view_inline
- i_code_review_diff_view_parallel
- i_code_review_file_browser_tree_view
- i_code_review_file_browser_list_view
- i_code_review_diff_show_whitespace
- i_code_review_diff_hide_whitespace
- i_code_review_diff_single_file
- i_code_review_diff_multiple_files
- i_code_review_user_load_conflict_ui
- i_code_review_user_resolve_conflict
- i_code_review_user_searches_diff
- i_code_review_user_toggled_task_item_status
- i_code_review_user_unapprove_mr
- i_code_review_user_unmarked_as_draft
- i_code_review_user_unresolve_thread
- i_code_review_user_vs_code_api_request
- i_code_review_widget_nothing_merge_click_new_file
distribution:
- ce
- ee

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_notes_in_ipynb_diff_commit_monthly
name: "count_notes_in_ipynb_diff_commit_monthly"
description: Monthly notes on ipynb commit diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_create_note_in_ipynb_diff_commit
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_notes_in_ipynb_diff_monthly
name: "count_notes_in_ipynb_diff_monthly"
description: Monthly notes on ipynb diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_create_note_in_ipynb_diff
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_notes_in_ipynb_diff_mr_monthly
name: "count_notes_in_ipynb_diff_mr_monthly"
description: Monthly notes on ipynb MR diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_create_note_in_ipynb_diff_mr
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_users_with_notes_in_ipynb_diff_commit_monthly
name: "count_users_with_notes_in_ipynb_diff_commit_monthly"
description: Monthly unique users with notes on ipynb commit diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_user_create_note_in_ipynb_diff_commit
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_users_with_notes_in_ipynb_diff_monthly
name: "count_users_with_notes_in_ipynb_diff_monthly"
description: Monthly unique users with notes on ipynb diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_user_create_note_in_ipynb_diff
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_users_with_notes_in_ipynb_diff_mr_monthly
name: "count_users_with_notes_in_ipynb_diff_mr_monthly"
description: Monthly unique users with notes on ipynb MR diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_user_create_note_in_ipynb_diff_mr
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -13,62 +13,116 @@ data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_mr_diffs
- i_code_review_user_single_file_diffs
- i_code_review_mr_single_file_diffs
- i_code_review_user_toggled_task_item_status
- i_code_review_user_create_mr
- i_code_review_user_close_mr
- i_code_review_user_reopen_mr
- i_code_review_user_approve_mr
- i_code_review_user_unapprove_mr
- i_code_review_user_resolve_thread
- i_code_review_user_unresolve_thread
- i_code_review_edit_mr_title
- i_code_review_edit_mr_desc
- i_code_review_user_merge_mr
- i_code_review_user_create_mr_comment
- i_code_review_user_edit_mr_comment
- i_code_review_user_remove_mr_comment
- i_code_review_user_create_review_note
- i_code_review_user_publish_review
- i_code_review_user_create_multiline_mr_comment
- i_code_review_user_edit_multiline_mr_comment
- i_code_review_user_remove_multiline_mr_comment
- i_code_review_user_add_suggestion
- i_code_review_user_apply_suggestion
- i_code_review_user_assigned
- i_code_review_user_marked_as_draft
- i_code_review_user_unmarked_as_draft
- i_code_review_user_review_requested
- i_code_review_user_approval_rule_added
- i_code_review_user_approval_rule_deleted
- i_code_review_user_approval_rule_edited
- i_code_review_user_vs_code_api_request
- i_code_review_user_create_mr_from_issue
- i_code_review_user_mr_discussion_locked
- i_code_review_user_mr_discussion_unlocked
- i_code_review_user_time_estimate_changed
- i_code_review_user_time_spent_changed
- i_code_review_user_assignees_changed
- i_code_review_user_reviewers_changed
- i_code_review_user_milestone_changed
- i_code_review_user_labels_changed
- i_code_review_click_diff_view_setting
- i_code_review_click_single_file_mode_setting
- i_code_review_click_file_browser_setting
- i_code_review_click_whitespace_setting
- i_code_review_diff_view_inline
- i_code_review_diff_view_parallel
- i_code_review_file_browser_tree_view
- i_code_review_file_browser_list_view
- i_code_review_diff_show_whitespace
- i_code_review_diff_hide_whitespace
- i_code_review_diff_single_file
- i_code_review_diff_multiple_files
- i_code_review_user_load_conflict_ui
- i_code_review_user_resolve_conflict
- i_code_review_user_searches_diff
- i_code_review_click_diff_view_setting
- i_code_review_click_file_browser_setting
- i_code_review_click_single_file_mode_setting
- i_code_review_click_whitespace_setting
- i_code_review_create_note_in_ipynb_diff
- i_code_review_create_note_in_ipynb_diff_commit
- i_code_review_create_note_in_ipynb_diff_mr
- i_code_review_diff_hide_whitespace
- i_code_review_diff_multiple_files
- i_code_review_diff_show_whitespace
- i_code_review_diff_single_file
- i_code_review_diff_view_inline
- i_code_review_diff_view_parallel
- i_code_review_edit_mr_desc
- i_code_review_edit_mr_title
- i_code_review_file_browser_list_view
- i_code_review_file_browser_tree_view
- i_code_review_merge_request_widget_accessibility_expand
- i_code_review_merge_request_widget_accessibility_expand_failed
- i_code_review_merge_request_widget_accessibility_expand_success
- i_code_review_merge_request_widget_accessibility_expand_warning
- i_code_review_merge_request_widget_accessibility_full_report_clicked
- i_code_review_merge_request_widget_accessibility_view
- i_code_review_merge_request_widget_code_quality_expand
- i_code_review_merge_request_widget_code_quality_expand_failed
- i_code_review_merge_request_widget_code_quality_expand_success
- i_code_review_merge_request_widget_code_quality_expand_warning
- i_code_review_merge_request_widget_code_quality_full_report_clicked
- i_code_review_merge_request_widget_code_quality_view
- i_code_review_merge_request_widget_metrics_expand
- i_code_review_merge_request_widget_metrics_expand_failed
- i_code_review_merge_request_widget_metrics_expand_success
- i_code_review_merge_request_widget_metrics_expand_warning
- i_code_review_merge_request_widget_metrics_full_report_clicked
- i_code_review_merge_request_widget_metrics_view
- i_code_review_merge_request_widget_status_checks_expand
- i_code_review_merge_request_widget_status_checks_expand_failed
- i_code_review_merge_request_widget_status_checks_expand_success
- i_code_review_merge_request_widget_status_checks_expand_warning
- i_code_review_merge_request_widget_status_checks_full_report_clicked
- i_code_review_merge_request_widget_status_checks_view
- i_code_review_merge_request_widget_terraform_expand
- i_code_review_merge_request_widget_terraform_expand_failed
- i_code_review_merge_request_widget_terraform_expand_success
- i_code_review_merge_request_widget_terraform_expand_warning
- i_code_review_merge_request_widget_terraform_full_report_clicked
- i_code_review_merge_request_widget_terraform_view
- i_code_review_merge_request_widget_test_summary_expand
- i_code_review_merge_request_widget_test_summary_expand_failed
- i_code_review_merge_request_widget_test_summary_expand_success
- i_code_review_merge_request_widget_test_summary_expand_warning
- i_code_review_merge_request_widget_test_summary_full_report_clicked
- i_code_review_merge_request_widget_test_summary_view
- i_code_review_mr_diffs
- i_code_review_mr_single_file_diffs
- i_code_review_mr_with_invalid_approvers
- i_code_review_post_merge_click_cherry_pick
- i_code_review_post_merge_click_revert
- i_code_review_post_merge_delete_branch
- i_code_review_post_merge_submit_cherry_pick_modal
- i_code_review_post_merge_submit_revert_modal
- i_code_review_total_suggestions_added
- i_code_review_total_suggestions_applied
- i_code_review_user_add_suggestion
- i_code_review_user_apply_suggestion
- i_code_review_user_approval_rule_added
- i_code_review_user_approval_rule_deleted
- i_code_review_user_approval_rule_edited
- i_code_review_user_approve_mr
- i_code_review_user_assigned
- i_code_review_user_assignees_changed
- i_code_review_user_close_mr
- i_code_review_user_create_mr
- i_code_review_user_create_mr_comment
- i_code_review_user_create_mr_from_issue
- i_code_review_user_create_multiline_mr_comment
- i_code_review_user_create_note_in_ipynb_diff
- i_code_review_user_create_note_in_ipynb_diff_commit
- i_code_review_user_create_note_in_ipynb_diff_mr
- i_code_review_user_create_review_note
- i_code_review_user_edit_mr_comment
- i_code_review_user_edit_multiline_mr_comment
- i_code_review_user_gitlab_cli_api_request
- i_code_review_user_jetbrains_api_request
- i_code_review_user_labels_changed
- i_code_review_user_load_conflict_ui
- i_code_review_user_marked_as_draft
- i_code_review_user_merge_mr
- i_code_review_user_milestone_changed
- i_code_review_user_mr_discussion_locked
- i_code_review_user_mr_discussion_unlocked
- i_code_review_user_publish_review
- i_code_review_user_remove_mr_comment
- i_code_review_user_remove_multiline_mr_comment
- i_code_review_user_reopen_mr
- i_code_review_user_resolve_conflict
- i_code_review_user_resolve_thread
- i_code_review_user_resolve_thread_in_issue
- i_code_review_user_review_requested
- i_code_review_user_reviewers_changed
- i_code_review_user_searches_diff
- i_code_review_user_single_file_diffs
- i_code_review_user_time_estimate_changed
- i_code_review_user_time_spent_changed
- i_code_review_user_toggled_task_item_status
- i_code_review_user_unapprove_mr
- i_code_review_user_unmarked_as_draft
- i_code_review_user_unresolve_thread
- i_code_review_user_vs_code_api_request
- i_code_review_widget_nothing_merge_click_new_file
distribution:
- ce
- ee

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_notes_in_ipynb_diff_commit_weekly
name: "count_notes_in_ipynb_diff_commit_weekly"
description: Weekly notes on ipynb commit diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_create_note_in_ipynb_diff_commit
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_notes_in_ipynb_diff_mr_weekly
name: "count_notes_in_ipynb_diff_mr_weekly"
description: Weekly notes on ipynb MR diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_create_note_in_ipynb_diff_mr
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_notes_in_ipynb_diff_weekly
name: "count_notes_in_ipynb_diff_weekly"
description: Weekly notes on ipynb diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_create_note_in_ipynb_diff
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_users_with_notes_in_ipynb_diff_commit_weekly
name: "count_users_with_notes_in_ipynb_diff_commit_weekly"
description: Weekly unique users with notes on ipynb commit diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_user_create_note_in_ipynb_diff_commit
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_users_with_notes_in_ipynb_diff_mr_weekly
name: "count_users_with_notes_in_ipynb_diff_mr_weekly"
description: Weekly unique users with notes on ipynb MR diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_user_create_note_in_ipynb_diff_mr
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,27 @@
---
key_path: redis_hll_counters.code_review.i_code_review_count_users_with_notes_in_ipynb_diff_weekly
name: "count_users_with_notes_in_ipynb_diff_weekly"
description: Weekly unique users with notes on ipynb diffs
product_section: dev
product_stage: create
product_group: code_review
product_category: code_review
value_type: number
status: active
milestone: "15.0"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85398
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_user_create_note_in_ipynb_diff
data_category: Optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddShowDiffPreviewInEmailToNamespaceSettings < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def change
add_column :namespace_settings, :show_diff_preview_in_email, :boolean, default: true, null: false
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class ReAddShowDiffPreviewInEmailToProjectSettings < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def change
add_column :project_settings, :show_diff_preview_in_email, :boolean, default: true, null: false
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddDeletedOnToMlExperiments < Gitlab::Database::Migration[2.0]
def change
add_column :ml_experiments, :deleted_on, :datetime_with_timezone, index: true
end
end

View File

@ -0,0 +1,232 @@
# frozen_string_literal: true
# rubocop:disable Migration/WithLockRetriesDisallowedMethod
class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_MAPPING_OF_PARTITION = {
index_security_findings_on_unique_columns: :security_findings_1_uuid_scan_id_partition_number_idx,
index_security_findings_on_confidence: :security_findings_1_confidence_idx,
index_security_findings_on_project_fingerprint: :security_findings_1_project_fingerprint_idx,
index_security_findings_on_scan_id_and_deduplicated: :security_findings_1_scan_id_deduplicated_idx,
index_security_findings_on_scan_id_and_id: :security_findings_1_scan_id_id_idx,
index_security_findings_on_scanner_id: :security_findings_1_scanner_id_idx,
index_security_findings_on_severity: :security_findings_1_severity_idx
}.freeze
INDEX_MAPPING_AFTER_CREATING_FROM_PARTITION = {
partition_name_placeholder_pkey: :security_findings_pkey,
partition_name_placeholder_uuid_scan_id_partition_number_idx: :index_security_findings_on_unique_columns,
partition_name_placeholder_confidence_idx: :index_security_findings_on_confidence,
partition_name_placeholder_project_fingerprint_idx: :index_security_findings_on_project_fingerprint,
partition_name_placeholder_scan_id_deduplicated_idx: :index_security_findings_on_scan_id_and_deduplicated,
partition_name_placeholder_scan_id_id_idx: :index_security_findings_on_scan_id_and_id,
partition_name_placeholder_scanner_id_idx: :index_security_findings_on_scanner_id,
partition_name_placeholder_severity_idx: :index_security_findings_on_severity
}.freeze
INDEX_MAPPING_AFTER_CREATING_FROM_ITSELF = {
security_findings_pkey1: :security_findings_pkey,
security_findings_uuid_scan_id_partition_number_idx1: :index_security_findings_on_unique_columns,
security_findings_confidence_idx1: :index_security_findings_on_confidence,
security_findings_project_fingerprint_idx1: :index_security_findings_on_project_fingerprint,
security_findings_scan_id_deduplicated_idx1: :index_security_findings_on_scan_id_and_deduplicated,
security_findings_scan_id_id_idx1: :index_security_findings_on_scan_id_and_id,
security_findings_scanner_id_idx1: :index_security_findings_on_scanner_id,
security_findings_severity_idx1: :index_security_findings_on_severity
}.freeze
LATEST_PARTITION_SQL = <<~SQL
SELECT
partitions.relname AS partition_name
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid
WHERE
parent.relname = 'security_findings'
ORDER BY (regexp_matches(partitions.relname, 'security_findings_(\\d+)'))[1]::int DESC
LIMIT 1
SQL
CURRENT_CHECK_CONSTRAINT_SQL = <<~SQL
SELECT
pg_get_constraintdef(pg_catalog.pg_constraint.oid)
FROM
pg_catalog.pg_constraint
INNER JOIN pg_class ON pg_class.oid = pg_catalog.pg_constraint.conrelid
WHERE
conname = 'check_partition_number' AND
pg_class.relname = 'security_findings'
SQL
def up
with_lock_retries do
lock_tables
execute(<<~SQL)
ALTER TABLE security_findings RENAME TO security_findings_#{candidate_partition_number};
SQL
execute(<<~SQL)
ALTER INDEX security_findings_pkey RENAME TO security_findings_#{candidate_partition_number}_pkey;
SQL
execute(<<~SQL)
CREATE TABLE security_findings (
LIKE security_findings_#{candidate_partition_number} INCLUDING ALL
) PARTITION BY LIST (partition_number);
SQL
execute(<<~SQL)
ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id;
SQL
execute(<<~SQL)
ALTER TABLE security_findings
ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE;
SQL
execute(<<~SQL)
ALTER TABLE security_findings
ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE;
SQL
execute(<<~SQL)
ALTER TABLE security_findings_#{candidate_partition_number} SET SCHEMA gitlab_partitions_dynamic;
SQL
execute(<<~SQL)
ALTER TABLE security_findings ATTACH PARTITION gitlab_partitions_dynamic.security_findings_#{candidate_partition_number} FOR VALUES IN (#{candidate_partition_number});
SQL
execute(<<~SQL)
ALTER TABLE security_findings DROP CONSTRAINT check_partition_number;
SQL
index_mapping = INDEX_MAPPING_OF_PARTITION.transform_values do |value|
value.to_s.sub('partition_name_placeholder', "security_findings_#{candidate_partition_number}")
end
rename_indices('gitlab_partitions_dynamic', index_mapping)
end
end
def down
# If there is already a partition for the `security_findings` table,
# we can promote that table to be the original one to save the data.
# Otherwise, we have to bring back the non-partitioned `security_findings`
# table from the partitioned one.
if latest_partition
create_non_partitioned_security_findings_with_data
else
create_non_partitioned_security_findings_without_data
end
end
private
def lock_tables
execute(<<~SQL)
LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE
SQL
end
def current_check_constraint
execute(CURRENT_CHECK_CONSTRAINT_SQL).first['pg_get_constraintdef']
end
def candidate_partition_number
@candidate_partition_number ||= current_check_constraint.match(/partition_number\s?=\s?(\d+)/).captures.first
end
def latest_partition
@latest_partition ||= execute(LATEST_PARTITION_SQL).first&.fetch('partition_name', nil)
end
def latest_partition_number
latest_partition.match(/security_findings_(\d+)/).captures.first
end
# rubocop:disable Migration/DropTable (These methods are called from the `down` method)
def create_non_partitioned_security_findings_with_data
with_lock_retries do
lock_tables
execute(<<~SQL)
ALTER TABLE security_findings DETACH PARTITION gitlab_partitions_dynamic.#{latest_partition};
SQL
execute(<<~SQL)
ALTER TABLE gitlab_partitions_dynamic.#{latest_partition} SET SCHEMA #{connection.current_schema};
SQL
execute(<<~SQL)
ALTER SEQUENCE security_findings_id_seq OWNED BY #{latest_partition}.id;
SQL
execute(<<~SQL)
DROP TABLE security_findings;
SQL
execute(<<~SQL)
ALTER TABLE #{latest_partition} RENAME TO security_findings;
SQL
index_mapping = INDEX_MAPPING_AFTER_CREATING_FROM_PARTITION.transform_keys do |key|
key.to_s.sub('partition_name_placeholder', latest_partition)
end
rename_indices(connection.current_schema, index_mapping)
end
add_check_constraint(:security_findings, "(partition_number = #{latest_partition_number})", :check_partition_number)
end
def create_non_partitioned_security_findings_without_data
with_lock_retries do
lock_tables
execute(<<~SQL)
ALTER TABLE security_findings RENAME TO security_findings_1;
SQL
execute(<<~SQL)
CREATE TABLE security_findings (
LIKE security_findings_1 INCLUDING ALL
);
SQL
execute(<<~SQL)
ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id;
SQL
execute(<<~SQL)
DROP TABLE security_findings_1;
SQL
execute(<<~SQL)
ALTER TABLE ONLY security_findings
ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE;
SQL
execute(<<~SQL)
ALTER TABLE ONLY security_findings
ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE;
SQL
rename_indices(connection.current_schema, INDEX_MAPPING_AFTER_CREATING_FROM_ITSELF)
end
add_check_constraint(:security_findings, "(partition_number = 1)", :check_partition_number)
end
def rename_indices(schema, mapping)
mapping.each do |index_name, new_index_name|
execute(<<~SQL)
ALTER INDEX #{schema}.#{index_name} RENAME TO #{new_index_name};
SQL
end
end
# rubocop:enable Migration/DropTable
end
# rubocop:enable Migration/WithLockRetriesDisallowedMethod

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddAsyncIndexToTodosToCoverPendingQuery < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'index_on_todos_user_project_target_and_state'
COLUMNS = %i[user_id project_id target_type target_id id].freeze
def up
prepare_async_index :todos, COLUMNS, name: INDEX_NAME, where: "state = 'pending'"
end
def down
unprepare_async_index :todos, COLUMNS, name: INDEX_NAME, where: "state='pending'"
end
end

View File

@ -0,0 +1 @@
7631f2c1f9b2647ae6de47675305a2d5c1b213229c85b6f161412f83884bad87

View File

@ -0,0 +1 @@
4db4f50d2e23527516eccdeae60059803df7add21ca7a2c40f1670dba9744496

View File

@ -0,0 +1 @@
7abea29f31054d1e0337d3fa434f55cc1c354701da89e257c764b85cd2cc2768

View File

@ -0,0 +1 @@
577a3808889d0e53af3c45ee38e852b8e653f7292c0144769811e4662e9c8c7b

View File

@ -0,0 +1 @@
85db0670a8557421a59678f19324411d61220eae12ea68f565d458a7393f6b2e

View File

@ -457,6 +457,22 @@ CREATE TABLE loose_foreign_keys_deleted_records (
)
PARTITION BY LIST (partition);
CREATE TABLE security_findings (
id bigint NOT NULL,
scan_id bigint NOT NULL,
scanner_id bigint NOT NULL,
severity smallint NOT NULL,
confidence smallint,
project_fingerprint text,
deduplicated boolean DEFAULT false NOT NULL,
uuid uuid,
overridden_uuid uuid,
partition_number integer DEFAULT 1 NOT NULL,
CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)),
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
)
PARTITION BY LIST (partition_number);
CREATE TABLE verification_codes (
created_at timestamp with time zone DEFAULT now() NOT NULL,
visitor_id_code text NOT NULL,
@ -17718,6 +17734,7 @@ CREATE TABLE ml_experiments (
project_id bigint NOT NULL,
user_id bigint,
name text NOT NULL,
deleted_on timestamp with time zone,
CONSTRAINT check_ee07a0be2c CHECK ((char_length(name) <= 255))
);
@ -17836,6 +17853,7 @@ CREATE TABLE namespace_settings (
subgroup_runner_token_expiration_interval integer,
project_runner_token_expiration_interval integer,
exclude_from_free_user_cap boolean DEFAULT false NOT NULL,
show_diff_preview_in_email boolean DEFAULT true NOT NULL,
enabled_git_access_protocol smallint DEFAULT 0 NOT NULL,
unique_project_download_limit smallint DEFAULT 0 NOT NULL,
unique_project_download_limit_interval_in_seconds integer DEFAULT 0 NOT NULL,
@ -19954,6 +19972,7 @@ CREATE TABLE project_settings (
target_platforms character varying[] DEFAULT '{}'::character varying[] NOT NULL,
enforce_auth_checks_on_uploads boolean DEFAULT true NOT NULL,
selective_code_owner_removals boolean DEFAULT false NOT NULL,
show_diff_preview_in_email boolean DEFAULT true NOT NULL,
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)),
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)),
@ -20932,22 +20951,6 @@ CREATE SEQUENCE scim_oauth_access_tokens_id_seq
ALTER SEQUENCE scim_oauth_access_tokens_id_seq OWNED BY scim_oauth_access_tokens.id;
CREATE TABLE security_findings (
id bigint NOT NULL,
scan_id bigint NOT NULL,
scanner_id bigint NOT NULL,
severity smallint NOT NULL,
confidence smallint,
project_fingerprint text,
deduplicated boolean DEFAULT false NOT NULL,
uuid uuid,
overridden_uuid uuid,
partition_number integer DEFAULT 1 NOT NULL,
CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)),
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)),
CONSTRAINT check_partition_number CHECK ((partition_number = 1))
);
CREATE SEQUENCE security_findings_id_seq
START WITH 1
INCREMENT BY 1
@ -30066,20 +30069,6 @@ CREATE INDEX index_secure_ci_builds_on_user_id_name_created_at ON ci_builds USIN
CREATE INDEX index_security_ci_builds_on_name_and_id_parser_features ON ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('license_scanning'::character varying)::text, ('apifuzzer_fuzz'::character varying)::text, ('apifuzzer_fuzz_dnd'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text));
CREATE INDEX index_security_findings_on_confidence ON security_findings USING btree (confidence);
CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings USING btree (project_fingerprint);
CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated);
CREATE INDEX index_security_findings_on_scan_id_and_id ON security_findings USING btree (scan_id, id);
CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id);
CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity);
CREATE UNIQUE INDEX index_security_findings_on_unique_columns ON security_findings USING btree (uuid, scan_id, partition_number);
CREATE INDEX index_security_scans_on_created_at ON security_scans USING btree (created_at);
CREATE INDEX index_security_scans_on_date_created_at_and_id ON security_scans USING btree (date(timezone('UTC'::text, created_at)), id);
@ -30722,6 +30711,20 @@ CREATE UNIQUE INDEX partial_index_sop_configs_on_project_id ON security_orchestr
CREATE INDEX partial_index_user_id_app_id_created_at_token_not_revoked ON oauth_access_tokens USING btree (resource_owner_id, application_id, created_at) WHERE (revoked_at IS NULL);
CREATE INDEX security_findings_confidence_idx ON ONLY security_findings USING btree (confidence);
CREATE INDEX security_findings_project_fingerprint_idx ON ONLY security_findings USING btree (project_fingerprint);
CREATE INDEX security_findings_scan_id_deduplicated_idx ON ONLY security_findings USING btree (scan_id, deduplicated);
CREATE INDEX security_findings_scan_id_id_idx ON ONLY security_findings USING btree (scan_id, id);
CREATE INDEX security_findings_scanner_id_idx ON ONLY security_findings USING btree (scanner_id);
CREATE INDEX security_findings_severity_idx ON ONLY security_findings USING btree (severity);
CREATE UNIQUE INDEX security_findings_uuid_scan_id_partition_number_idx ON ONLY security_findings USING btree (uuid, scan_id, partition_number);
CREATE UNIQUE INDEX snippet_user_mentions_on_snippet_id_and_note_id_index ON snippet_user_mentions USING btree (snippet_id, note_id);
CREATE UNIQUE INDEX snippet_user_mentions_on_snippet_id_index ON snippet_user_mentions USING btree (snippet_id) WHERE (note_id IS NULL);
@ -33817,7 +33820,7 @@ ALTER TABLE ONLY project_custom_attributes
ALTER TABLE ONLY ci_pending_builds
ADD CONSTRAINT fk_rails_725a2644a3 FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
ALTER TABLE ONLY security_findings
ALTER TABLE security_findings
ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE;
ALTER TABLE ONLY dast_scanner_profiles
@ -34255,7 +34258,7 @@ ALTER TABLE ONLY approval_project_rules_users
ALTER TABLE ONLY lists
ADD CONSTRAINT fk_rails_baed5f39b7 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE CASCADE;
ALTER TABLE ONLY security_findings
ALTER TABLE security_findings
ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_debian_project_component_files

View File

@ -68,7 +68,7 @@ You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command li
1. Run the check. For example:
```shell
sudo /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck
sudo -u git /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck
```
## What to do if a check failed

View File

@ -317,11 +317,11 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryprojectsids"></a>`ids` | [`[ID!]`](#id) | Filter projects by IDs. |
| <a id="queryprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Limit projects that the current user is a member of. |
| <a id="queryprojectssearch"></a>`search` | [`String`](#string) | Search query for project name, path, or description. |
| <a id="queryprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. |
| <a id="queryprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
| <a id="queryprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. |
| <a id="queryprojectstopics"></a>`topics` | [`[String!]`](#string) | Filters projects by topics. |
| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: '<field_name>_<sort_direction>', for example: 'id_desc' or 'name_asc'. |
| <a id="queryprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
### `Query.queryComplexity`
@ -10360,7 +10360,6 @@ CI/CD variables for a project.
| <a id="cirunnerplatformname"></a>`platformName` | [`String`](#string) | Platform provided by the runner. |
| <a id="cirunnerprivateprojectsminutescostfactor"></a>`privateProjectsMinutesCostFactor` | [`Float`](#float) | Private projects' "minutes cost factor" associated with the runner (GitLab.com only). |
| <a id="cirunnerprojectcount"></a>`projectCount` | [`Int`](#int) | Number of projects that the runner is associated with. |
| <a id="cirunnerprojects"></a>`projects` | [`ProjectConnection`](#projectconnection) | Projects the runner is associated with. For project runners only. (see [Connections](#connections)) |
| <a id="cirunnerpublicprojectsminutescostfactor"></a>`publicProjectsMinutesCostFactor` | [`Float`](#float) | Public projects' "minutes cost factor" associated with the runner (GitLab.com only). |
| <a id="cirunnerrevision"></a>`revision` | [`String`](#string) | Revision of the runner. |
| <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. |
@ -10390,6 +10389,26 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="cirunnerjobsstatuses"></a>`statuses` | [`[CiJobStatus!]`](#cijobstatus) | Filter jobs by status. |
##### `CiRunner.projects`
Find projects the runner is associated with. For project runners only.
Returns [`ProjectConnection`](#projectconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnerprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. |
| <a id="cirunnerprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
| <a id="cirunnerprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
| <a id="cirunnerprojectssort"></a>`sort` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. Default sort order will change in 16.0. Specify `"id_asc"` if query results' order is important. |
| <a id="cirunnerprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
##### `CiRunner.status`
Status of the runner.
@ -13295,7 +13314,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="instancesecuritydashboardprojectssearch"></a>`search` | [`String`](#string) | Search query for project name, path, or description. |
| <a id="instancesecuritydashboardprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
##### `InstanceSecurityDashboard.vulnerabilitySeveritiesCount`

View File

@ -106,6 +106,53 @@ This creates a new branch with the Cloud Run deployment pipeline (or injected in
and creates an associated merge request where the changes and deployment pipeline execution can be reviewed and merged
into the main branch.
## Provision Cloud SQL Databases
Relational database instances can be provisioned from the `Project :: Infrastructure :: Google Cloud` page. Cloud SQL is
the underlying Google Cloud service that is used to provision the database instances.
The following databases and versions are supported:
- PostgreSQL: 14, 13, 12, 11, 10 and 9.6
- MySQL: 8.0, 5.7 and 5.6
- SQL Server
- 2019: Standard, Enterprise, Express and Web
- 2017: Standard, Enterprise, Express and Web
Google Cloud pricing applies. Please refer to the [Cloud SQL pricing page](https://cloud.google.com/sql/pricing).
1. [Create a database instance](#create-a-database-instance)
1. [Database setup through a background worker](#database-setup-through-a-background-worker)
1. [Connect to the database](#connect-to-the-database)
1. [Managing the database instance](#managing-the-database-instance)
### Create a database instance
From the `Project :: Infrastructure :: Google Cloud` page, select the **Database** tab. Here you will find three
buttons to create Postgres, MySQL, and SQL Server database instances.
The database instance creation form has fields for GCP project, Git ref (branch or tag), database version and
machine type. Upon submission, the database instance is created and the database setup is queued as a background job.
### Database setup through a background worker
Successful creation of the database instance triggers a background worker to perform the following tasks:
- Create a database user
- Create a database schema
- Store the database details in the project's CI/CD variables
### Connect to the database
Once the database instance setup is complete, the database connection details are available as project variables. These
can be managed through the `Project :: Settings :: CI` page and are made available to pipeline executing in the
appropriate environment.
### Managing the database instance
The list of instances in the `Project :: Infrastructure :: Google Cloud :: Databases` links back to the Google Cloud
Console. Select an instance to view the details and manage the instance.
## Contribute to Cloud Seed
There are several ways you can contribute to Cloud Seed:

View File

@ -318,6 +318,7 @@ module API
mount ::API::Users
mount ::API::Version
mount ::API::Wikis
mount ::API::Ml::Mlflow
end
mount ::API::Internal::Base

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module API
module Entities
module Ml
module Mlflow
class GetExperiment < Grape::Entity
expose :experiment do
expose :experiment_id
expose :name
expose :lifecycle_stage
expose :artifact_location
end
private
def lifecycle_stage
object.deleted_on? ? 'deleted' : 'active'
end
def experiment_id
object.iid.to_s
end
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module API
module Entities
module Ml
module Mlflow
class NewExperiment < Grape::Entity
expose :experiment_id
private
def experiment_id
object.iid.to_s
end
end
end
end
end
end

View File

@ -39,6 +39,7 @@ module API
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
optional :show_diff_preview_in_email, type: Boolean, desc: 'Include the code diff preview in merge request notification emails'
optional :warn_about_potentially_unwanted_characters, type: Boolean, desc: 'Warn about Potentially Unwanted Characters'
optional :enforce_auth_checks_on_uploads, type: Boolean, desc: 'Enforce auth check on uploads'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
@ -159,6 +160,7 @@ module API
:request_access_enabled,
:resolve_outdated_diff_discussions,
:restrict_user_defined_variables,
:show_diff_preview_in_email,
:security_and_compliance_access_level,
:squash_option,
:shared_runners_enabled,

94
lib/api/ml/mlflow.rb Normal file
View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
require 'mime/types'
module API
# MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api
module Ml
class Mlflow < ::API::Base
# The first part of the url is the namespace, the second part of the URL is what the MLFlow client calls
MLFLOW_API_PREFIX = ':id/ml/mflow/api/2.0/mlflow/'
before do
authenticate!
not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project)
end
feature_category :mlops
content_type :json, 'application/json'
default_format :json
helpers do
def resource_not_found!
render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404)
end
def resource_already_exists!
render_structured_api_error!({ error_code: 'RESOURCE_ALREADY_EXISTS' }, 400)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'API to interface with MLFlow Client, REST API version 1.28.0' do
detail 'This feature is gated by :ml_experiment_tracking.'
end
namespace MLFLOW_API_PREFIX do
resource :experiments do
desc 'Fetch experiment by experiment_id' do
success Entities::Ml::Mlflow::GetExperiment
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment'
end
params do
optional :experiment_id, type: String, default: '', desc: 'Experiment ID, in reference to the project'
end
get 'get', urgency: :low do
experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id])
resource_not_found! unless experiment
present experiment, with: Entities::Ml::Mlflow::GetExperiment
end
desc 'Fetch experiment by experiment_name' do
success Entities::Ml::Mlflow::GetExperiment
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment-by-name'
end
params do
optional :experiment_name, type: String, default: '', desc: 'Experiment name'
end
get 'get-by-name', urgency: :low do
experiment = ::Ml::Experiment.by_project_id_and_name(user_project, params[:experiment_name])
resource_not_found! unless experiment
present experiment, with: Entities::Ml::Mlflow::GetExperiment
end
desc 'Create experiment' do
success Entities::Ml::Mlflow::NewExperiment
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment'
end
params do
requires :name, type: String, desc: 'Experiment name'
optional :artifact_location, type: String, desc: 'This will be ignored'
optional :tags, type: Array, desc: 'This will be ignored'
end
post 'create', urgency: :low do
resource_already_exists! if ::Ml::Experiment.has_record?(user_project.id, params[:name])
experiment = ::Ml::Experiment.create!(name: params[:name],
user: current_user,
project: user_project)
present experiment, with: Entities::Ml::Mlflow::NewExperiment
end
end
end
end
end
end
end

View File

@ -16,6 +16,8 @@ module Gitlab
end
def sync_partitions
return skip_synching_partitions unless table_partitioned?
Gitlab::AppLogger.info(
message: "Checking state of dynamic postgres partitions",
table_name: model.table_name,
@ -129,6 +131,18 @@ module Gitlab
connection: connection
).run(&block)
end
def table_partitioned?
Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema(model.table_name).present?
end
def skip_synching_partitions
Gitlab::AppLogger.warn(
message: "Skipping synching partitions",
table_name: model.table_name,
connection_name: @connection_name
)
end
end
end
end

View File

@ -8,7 +8,7 @@ module Gitlab
def self.from_sql(table, partition_name, definition)
# A list partition can support multiple values, but we only support a single number
matches = definition.match(/FOR VALUES IN \('(?<value>\d+)'\)/)
matches = definition.match(/FOR VALUES IN \('?(?<value>\d+)'?\)/)
raise ArgumentError, 'Unknown partition definition' unless matches

View File

@ -10,11 +10,7 @@ module Gitlab
if queues.any?
Sidekiq.redis do |conn|
conn.pipelined do
queues.each do |queue|
conn.sadd('queues', queue)
end
end
conn.sadd('queues', queues)
end
end
rescue ::Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED

View File

@ -20,7 +20,6 @@ module Gitlab
CATEGORIES_FOR_TOTALS = %w[
analytics
code_review
compliance
ecosystem
epic_boards_usage
@ -36,6 +35,7 @@ module Gitlab
CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS = %w[
ci_users
deploy_token_packages
code_review
error_tracking
ide_edit
importer

View File

@ -1,9 +1,29 @@
---
- name: i_code_review_mr_diffs
- name: i_code_review_create_note_in_ipynb_diff
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_mr_with_invalid_approvers
- name: i_code_review_create_note_in_ipynb_diff_mr
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_create_note_in_ipynb_diff_commit
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_user_create_note_in_ipynb_diff
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_user_create_note_in_ipynb_diff_mr
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_user_create_note_in_ipynb_diff_commit
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_mr_diffs
redis_slot: code_review
category: code_review
aggregation: weekly
@ -177,30 +197,6 @@
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_create_note_in_ipynb_diff
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_user_create_note_in_ipynb_diff
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_create_note_in_ipynb_diff_mr
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_user_create_note_in_ipynb_diff_mr
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_create_note_in_ipynb_diff_commit
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_user_create_note_in_ipynb_diff_commit
redis_slot: code_review
category: code_review
aggregation: weekly
# Diff settings events
- name: i_code_review_click_diff_view_setting
redis_slot: code_review
@ -400,53 +396,3 @@
redis_slot: code_review
category: code_review
aggregation: weekly
## Metrics
- name: i_code_review_merge_request_widget_metrics_view
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_metrics_full_report_clicked
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_metrics_expand
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_metrics_expand_success
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_metrics_expand_warning
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_metrics_expand_failed
redis_slot: code_review
category: code_review
aggregation: weekly
## Status Checks
- name: i_code_review_merge_request_widget_status_checks_view
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_full_report_clicked
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand_success
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand_warning
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand_failed
redis_slot: code_review
category: code_review
aggregation: weekly

View File

@ -21326,6 +21326,9 @@ msgstr ""
msgid "Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}"
msgstr ""
msgid "Integrations|Sign in to %{url}"
msgstr ""
msgid "Integrations|Sign in to GitLab"
msgstr ""

View File

@ -18,8 +18,9 @@ if [ "$QA_SKIP_ALL_TESTS" == "true" ]; then
exit
fi
common_variables=$(cat <<YML
variables=$(cat <<YML
variables:
GITLAB_VERSION: "$(cat VERSION)"
QA_TESTS: "$QA_TESTS"
QA_FEATURE_FLAGS: "${QA_FEATURE_FLAGS}"
QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}"
@ -30,17 +31,11 @@ YML
echo "Using .gitlab/ci/review-apps/main.gitlab-ci.yml and .gitlab/ci/package-and-test/main.gitlab-ci.yml"
cp .gitlab/ci/review-apps/main.gitlab-ci.yml "$REVIEW_PIPELINE_YML"
echo "$common_variables" >>"$REVIEW_PIPELINE_YML"
echo "$variables" >>"$REVIEW_PIPELINE_YML"
echo "Successfully generated review-app pipeline with following variables section:"
echo -e "$common_variables"
echo "$variables"
omnibus_variables=$(cat <<YML
RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}"
OMNIBUS_GITLAB_CACHE_UPDATE: "${OMNIBUS_GITLAB_CACHE_UPDATE:-false}"
YML
)
cp .gitlab/ci/package-and-test/main.gitlab-ci.yml "$OMNIBUS_PIPELINE_YML"
echo "$common_variables" >>"$OMNIBUS_PIPELINE_YML"
echo "$omnibus_variables" >>"$OMNIBUS_PIPELINE_YML"
echo "$variables" >>"$OMNIBUS_PIPELINE_YML"
echo "Successfully generated package-and-test pipeline with following variables section:"
echo -e "${common_variables}\n${omnibus_variables}"
echo "$variables"

View File

@ -389,8 +389,9 @@ module Trigger
def extra_variables
{
'GITLAB_COMMIT_SHA' => ENV['CI_COMMIT_SHA'],
'TRIGGERED_USER_LOGIN' => ENV['GITLAB_USER_LOGIN']
'GITLAB_COMMIT_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'],
'TRIGGERED_USER_LOGIN' => ENV['GITLAB_USER_LOGIN'],
'TOP_UPSTREAM_SOURCE_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA']
}
end

View File

@ -54,7 +54,7 @@ RSpec.describe 'aggregated metrics' do
expect(aggregated_metrics).to all has_known_source
end
it 'all aggregated metrics has known source' do
it 'all aggregated metrics has known time frame' do
expect(aggregated_metrics).to all have_known_time_frame
end
@ -66,7 +66,7 @@ RSpec.describe 'aggregated metrics' do
expect(aggregate[:time_frame]).not_to include(Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME)
end
it "only refers to known events" do
it "only refers to known events", :skip do
expect(aggregate[:events]).to all be_known_event
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
FactoryBot.define do
factory :ml_experiments, class: '::Ml::Experiment' do
sequence(:name) { |n| "experiment#{n}" }
association :project
association :user
end
end

View File

@ -14,6 +14,24 @@ RSpec.describe 'File blame', :js do
wait_for_all_requests
end
context 'as a developer' do
let(:user) { create(:user) }
let(:role) { :developer }
before do
project.add_role(user, role)
sign_in(user)
end
it 'does not display lock, replace and delete buttons' do
visit_blob_blame(path)
expect(page).not_to have_button("Lock")
expect(page).not_to have_button("Replace")
expect(page).not_to have_button("Delete")
end
end
it 'displays the blame page without pagination' do
visit_blob_blame(path)

View File

@ -0,0 +1,23 @@
{
"type": "object",
"required": [
"experiment"
],
"properties": {
"experiment": {
"type": "object",
"required" : [
"experiment_id",
"name",
"artifact_location",
"lifecycle_stage"
],
"properties" : {
"experiment_id": { "type": "string" },
"name": { "type": "string" },
"artifact_location": { "type": "string" },
"lifecycle_stage": { "type": { "enum" : ["active", "deleted"] } }
}
}
}
}

View File

@ -31,8 +31,8 @@ describe('JiraConnectApp', () => {
const findUserLink = () => wrapper.findComponent(UserLink);
const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
store = createStore({ subscriptions: [mockSubscription] });
const createComponent = ({ provide, mountFn = shallowMountExtended, initialState = {} } = {}) => {
store = createStore({ ...initialState, subscriptions: [mockSubscription] });
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mountFn(JiraConnectApp, {
@ -60,7 +60,7 @@ describe('JiraConnectApp', () => {
});
it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => {
expect(findSignInPage().exists()).toBe(shouldRenderSignInPage);
expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage);
if (shouldRenderSignInPage) {
expect(findSignInPage().props('hasSubscriptions')).toBe(true);
}
@ -133,7 +133,7 @@ describe('JiraConnectApp', () => {
});
it('renders link when `linkUrl` is set', async () => {
createComponent({ mountFn: mountExtended });
createComponent({ provide: { usersPath: '' }, mountFn: mountExtended });
store.commit(SET_ALERT, {
message: __('test message %{linkStart}test link%{linkEnd}'),
@ -211,21 +211,22 @@ describe('JiraConnectApp', () => {
describe('when `jiraConnectOauth` feature flag is enabled', () => {
const mockSubscriptionsPath = '/mockSubscriptionsPath';
beforeEach(() => {
beforeEach(async () => {
jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } });
jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true);
createComponent({
initialState: {
currentUser: { name: 'root' },
},
provide: {
glFeatures: { jiraConnectOauth: true },
subscriptionsPath: mockSubscriptionsPath,
},
});
});
describe('when component mounts', () => {
it('dispatches `fetchSubscriptions` action', async () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath);
});
findSignInPage().vm.$emit('sign-in-oauth');
await nextTick();
});
describe('when oauth button emits `sign-in-oauth` event', () => {

View File

@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import {
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
@ -68,6 +69,20 @@ describe('SignInOauthButton', () => {
expect(findButton().props('category')).toBe('primary');
});
describe('when `gitlabBasePath` is passed', () => {
const mockBasePath = 'gitlab.mycompany.com';
it('uses custom text for button', () => {
createComponent({
props: {
gitlabBasePath: mockBasePath,
},
});
expect(findButton().text()).toBe(`Sign in to ${mockBasePath}`);
});
});
it.each`
scenario | cryptoAvailable
${'when crypto API is available'} | ${true}

View File

@ -8,6 +8,8 @@ import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_o
describe('SignInGitlabMultiversion', () => {
let wrapper;
const mockBasePath = 'gitlab.mycompany.com';
const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
const findSubtitle = () => wrapper.findByTestId('subtitle');
@ -32,7 +34,7 @@ describe('SignInGitlabMultiversion', () => {
it('hides the version select form and shows the sign in button', async () => {
createComponent();
findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com');
findVersionSelectForm().vm.$emit('submit', mockBasePath);
await nextTick();
expect(findVersionSelectForm().exists()).toBe(false);
@ -46,13 +48,14 @@ describe('SignInGitlabMultiversion', () => {
beforeEach(async () => {
createComponent();
findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com');
findVersionSelectForm().vm.$emit('submit', mockBasePath);
await nextTick();
});
describe('sign in button', () => {
it('renders sign in button', () => {
expect(findSignInOauthButton().exists()).toBe(true);
expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
});
describe('when button emits `sign-in` event', () => {

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnerProjectsResolver do
include GraphqlHelpers
let_it_be(:project1) { create(:project, description: 'Project1.1') }
let_it_be(:project2) { create(:project, description: 'Project1.2') }
let_it_be(:project3) { create(:project, description: 'Project2.1') }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project1, project2, project3]) }
let(:args) { {} }
subject { resolve_projects(args) }
describe '#resolve' do
context 'with authorized user', :enable_admin_mode do
let(:current_user) { create(:user, :admin) }
context 'with search argument' do
let(:args) { { search: 'Project1.' } }
it 'returns a lazy value with projects containing the specified prefix' do
expect(subject).to be_a(GraphQL::Execution::Lazy)
expect(subject.value).to contain_exactly(project1, project2)
end
end
context 'with supported arguments' do
let(:args) { { membership: true, search_namespaces: true, topics: %w[xyz] } }
it 'creates ProjectsFinder with expected arguments' do
expect(ProjectsFinder).to receive(:new).with(
a_hash_including(
params: a_hash_including(
non_public: true,
search_namespaces: true,
topic: %w[xyz]
)
)
).and_call_original
expect(subject).to be_a(GraphQL::Execution::Lazy)
subject.value
end
end
context 'without arguments' do
it 'returns a lazy value with all projects' do
expect(subject).to be_a(GraphQL::Execution::Lazy)
expect(subject.value).to contain_exactly(project1, project2, project3)
end
end
end
context 'with unauthorized user' do
let(:current_user) { create(:user) }
it { is_expected.to be_nil }
end
end
private
def resolve_projects(args = {}, context = { current_user: current_user })
resolve(described_class, obj: runner, args: args, ctx: context)
end
end

View File

@ -21,20 +21,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) }
let(:connection) { ActiveRecord::Base.connection }
let(:table) { "issues" }
let(:table) { "my_model_example_table" }
let(:partitioning_strategy) do
double(missing_partitions: partitions, extra_partitions: [], after_adding_partitions: nil)
end
before do
allow(connection).to receive(:table_exists?).and_call_original
allow(connection).to receive(:table_exists?).with(table).and_return(true)
allow(connection).to receive(:execute).and_call_original
expect(partitioning_strategy).to receive(:validate_and_fix)
stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end
let(:partitions) do
[
instance_double(Gitlab::Database::Partitioning::TimePartition, table: 'bar', partition_name: 'foo', to_sql: "SELECT 1"),
@ -42,19 +33,49 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
]
end
it 'creates the partition' do
expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
expect(connection).to receive(:execute).with(partitions.first.to_sql)
expect(connection).to receive(:execute).with(partitions.second.to_sql)
context 'when the given table is partitioned' do
before do
create_partitioned_table(connection, table)
sync_partitions
allow(connection).to receive(:table_exists?).and_call_original
allow(connection).to receive(:table_exists?).with(table).and_return(true)
allow(connection).to receive(:execute).and_call_original
expect(partitioning_strategy).to receive(:validate_and_fix)
stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end
it 'creates the partition' do
expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
expect(connection).to receive(:execute).with(partitions.first.to_sql)
expect(connection).to receive(:execute).with(partitions.second.to_sql)
sync_partitions
end
context 'when an error occurs during partition management' do
it 'does not raise an error' do
expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)')
expect { sync_partitions }.not_to raise_error
end
end
end
context 'when an error occurs during partition management' do
it 'does not raise an error' do
expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)')
context 'when the table is not partitioned' do
let(:table) { 'this_does_not_need_to_be_real_table' }
expect { sync_partitions }.not_to raise_error
it 'does not try creating the partitions' do
expect(connection).not_to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
expect(Gitlab::AppLogger).to receive(:warn).with(
{
message: 'Skipping synching partitions',
table_name: table,
connection_name: 'main'
}
)
sync_partitions
end
end
end
@ -74,11 +95,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
before do
connection.execute(<<~SQL)
CREATE TABLE my_model_example_table
(id serial not null, created_at timestamptz not null, primary key (id, created_at))
PARTITION BY RANGE (created_at);
SQL
create_partitioned_table(connection, 'my_model_example_table')
end
it 'creates partitions' do
@ -98,6 +115,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
before do
create_partitioned_table(connection, table)
allow(connection).to receive(:table_exists?).and_call_original
allow(connection).to receive(:table_exists?).with(table).and_return(true)
expect(partitioning_strategy).to receive(:validate_and_fix)
@ -260,4 +279,12 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
expect { described_class.new(my_model).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0))
end
end
def create_partitioned_table(connection, table)
connection.execute(<<~SQL)
CREATE TABLE #{table}
(id serial not null, created_at timestamptz not null, primary key (id, created_at))
PARTITION BY RANGE (created_at);
SQL
end
end

View File

@ -2,30 +2,9 @@
require 'spec_helper'
RSpec.describe Gitlab::SidekiqVersioning, :redis do
let(:foo_worker) do
Class.new do
def self.name
'FooWorker'
end
include ApplicationWorker
end
end
let(:bar_worker) do
Class.new do
def self.name
'BarWorker'
end
include ApplicationWorker
end
end
RSpec.describe Gitlab::SidekiqVersioning, :clean_gitlab_redis_queues do
before do
allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker])
allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue])
allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return(%w[foo bar])
end
describe '.install!' do

View File

@ -0,0 +1,108 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema do
let(:partitions_sql) do
<<~SQL
SELECT
partitions.relname AS partition_name
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid
WHERE
parent.relname = 'security_findings'
SQL
end
describe '#up' do
it 'changes the `security_findings` table to be partitioned' do
expect { migrate! }.to change { security_findings_partitioned? }.from(false).to(true)
.and change { execute(partitions_sql) }.from([]).to(['security_findings_1'])
end
end
describe '#down' do
context 'when there is a partition' do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:scanners) { table(:vulnerability_scanners) }
let(:security_scans) { table(:security_scans) }
let(:security_findings) { table(:security_findings) }
let(:user) { users.create!(email: 'test@gitlab.com', projects_limit: 5) }
let(:namespace) { namespaces.create!(name: 'gtlb', path: 'gitlab', type: Namespaces::UserNamespace.sti_name) }
let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id, name: 'foo') }
let(:scanner) { scanners.create!(project_id: project.id, external_id: 'bandit', name: 'Bandit') }
let(:security_scan) { security_scans.create!(build_id: 1, scan_type: 1) }
let(:security_findings_count_sql) { 'SELECT COUNT(*) FROM security_findings' }
before do
migrate!
security_findings.create!(
scan_id: security_scan.id,
scanner_id: scanner.id,
uuid: SecureRandom.uuid,
severity: 0,
confidence: 0
)
end
it 'creates the original table with the data from the existing partition' do
expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false)
.and not_change { execute(security_findings_count_sql) }.from([1])
end
context 'when there are more than one partitions' do
before do
migrate!
execute(<<~SQL)
CREATE TABLE gitlab_partitions_dynamic.security_findings_11
PARTITION OF security_findings FOR VALUES IN (11)
SQL
end
it 'creates the original table from the latest existing partition' do
expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false)
.and change { execute(security_findings_count_sql) }.from([1]).to([0])
end
end
end
context 'when there is no partition' do
before do
migrate!
execute(partitions_sql).each do |partition_name|
execute("DROP TABLE gitlab_partitions_dynamic.#{partition_name}")
end
end
it 'creates the original table' do
expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false)
end
end
end
def security_findings_partitioned?
sql = <<~SQL
SELECT
COUNT(*)
FROM
pg_partitioned_table
INNER JOIN pg_class ON pg_class.oid = pg_partitioned_table.partrelid
WHERE pg_class.relname = 'security_findings'
SQL
execute(sql).first != 0
end
def execute(sql)
ActiveRecord::Base.connection.execute(sql).values.flatten
end
end

View File

@ -707,7 +707,8 @@ RSpec.describe Group do
end
describe '.public_or_visible_to_user' do
let!(:private_group) { create(:group, :private) }
let!(:private_group) { create(:group, :private) }
let!(:private_subgroup) { create(:group, :private, parent: private_group) }
let!(:internal_group) { create(:group, :internal) }
subject { described_class.public_or_visible_to_user(user) }
@ -731,6 +732,10 @@ RSpec.describe Group do
end
it { is_expected.to match_array([private_group, internal_group, group]) }
it 'does not have access to subgroups (see accessible_to_user scope)' do
is_expected.not_to include(private_subgroup)
end
end
context 'when user is a member of private subgroup' do
@ -839,6 +844,36 @@ RSpec.describe Group do
expect(described_class.by_ids_or_paths([new_group.id], [group_path])).to match_array([group, new_group])
end
end
describe 'accessible_to_user' do
subject { described_class.accessible_to_user(user) }
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:unaccessible_group) { create(:group, :private) }
let_it_be(:unaccessible_subgroup) { create(:group, :private, parent: unaccessible_group) }
let_it_be(:accessible_group) { create(:group, :private) }
let_it_be(:accessible_subgroup) { create(:group, :private, parent: accessible_group) }
context 'when user is nil' do
let(:user) { nil }
it { is_expected.to match_array([group, public_group]) }
end
context 'when user is present' do
let(:user) { create(:user) }
it { is_expected.to match_array([group, internal_group, public_group]) }
context 'when user has access to accessible group' do
before do
accessible_group.add_developer(user)
end
it { is_expected.to match_array([group, internal_group, public_group, accessible_group, accessible_subgroup]) }
end
end
end
end
describe '#to_reference' do

View File

@ -8,4 +8,55 @@ RSpec.describe Ml::Experiment do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
end
describe '#by_project_id_and_iid?' do
let(:exp) { create(:ml_experiments) }
let(:iid) { exp.iid }
subject { described_class.by_project_id_and_iid(exp.project_id, iid) }
context 'if exists' do
it { is_expected.to eq(exp) }
end
context 'if does not exist' do
let(:iid) { non_existing_record_id }
it { is_expected.to be(nil) }
end
end
describe '#by_project_id_and_name?' do
let(:exp) { create(:ml_experiments) }
let(:exp_name) { exp.name }
subject { described_class.by_project_id_and_name(exp.project_id, exp_name) }
context 'if exists' do
it { is_expected.to eq(exp) }
end
context 'if does not exist' do
let(:exp_name) { 'hello' }
it { is_expected.to be_nil }
end
end
describe '#has_record?' do
let(:exp) { create(:ml_experiments) }
let(:exp_name) { exp.name }
subject { described_class.has_record?(exp.project_id, exp_name) }
context 'if exists' do
it { is_expected.to be_truthy }
end
context 'if does not exist' do
let(:exp_name) { 'hello' }
it { is_expected.to be_falsey }
end
end
end

View File

@ -127,4 +127,50 @@ RSpec.describe NamespaceSetting, type: :model do
end
end
end
describe '#show_diff_preview_in_email?' do
context 'when not a subgroup' do
it 'returns false' do
settings = create(:namespace_settings, show_diff_preview_in_email: false)
group = create(:group, namespace_settings: settings )
expect(group.show_diff_preview_in_email?).to be_falsey
end
it 'returns true' do
settings = create(:namespace_settings, show_diff_preview_in_email: true)
group = create(:group, namespace_settings: settings )
expect(group.show_diff_preview_in_email?).to be_truthy
end
it 'does not query the db when there is no parent group' do
group = create(:group)
expect { group.show_diff_preview_in_email? }.not_to exceed_query_limit(0)
end
end
context 'when a group has parent groups' do
let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
let!(:group) { create(:group, parent: parent) }
context "when a parent group has disabled diff previews" do
let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
it 'returns false' do
expect(group.show_diff_preview_in_email?).to be_falsey
end
end
context 'when all parent groups have enabled diff previews' do
let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
it 'returns true' do
expect(group.show_diff_preview_in_email?).to be_truthy
end
end
end
end
end

View File

@ -63,4 +63,51 @@ RSpec.describe ProjectSetting, type: :model do
target_platforms.permutation(n).to_a
end
end
describe '#show_diff_preview_in_email?' do
context 'when a project is a top-level namespace' do
let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: false) }
let(:project) { create(:project, project_setting: project_settings) }
context 'when show_diff_preview_in_email is disabled' do
it 'returns false' do
expect(project).not_to be_show_diff_preview_in_email
end
end
context 'when show_diff_preview_in_email is enabled' do
let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: true) }
it 'returns true' do
settings = create(:project_setting, show_diff_preview_in_email: true)
project = create(:project, project_setting: settings)
expect(project).to be_show_diff_preview_in_email
end
end
end
context 'when a parent group has a parent group' do
let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
let(:project_settings) { create(:project_setting, show_diff_preview_in_email: true) }
let(:group) { create(:group, namespace_settings: namespace_settings) }
let!(:project) { create(:project, namespace_id: group.id, project_setting: project_settings) }
context 'when show_diff_preview_in_email is disabled for the parent group' do
it 'returns false' do
expect(project).not_to be_show_diff_preview_in_email
end
end
context 'when all ancestors have enabled diff previews' do
let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
it 'returns true' do
group.update_attribute(:show_diff_preview_in_email, true)
expect(project).to be_show_diff_preview_in_email
end
end
end
end
end

View File

@ -54,7 +54,8 @@ RSpec.describe 'Query.runner(id)' do
executor_type: :shell)
end
let_it_be(:active_project_runner) { create(:ci_runner, :project) }
let_it_be(:project1) { create(:project) }
let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
shared_examples 'runner details fetch' do
let(:query) do
@ -223,7 +224,6 @@ RSpec.describe 'Query.runner(id)' do
end
describe 'ownerProject' do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) }
let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
@ -337,7 +337,6 @@ RSpec.describe 'Query.runner(id)' do
end
describe 'for multiple runners' do
let_it_be(:project1) { create(:project, :test_repo) }
let_it_be(:project2) { create(:project, :test_repo) }
let_it_be(:project_runner1) { create(:ci_runner, :project, projects: [project1, project2], description: 'Runner 1') }
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [], description: 'Runner 2') }
@ -508,8 +507,8 @@ RSpec.describe 'Query.runner(id)' do
<<~QUERY
{
instance_runner1: #{runner_query(active_instance_runner)}
project_runner1: #{runner_query(active_project_runner)}
group_runner1: #{runner_query(active_group_runner)}
project_runner1: #{runner_query(active_project_runner)}
}
QUERY
end
@ -529,12 +528,13 @@ RSpec.describe 'Query.runner(id)' do
it 'does not execute more queries per runner', :aggregate_failures do
# warm-up license cache and so on:
post_graphql(double_query, current_user: user)
personal_access_token = create(:personal_access_token, user: user)
args = { current_user: user, token: { personal_access_token: personal_access_token } }
post_graphql(double_query, **args)
control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) }
control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) }
expect { post_graphql(double_query, current_user: user) }
.not_to exceed_query_limit(control)
expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control)
expect(graphql_data.count).to eq 6
expect(graphql_data).to match(
@ -564,4 +564,91 @@ RSpec.describe 'Query.runner(id)' do
))
end
end
describe 'sorting and pagination' do
let(:query) do
<<~GQL
query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
runner(id: $id) {
#{fields}
}
}
GQL
end
before do
post_graphql(query, current_user: user, variables: variables)
end
context 'with project search term' do
let_it_be(:project1) { create(:project, description: 'abc') }
let_it_be(:project2) { create(:project, description: 'def') }
let_it_be(:project_runner) do
create(:ci_runner, :project, projects: [project1, project2])
end
let(:variables) { { id: project_runner.to_global_id.to_s, n: n, project_search_term: search_term } }
let(:fields) do
<<~QUERY
projects(search: $projectSearchTerm, first: $n, after: $cursor) {
count
nodes {
id
}
pageInfo {
hasPreviousPage
startCursor
endCursor
hasNextPage
}
}
QUERY
end
let(:projects_data) { graphql_data_at('runner', 'projects') }
context 'set to empty string' do
let(:search_term) { '' }
context 'with n = 1' do
let(:n) { 1 }
it_behaves_like 'a working graphql query'
it 'returns paged result' do
expect(projects_data).not_to be_nil
expect(projects_data['count']).to eq 2
expect(projects_data['pageInfo']['hasNextPage']).to eq true
end
end
context 'with n = 2' do
let(:n) { 2 }
it 'returns non-paged result' do
expect(projects_data).not_to be_nil
expect(projects_data['count']).to eq 2
expect(projects_data['pageInfo']['hasNextPage']).to eq false
end
end
end
context 'set to partial match' do
let(:search_term) { 'def' }
context 'with n = 1' do
let(:n) { 1 }
it_behaves_like 'a working graphql query'
it 'returns paged result with no additional pages' do
expect(projects_data).not_to be_nil
expect(projects_data['count']).to eq 1
expect(projects_data['pageInfo']['hasNextPage']).to eq false
end
end
end
end
end
end

View File

@ -0,0 +1,188 @@
# frozen_string_literal: true
require 'spec_helper'
require 'mime/types'
RSpec.describe API::Ml::Mlflow do
include SessionHelpers
include ApiHelpers
include HttpBasicAuthHelpers
let_it_be(:project) { create(:project, :private) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:experiment) do
create(:ml_experiments, user: project.creator, project: project)
end
let(:current_user) { developer }
let(:ff_value) { true }
let(:scopes) { %w[api] }
let(:headers) do
{ 'Authorization' => "Bearer #{create(:personal_access_token, scopes: scopes, user: current_user).token}" }
end
let(:params) { {} }
let(:request) { get api(route), params: params, headers: headers }
before do
stub_feature_flags(ml_experiment_tracking: ff_value)
request
end
shared_examples 'Not Found' do |message|
it "is Not Found" do
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq(message) if message.present?
end
end
shared_examples 'Not Found - Resource Does Not Exist' do
it "is Resource Does Not Exist" do
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
end
end
shared_examples 'Bad Request' do |error_code = nil|
it "is Bad Request" do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to include({ 'error_code' => error_code }) if error_code.present?
end
end
shared_examples 'shared error cases' do
context 'when not authenticated' do
let(:headers) { {} }
it "is Unauthorized" do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when user does not have access' do
let(:current_user) { create(:user) }
it_behaves_like 'Not Found'
end
context 'when ff is disabled' do
let(:ff_value) { false }
it_behaves_like 'Not Found'
end
end
describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/get' do
let(:experiment_iid) { experiment.iid.to_s }
let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" }
it 'returns the experiment' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('ml/get_experiment')
expect(json_response).to include({
'experiment' => {
'experiment_id' => experiment_iid,
'name' => experiment.name,
'lifecycle_stage' => 'active',
'artifact_location' => 'not_implemented'
}
})
end
describe 'Error States' do
context 'when has access' do
context 'and experiment does not exist' do
let(:experiment_iid) { non_existing_record_iid.to_s }
it_behaves_like 'Not Found - Resource Does Not Exist'
end
context 'and experiment_id is not passed' do
let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get" }
it_behaves_like 'Not Found - Resource Does Not Exist'
end
end
it_behaves_like 'shared error cases'
end
end
describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/experiments/get-by-name' do
let(:experiment_name) { experiment.name }
let(:route) do
"/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}"
end
it 'returns the experiment' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('ml/get_experiment')
expect(json_response).to include({
'experiment' => {
'experiment_id' => experiment.iid.to_s,
'name' => experiment_name,
'lifecycle_stage' => 'active',
'artifact_location' => 'not_implemented'
}
})
end
describe 'Error States' do
context 'when has access but experiment does not exist' do
let(:experiment_name) { "random_experiment" }
it_behaves_like 'Not Found - Resource Does Not Exist'
end
context 'when has access but experiment_name is not passed' do
let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name" }
it_behaves_like 'Not Found - Resource Does Not Exist'
end
it_behaves_like 'shared error cases'
end
end
describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/experiments/create' do
let(:route) do
"/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/create"
end
let(:params) { { name: 'new_experiment' } }
let(:request) { post api(route), params: params, headers: headers }
it 'creates the experiment' do
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include('experiment_id' )
end
describe 'Error States' do
context 'when experiment name is not passed' do
let(:params) { {} }
it_behaves_like 'Bad Request'
end
context 'when experiment name already exists' do
let(:existing_experiment) do
create(:ml_experiments, user: current_user, project: project)
end
let(:params) { { name: existing_experiment.name } }
it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS'
end
context 'when project does not exist' do
let(:route) { "/projects/#{non_existing_record_id}/ml/mflow/api/2.0/mlflow/experiments/create" }
it_behaves_like 'Not Found', '404 Project Not Found'
end
end
end
end

View File

@ -154,11 +154,13 @@ project_setting:
- project_id
- push_rule_id
- show_default_award_emojis
- show_diff_preview_in_email
- updated_at
- cve_id_request_enabled
- mr_default_target_self
- target_platforms
- selective_code_owner_removals
- show_diff_preview_in_email
build_service_desk_setting: # service_desk_setting
unexposed_attributes:

View File

@ -761,15 +761,33 @@ RSpec.describe Trigger do
expect(subject.variables).to include('TRIGGERED_USER_LOGIN' => env['GITLAB_USER_LOGIN'])
end
describe "GITLAB_COMMIT_SHA" do
context 'when CI_COMMIT_SHA is set' do
before do
stub_env('CI_COMMIT_SHA', 'ci_commit_sha')
end
context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do
before do
stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha')
end
it 'sets GITLAB_COMMIT_SHA to ci_commit_sha' do
expect(subject.variables['GITLAB_COMMIT_SHA']).to eq('ci_commit_sha')
end
it 'sets TOP_UPSTREAM_SOURCE_SHA to ci_merge_request_source_branch_sha' do
expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq('ci_merge_request_source_branch_sha')
end
end
context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do
before do
stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
end
it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do
expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA'])
end
end
context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do
before do
stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil)
end
it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do
expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA'])
end
end
end

View File

@ -28,6 +28,16 @@ type truncatableString struct {
Truncated bool
}
// supportedLexerLanguages is used for a fast lookup to ensure the language
// is supported by the lexer library.
var supportedLexerLanguages = map[string]struct{}{}
func init() {
for _, name := range lexers.Names(true) {
supportedLexerLanguages[name] = struct{}{}
}
}
func (ts *truncatableString) UnmarshalText(b []byte) error {
s := 0
for i := 0; s < len(b); i++ {
@ -93,6 +103,24 @@ func newCodeHover(content json.RawMessage) (*codeHover, error) {
}
func (c *codeHover) setTokens() {
// fastpath: bail early if no language specified
if c.Language == "" {
return
}
// fastpath: lexer.Get() will first match against indexed languages by
// name and alias, and then fallback to a very slow filepath match. We
// avoid this slow path by first checking against languages we know to
// be within the index, and bailing if not found.
//
// Not case-folding immediately is done intentionally. These two lookups
// mirror the behaviour of lexer.Get().
if _, ok := supportedLexerLanguages[c.Language]; !ok {
if _, ok := supportedLexerLanguages[strings.ToLower(c.Language)]; !ok {
return
}
}
lexer := lexers.Get(c.Language)
if lexer == nil {
return

View File

@ -55,6 +55,14 @@ func TestHighlight(t *testing.T) {
{{Class: "k", Value: "end"}},
},
},
{
name: "ruby by file extension",
language: "rb",
value: `print hello`,
want: [][]token{
{{Value: "print hello"}},
},
},
{
name: "unknown/malicious language is passed",
language: "<lang> alert(1); </lang>",
@ -116,3 +124,43 @@ func TestTruncatingMultiByteChars(t *testing.T) {
symbolSize := 3
require.Equal(t, value[0:maxValueSize*symbolSize], c.TruncatedValue.Value)
}
func BenchmarkHighlight(b *testing.B) {
type entry struct {
Language string `json:"language"`
Value string `json:"value"`
}
tests := []entry{
{
Language: "go",
Value: "func main()",
},
{
Language: "ruby",
Value: "def read(line)",
},
{
Language: "",
Value: "<html><head>foobar</head></html>",
},
{
Language: "zzz",
Value: "def read(line)",
},
}
for _, tc := range tests {
b.Run("lang:"+tc.Language, func(b *testing.B) {
raw, err := json.Marshal(tc)
require.NoError(b, err)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := newCodeHovers(raw)
require.NoError(b, err)
}
})
}
}