Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-11-04 12:16:26 +00:00
parent 492ef8d5bb
commit 62d12a8a8d
87 changed files with 1788 additions and 1412 deletions

View File

@ -2,19 +2,7 @@
# Cop supports --autocorrect.
Gitlab/StrongMemoizeAttr:
Exclude:
- 'app/components/pajamas/avatar_component.rb'
- 'app/controllers/application_controller.rb'
- 'app/controllers/concerns/boards_actions.rb'
- 'app/controllers/concerns/creates_commit.rb'
- 'app/controllers/concerns/find_snippet.rb'
- 'app/controllers/concerns/impersonation.rb'
- 'app/controllers/concerns/issuable_actions.rb'
- 'app/controllers/concerns/issuable_collections.rb'
- 'app/controllers/concerns/known_sign_in.rb'
- 'app/controllers/concerns/wiki_actions.rb'
- 'app/controllers/ide_controller.rb'
- 'app/controllers/import/github_controller.rb'
- 'app/controllers/invites_controller.rb'
- 'app/controllers/jira_connect/application_controller.rb'
- 'app/controllers/jwt_controller.rb'
- 'app/controllers/oauth/authorizations_controller.rb'

View File

@ -122,22 +122,7 @@ Layout/LineLength:
- 'app/graphql/types/issue_sort_enum.rb'
- 'app/graphql/types/issue_type.rb'
- 'app/graphql/types/member_interface.rb'
- 'app/graphql/types/merge_request_type.rb'
- 'app/graphql/types/milestone_sort_enum.rb'
- 'app/graphql/types/milestone_type.rb'
- 'app/graphql/types/namespace/package_settings_type.rb'
- 'app/graphql/types/notes/diff_position_input_type.rb'
- 'app/graphql/types/notes/noteable_interface.rb'
- 'app/graphql/types/packages/composer/metadatum_type.rb'
- 'app/graphql/types/packages/conan/file_metadatum_type.rb'
- 'app/graphql/types/packages/helm/dependency_type.rb'
- 'app/graphql/types/packages/helm/metadata_type.rb'
- 'app/graphql/types/packages/nuget/dependency_link_metadatum_type.rb'
- 'app/graphql/types/packages/package_dependency_link_type.rb'
- 'app/graphql/types/packages/package_details_type.rb'
- 'app/graphql/types/packages/package_type_enum.rb'
- 'app/graphql/types/packages/pypi/metadatum_type.rb'
- 'app/graphql/types/project_type.rb'
- 'app/graphql/types/query_type.rb'
- 'app/graphql/types/repository/blob_type.rb'
- 'app/graphql/types/repository_type.rb'

View File

@ -1,6 +0,0 @@
---
# Cop supports --autocorrect.
Lint/UselessNumericOperation:
Details: grace period
Exclude:
- 'ee/spec/lib/gitlab/geo_spec.rb'

View File

@ -1 +1 @@
e8ecda472f3b2d7ea74fda18e64d9bf77e2e1487
2bb6448d821d9436d6b74b064a4cb48965603182

View File

@ -302,7 +302,7 @@ export default {
<div class="design-discussion-wrapper" @click="$emit('update-active-discussion')">
<design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
<ul
class="design-discussion bordered-box gl-relative gl-list-none gl-p-0"
class="design-discussion gl-border gl-relative gl-list-none gl-rounded-base gl-border-section gl-p-0"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
data-testid="design-discussion-content"
>

View File

@ -824,6 +824,7 @@ export default {
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
:active="active"
:is-diff-view-active="currentDiffFileId === item.file_hash"
/>
</dynamic-scroller-item>
</template>
@ -844,6 +845,7 @@ export default {
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
:is-diff-view-active="currentDiffFileId === file.file_hash"
/>
</template>
<div

View File

@ -104,6 +104,11 @@ export default {
required: false,
default: null,
},
isDiffViewActive: {
type: Boolean,
required: false,
default: false,
},
},
idState() {
return {
@ -415,6 +420,8 @@ export default {
'has-body': showBody,
'is-virtual-scrolling': isVirtualScrollingEnabled,
'linked-file': isLinkedFile,
'gl-border-gray-200': isDiffViewActive && !file.conflict_type,
'gl-border-red-700': isDiffViewActive && file.conflict_type,
}"
:data-path="file.new_path"
class="diff-file file-holder gl-mb-5"
@ -475,6 +482,8 @@ export default {
hasBodyClasses.header,
{
'!gl-rounded-none !gl-bg-red-200': file.conflict_type,
'!gl-bg-strong': isDiffViewActive && !file.conflict_type,
'!gl-bg-red-300': isDiffViewActive && file.conflict_type,
'!gl-rounded-tl-none !gl-rounded-tr-none': file.conflict_type && isCollapsed,
'!gl-border-0': file.conflict_type || isCollapsed,
},

View File

@ -232,7 +232,7 @@ export default {
:restricted-tool-bar-items="$options.restrictedToolBarItems"
markdown-docs-path=""
:enable-preview="false"
class="bordered-box gl-mt-0"
class="gl-border gl-mt-0 gl-rounded-base gl-border-section"
>
<template #textarea>
<gl-form-textarea

View File

@ -117,7 +117,7 @@ export default {
<template>
<div class="tree-content-holder">
<div class="table-holder bordered-box">
<div class="table-holder gl-border gl-rounded-base gl-border-section">
<table
:aria-label="tableCaption"
class="table tree-table"

View File

@ -108,7 +108,7 @@ export default {
<div class="design-discussion-wrapper" @click="$emit('update-active-discussion')">
<design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
<ul
class="design-discussion bordered-box gl-relative gl-list-none gl-p-0"
class="design-discussion gl-border gl-relative gl-list-none gl-rounded-base gl-border-section gl-p-0"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
data-testid="design-discussion-content"
>

View File

@ -0,0 +1,23 @@
import RichTimestampTooltip from './rich_timestamp_tooltip.vue';
export default {
component: RichTimestampTooltip,
title: 'work_items/rich_timestamp_tooltip',
};
const Template = (args, { argTypes }) => ({
components: { RichTimestampTooltip },
props: Object.keys(argTypes),
template: `
<div>
<span ref="targetElement">example text</span>
<rich-timestamp-tooltip :target="() => $refs.targetElement" v-bind="$props" />
</div>`,
});
export const Default = Template.bind({});
Default.args = {
rawTimestamp: '2023-10-26T14:32:12.000Z',
timestampTypeText: 'Created',
};

View File

@ -316,11 +316,6 @@ li.note {
}
}
.bordered-box {
border: 1px solid $border-color;
border-radius: $gl-border-radius-base;
}
.tooltip {
.tooltip-inner {
word-wrap: break-word;

View File

@ -41,7 +41,7 @@ table {
}
th {
background-color: $gray-10;
@apply gl-bg-subtle;
border-bottom: 0;
&.wide {
@ -53,7 +53,7 @@ table {
.thead-white {
th {
color: $gl-text-color-secondary;
@apply gl-text-subtle;
border-top: 0;
}
}

View File

@ -178,7 +178,7 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
.design-note {
padding: $gl-padding-8;
list-style: none;
border-top-left-radius: $gl-border-radius-base; // same border radius used by .bordered-box
border-top-left-radius: $gl-border-radius-base;
border-top-right-radius: $gl-border-radius-base;
@apply gl-transition-background;

View File

@ -92,9 +92,10 @@
table.tree-table {
margin-bottom: 0;
@apply dark:gl-bg-gray-50;
tr {
border-bottom: 1px solid var(--gray-50, $gray-50);
@apply gl-border-b gl-border-b-section;
td,
th {
@ -102,28 +103,51 @@
}
th {
@apply gl-bg-subtle dark:gl-bg-gray-100;
border: 0;
}
td {
border-color: var(--gl-border-color-default);
@apply gl-border-section;
}
&:hover:not(.tree-truncated-warning) {
td {
background-color: var(--blue-50, $blue-50);
@apply gl-bg-blue-50 dark:gl-bg-gray-100;
background-clip: padding-box;
border-top: 1px solid var(--blue-200, $blue-200);
border-bottom: 1px solid var(--blue-200, $blue-200);
border-top: 1px solid var(--gl-action-selected-border-color-default);
border-bottom: 1px solid var(--gl-action-selected-border-color-default);
cursor: pointer;
&:first-of-type {
box-shadow: inset 1px 0 0 0 var(--gl-action-selected-border-color-default);
}
&:last-of-type {
box-shadow: inset -1px 0 0 0 var(--gl-action-selected-border-color-default);
}
}
&:last-of-type td {
box-shadow: inset 0 -1px 0 0 var(--gl-action-selected-border-color-default);
&:first-of-type {
box-shadow: inset 1px 0 0 0 var(--gl-action-selected-border-color-default),
inset 0 -1px 0 0 var(--gl-action-selected-border-color-default);
border-bottom-left-radius: calc(#{$gl-border-radius-base} - 1px);
}
&:last-of-type {
box-shadow: inset -1px 0 0 0 var(--gl-action-selected-border-color-default),
inset 0 -1px 0 0 var(--gl-action-selected-border-color-default);
border-bottom-right-radius: calc(#{$gl-border-radius-base} - 1px);
}
}
}
&.selected {
td {
background: var(--gray-50, $gray-50);
border-top: 1px solid var(--gl-border-color-default);
border-bottom: 1px solid var(--gl-border-color-default);
@apply gl-bg-strong dark:gl-bg-gray-100 gl-border-t gl-border-b;
}
}
@ -151,7 +175,7 @@
i,
a {
color: var(--gl-text-color-default);
@apply gl-text-default;
}
img {

View File

@ -43,26 +43,25 @@ module Pajamas
end
def src
strong_memoize(:src) do
if @item.is_a?(String)
@item
elsif @item.is_a?(User)
# Users show a gravatar instead of an identicon. Also avatars of
# blocked users are only shown if the current_user is an admin.
# To not duplicate this logic, we are using existing helpers here.
current_user = begin
helpers.current_user
rescue StandardError
nil
end
helpers.avatar_icon_for_user(@item, @size, current_user: current_user)
elsif @item.is_a?(AvatarEmail)
helpers.avatar_icon_for_email(@item.email, @size)
elsif @item.try(:avatar_url)
"#{@item.avatar_url}?width=#{@size}"
if @item.is_a?(String)
@item
elsif @item.is_a?(User)
# Users show a gravatar instead of an identicon. Also avatars of
# blocked users are only shown if the current_user is an admin.
# To not duplicate this logic, we are using existing helpers here.
current_user = begin
helpers.current_user
rescue StandardError
nil
end
helpers.avatar_icon_for_user(@item, @size, current_user: current_user)
elsif @item.is_a?(AvatarEmail)
helpers.avatar_icon_for_email(@item.email, @size)
elsif @item.try(:avatar_url)
"#{@item.avatar_url}?width=#{@size}"
end
end
strong_memoize_attr :src
def srcset
return unless src

View File

@ -180,14 +180,13 @@ class ApplicationController < BaseActionController
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user.
#
def auth_user
strong_memoize(:auth_user) do
if user_signed_in?
current_user
else
try(:authenticated_user)
end
if user_signed_in?
current_user
else
try(:authenticated_user)
end
end
strong_memoize_attr :auth_user
# Devise defines current_user to be:
#

View File

@ -39,20 +39,18 @@ module BoardsActions
def push_licensed_features; end
def board
strong_memoize(:board) do
board_finder.execute.first
end
board_finder.execute.first
end
strong_memoize_attr :board
def board_visit_service
Boards::Visits::CreateService
end
def parent
strong_memoize(:parent) do
group? ? group : project
end
group? ? group : project
end
strong_memoize_attr :parent
def board_path(board)
if group?

View File

@ -127,16 +127,15 @@ module CreatesCommit
# rubocop:disable Gitlab/ModuleWithInstanceVariables
# rubocop: disable CodeReuse/ActiveRecord
def merge_request_exists?
strong_memoize(:merge_request) do
MergeRequestsFinder.new(current_user, project_id: @project.id)
MergeRequestsFinder.new(current_user, project_id: @project.id)
.execute
.opened
.find_by(
source_project_id: @project_to_commit_into,
source_branch: @branch_name,
target_branch: @start_branch)
end
end
strong_memoize_attr :merge_request_exists?
# rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables

View File

@ -8,10 +8,9 @@ module FindSnippet
# rubocop:disable CodeReuse/ActiveRecord
def snippet
strong_memoize(:snippet) do
snippet_klass.inc_relations_for_view.find_by(snippet_find_params)
end
snippet_klass.inc_relations_for_view.find_by(snippet_find_params)
end
strong_memoize_attr :snippet
# rubocop:enable CodeReuse/ActiveRecord
def snippet_klass

View File

@ -53,8 +53,7 @@ module Impersonation
end
def impersonator
strong_memoize(:impersonator) do
User.find(session[:impersonator_id]) if session[:impersonator_id]
end
User.find(session[:impersonator_id]) if session[:impersonator_id]
end
strong_memoize_attr :impersonator
end

View File

@ -166,20 +166,19 @@ module IssuableActions
private
def notes_filter
strong_memoize(:notes_filter) do
notes_filter_param = params[:notes_filter]&.to_i
notes_filter_param = params[:notes_filter]&.to_i
# GitLab Geo does not expect database UPDATE or INSERT statements to happen
# on GET requests.
# This is just a fail-safe in case notes_filter is sent via GET request in GitLab Geo.
# In some cases, we also force the filter to not be persisted with the `persist_filter` param
if Gitlab::Database.read_only? || params[:persist_filter] == 'false'
notes_filter_param || current_user&.notes_filter_for(issuable)
else
current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
end
# GitLab Geo does not expect database UPDATE or INSERT statements to happen
# on GET requests.
# This is just a fail-safe in case notes_filter is sent via GET request in GitLab Geo.
# In some cases, we also force the filter to not be persisted with the `persist_filter` param
if Gitlab::Database.read_only? || params[:persist_filter] == 'false'
notes_filter_param || current_user&.notes_filter_for(issuable)
else
current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
end
end
strong_memoize_attr :notes_filter
def discussion_cache_context
[current_user&.cache_key, project.team.human_max_access(current_user&.id), 'v2'].join(':')

View File

@ -59,37 +59,36 @@ module IssuableCollections
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def finder_options
strong_memoize(:finder_options) do
params[:state] = default_state if params[:state].blank?
params[:state] = default_state if params[:state].blank?
options = {
scope: params[:scope],
state: params[:state],
confidential: Gitlab::Utils.to_boolean(params[:confidential]),
sort: set_sort_order
}
options = {
scope: params[:scope],
state: params[:state],
confidential: Gitlab::Utils.to_boolean(params[:confidential]),
sort: set_sort_order
}
# Used by view to highlight active option
@sort = options[:sort]
# Used by view to highlight active option
@sort = options[:sort]
# When a user looks for an exact iid, we do not filter by search but only by iid
if params[:search] =~ /^#(?<iid>\d+)\z/
options[:iids] = Regexp.last_match[:iid]
params[:search] = nil
end
if @project
options[:project_id] = @project.id
options[:attempt_project_search_optimizations] = true
elsif @group
options[:group_id] = @group.id
options[:include_subgroups] = true
options[:attempt_group_search_optimizations] = true
end
params.permit(finder_type.valid_params).merge(options)
# When a user looks for an exact iid, we do not filter by search but only by iid
if params[:search] =~ /^#(?<iid>\d+)\z/
options[:iids] = Regexp.last_match[:iid]
params[:search] = nil
end
if @project
options[:project_id] = @project.id
options[:attempt_project_search_optimizations] = true
elsif @group
options[:group_id] = @group.id
options[:include_subgroups] = true
options[:attempt_group_search_optimizations] = true
end
params.permit(finder_type.valid_params).merge(options)
end
strong_memoize_attr :finder_options
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def default_state

View File

@ -36,10 +36,9 @@ module KnownSignIn
end
def sessions
strong_memoize(:session) do
ActiveSession.list(current_user).reject(&:is_impersonated)
end
ActiveSession.list(current_user).reject(&:is_impersonated)
end
strong_memoize_attr :sessions
def known_ip_addresses
[current_user.last_sign_in_ip, sessions.map(&:ip_address)].flatten

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true
class IdeController < ApplicationController
include Gitlab::Utils::StrongMemoize
include WebIdeCSP
include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize
include ProductAnalyticsTracking
before_action :authorize_read_project!, only: [:index]
@ -63,12 +63,11 @@ class IdeController < ApplicationController
end
def project
strong_memoize(:project) do
next unless params[:project_id].present?
return unless params[:project_id].present?
Project.find_by_full_path(params[:project_id])
end
Project.find_by_full_path(params[:project_id])
end
strong_memoize_attr :project
def tracking_namespace_source
project.namespace

View File

@ -154,10 +154,9 @@ class Import::GithubController < Import::BaseController
override :provider_url
def provider_url
strong_memoize(:provider_url) do
oauth_config&.dig('url').presence || 'https://github.com'
end
oauth_config&.dig('url').presence || 'https://github.com'
end
strong_memoize_attr :provider_url
private

View File

@ -56,17 +56,15 @@ class InvitesController < ApplicationController
end
def member?
strong_memoize(:is_member) do
@member.source.has_user?(current_user)
end
@member.source.has_user?(current_user)
end
strong_memoize_attr :member?
def member
strong_memoize(:member) do
@token = params[:id].to_s
Member.find_by_invite_token(@token)
end
@token = params[:id].to_s
Member.find_by_invite_token(@token)
end
strong_memoize_attr :member
def ensure_member_exists
return if member

View File

@ -82,8 +82,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
end
def doorkeeper_application
strong_memoize(:doorkeeper_application) { ::Doorkeeper::OAuth::Client.find(params['client_id'].to_s)&.application }
::Doorkeeper::OAuth::Client.find(params['client_id'].to_s)&.application
end
strong_memoize_attr :doorkeeper_application
def application_has_read_user_scope?
doorkeeper_application&.includes_scope?(Gitlab::Auth::READ_USER_SCOPE)

View File

@ -34,7 +34,7 @@ module Autocomplete
# Include current user if available to filter by "Me"
items.unshift(current_user) if prepend_current_user?
items.unshift(author) if prepend_author? && author&.active?
items.unshift(author) if prepend_author? && author&.active? && !author&.import_user? && !author&.placeholder?
end
items = filter_users_by_push_ability(items)

View File

@ -119,9 +119,9 @@ module Types
alpha: { milestone: '16.5' },
calls_gitaly: true
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true, calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, ' \
'allowing the merge request to be merged.'
field :rebase_commit_sha, GraphQL::Types::String, null: true,
description: 'Rebase commit SHA of the merge request.'
field :rebase_in_progress, GraphQL::Types::Boolean, method: :rebase_in_progress?, null: false, calls_gitaly: true,
@ -163,7 +163,8 @@ module Types
description: 'Pipeline running on the branch HEAD of the merge request.'
field :pipelines,
null: true,
description: 'Pipelines for the merge request. Note: for performance reasons, no more than the most recent 500 pipelines will be returned.',
description: 'Pipelines for the merge request. Note: for performance reasons, ' \
'no more than the most recent 500 pipelines will be returned.',
resolver: Resolvers::MergeRequestPipelinesResolver
field :assignees,
@ -193,8 +194,12 @@ module Types
description: 'Indicates if the merge request has conflicts.'
field :milestone, Types::MilestoneType, null: true,
description: 'Milestone of the merge request.'
field :participants, Types::MergeRequests::ParticipantType.connection_type, null: true, complexity: 15,
description: 'Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes.',
field :participants,
Types::MergeRequests::ParticipantType.connection_type,
null: true,
complexity: 15,
description: 'Participants in the merge request. This includes the author, ' \
'assignees, reviewers, and users mentioned in notes.',
resolver: Resolvers::Users::ParticipantsResolver
field :reference, GraphQL::Types::String, null: false, method: :to_reference,
description: 'Internal reference of the merge request. Returned in shortened format by default.' do
@ -240,11 +245,22 @@ module Types
description: 'User who merged this merge request or set it to auto-merge.'
field :mergeable, GraphQL::Types::Boolean, null: false, method: :mergeable?, calls_gitaly: true,
description: 'Indicates if the merge request is mergeable.'
field :security_auto_fix, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.', deprecated: { reason: 'Security Auto Fix experiment feature was removed. It was always hidden behind `security_auto_fix` feature flag', milestone: '16.11' }
field :security_auto_fix,
GraphQL::Types::Boolean,
null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.',
deprecated: {
reason: 'Security Auto Fix experiment feature was removed. ' \
'It was always hidden behind `security_auto_fix` feature flag',
milestone: '16.11'
}
field :squash, GraphQL::Types::Boolean, null: false,
description: 'Indicates if the merge request is set to be squashed when merged. [Project settings](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html#configure-squash-options-for-a-project) may override this value. Use `squash_on_merge` instead to take project squash options into account.'
description: <<~HEREDOC.squish
Indicates if the merge request is set to be squashed when merged.
[Project settings](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html#configure-squash-options-for-a-project)
may override this value. Use `squash_on_merge` instead to take project squash options into account.
HEREDOC
field :squash_on_merge, GraphQL::Types::Boolean, null: false, method: :squash_on_merge?,
description: 'Indicates if the merge request will be squashed when merged.'
field :timelogs, Types::TimelogType.connection_type, null: false,
@ -296,7 +312,11 @@ module Types
def user_discussions_count
BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_discussions_count) do |ids, loader, args|
counts = Note.count_for_collection(ids, 'MergeRequest', 'COUNT(DISTINCT discussion_id) as count').index_by(&:noteable_id)
counts = Note.count_for_collection(
ids,
'MergeRequest',
'COUNT(DISTINCT discussion_id) as count'
).index_by(&:noteable_id)
ids.each do |id|
loader.call(id, counts[id]&.count || 0)

View File

@ -7,7 +7,13 @@ module Types
value 'DUE_DATE_ASC', 'Milestone due date by ascending order.', value: :due_date_asc
value 'DUE_DATE_DESC', 'Milestone due date by descending order.', value: :due_date_desc
value 'EXPIRED_LAST_DUE_DATE_ASC', 'Group milestones in this order: non-expired milestones with due dates, non-expired milestones without due dates and expired milestones then sort by due date in ascending order.', value: :expired_last_due_date_asc
value 'EXPIRED_LAST_DUE_DATE_DESC', 'Group milestones in this order: non-expired milestones with due dates, non-expired milestones without due dates and expired milestones then sort by due date in descending order.', value: :expired_last_due_date_desc
value 'EXPIRED_LAST_DUE_DATE_ASC',
'Group milestones in this order: non-expired milestones with due dates, non-expired milestones ' \
'without due dates and expired milestones then sort by due date in ascending order.',
value: :expired_last_due_date_asc
value 'EXPIRED_LAST_DUE_DATE_DESC',
'Group milestones in this order: non-expired milestones with due dates, non-expired milestones ' \
'without due dates and expired milestones then sort by due date in descending order.',
value: :expired_last_due_date_desc
end
end

View File

@ -27,10 +27,12 @@ module Types
description: 'State of the milestone.'
field :expired, GraphQL::Types::Boolean, null: false,
description: 'Expired state of the milestone (a milestone is expired when the due date is past the current date). Defaults to `false` when due date has not been set.'
description: 'Expired state of the milestone (a milestone is expired when the due date is past the current ' \
'date). Defaults to `false` when due date has not been set.'
field :upcoming, GraphQL::Types::Boolean, null: false,
description: 'Upcoming state of the milestone (a milestone is upcoming when the start date is in the future). Defaults to `false` when start date has not been set.'
description: 'Upcoming state of the milestone (a milestone is upcoming when the start date is in the future). ' \
'Defaults to `false` when start date has not been set.'
field :web_path, GraphQL::Types::String, null: false, method: :milestone_path,
description: 'Web path of the milestone.'

View File

@ -10,13 +10,15 @@ module Types
field :generic_duplicate_exception_regex, Types::UntrustedRegexp,
null: true,
description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that ' \
'match this regex. Otherwise, this setting has no effect.'
field :generic_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
field :maven_duplicate_exception_regex, Types::UntrustedRegexp,
null: true,
description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that ' \
'match this regex. Otherwise, this setting has no effect.'
field :maven_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
@ -28,7 +30,8 @@ module Types
description: 'Indicates whether npm package forwarding is allowed for this namespace.'
field :nuget_duplicate_exception_regex, Types::UntrustedRegexp,
null: true,
description: 'When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. '
description: 'When nuget_duplicates_allowed is false, you can publish duplicate packages with names that ' \
'match this regex. Otherwise, this setting has no effect. '
field :nuget_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate NuGet packages are allowed for this namespace.'
@ -37,7 +40,8 @@ module Types
description: 'Indicates whether PyPI package forwarding is allowed for this namespace.'
field :terraform_module_duplicate_exception_regex, Types::UntrustedRegexp,
null: true,
description: 'When terraform_module_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
description: 'When terraform_module_duplicates_allowed is false, you can publish duplicate packages with ' \
'names that match this regex. Otherwise, this setting has no effect.'
field :terraform_module_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate Terraform packages are allowed for this namespace.'

View File

@ -5,8 +5,10 @@ module Types
module NoteableInterface
include Types::BaseInterface
field :notes, resolver: Resolvers::Noteable::NotesResolver, null: false, description: "All notes on this noteable."
field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable."
field :notes, resolver: Resolvers::Noteable::NotesResolver, null: false,
description: "All notes on this noteable."
field :discussions, Types::Notes::DiscussionType.connection_type, null: false,
description: "All discussions on this noteable."
field :commenters, Types::UserType.connection_type, null: false, description: "All commenters on this noteable."
def self.resolve_type(object, context)

View File

@ -9,7 +9,8 @@ module Types
authorize :read_package
field :composer_json, Types::Packages::Composer::JsonType, null: false, description: 'Data of the Composer JSON file.'
field :composer_json, Types::Packages::Composer::JsonType, null: false,
description: 'Data of the Composer JSON file.'
field :target_sha, GraphQL::Types::String, null: false, description: 'Target SHA of the package.'
end
end

View File

@ -11,11 +11,16 @@ module Types
authorize :read_package
field :conan_file_type, ::Types::Packages::Conan::MetadatumFileTypeEnum, null: false, description: 'Type of the Conan file.'
field :conan_package_reference, GraphQL::Types::String, null: true, description: 'Reference of the Conan package.'
field :id, ::Types::GlobalIDType[::Packages::Conan::FileMetadatum], null: false, description: 'ID of the metadatum.'
field :package_revision, GraphQL::Types::String, null: true, description: 'Revision of the package.', method: :package_revision_value
field :recipe_revision, GraphQL::Types::String, null: false, description: 'Revision of the Conan recipe.', method: :recipe_revision_value
field :conan_file_type, ::Types::Packages::Conan::MetadatumFileTypeEnum, null: false,
description: 'Type of the Conan file.'
field :conan_package_reference, GraphQL::Types::String, null: true,
description: 'Reference of the Conan package.'
field :id, ::Types::GlobalIDType[::Packages::Conan::FileMetadatum], null: false,
description: 'ID of the metadatum.'
field :package_revision, GraphQL::Types::String, null: true, description: 'Revision of the package.',
method: :package_revision_value
field :recipe_revision, GraphQL::Types::String, null: false, description: 'Revision of the Conan recipe.',
method: :recipe_revision_value
end
end
end

View File

@ -9,10 +9,15 @@ module Types
description 'Represents a Helm dependency'
# Need to be synced with app/validators/json_schemas/helm_metadata.json#dependencies
field :alias, GraphQL::Types::String, null: true, description: 'Alias of the dependency.', resolver_method: :resolve_alias
field :alias,
GraphQL::Types::String,
null: true,
description: 'Alias of the dependency.',
resolver_method: :resolve_alias
field :condition, GraphQL::Types::String, null: true, description: 'Condition of the dependency.'
field :enabled, GraphQL::Types::Boolean, null: true, description: 'Indicates the dependency is enabled.'
field :import_values, [GraphQL::Types::JSON], null: true, description: 'Import-values of the dependency.', hash_key: :'import-values'
field :import_values, [GraphQL::Types::JSON], null: true, description: 'Import-values of the dependency.',
hash_key: :'import-values'
field :name, GraphQL::Types::String, null: true, description: 'Name of the dependency.'
field :repository, GraphQL::Types::String, null: true, description: 'Repository of the dependency.'
field :tags, [GraphQL::Types::String], null: true, description: 'Tags of the dependency.'

View File

@ -10,17 +10,31 @@ module Types
# Need to be synced with app/validators/json_schemas/helm_metadata.json
field :annotations, GraphQL::Types::JSON, null: true, description: 'Annotations for the chart.' # rubocop:disable Graphql/JSONType
field :api_version, GraphQL::Types::String, null: false, description: 'API version of the chart.', hash_key: :apiVersion
field :app_version, GraphQL::Types::String, null: true, description: 'App version of the chart.', hash_key: :appVersion
field :api_version,
GraphQL::Types::String,
null: false,
description: 'API version of the chart.',
hash_key: :apiVersion
field :app_version,
GraphQL::Types::String,
null: true,
description: 'App version of the chart.',
hash_key: :appVersion
field :condition, GraphQL::Types::String, null: true, description: 'Condition for the chart.'
field :dependencies, [Types::Packages::Helm::DependencyType], null: true, description: 'Dependencies of the chart.'
field :dependencies, [Types::Packages::Helm::DependencyType], null: true,
description: 'Dependencies of the chart.'
field :deprecated, GraphQL::Types::Boolean, null: true, description: 'Indicates if the chart is deprecated.'
field :description, GraphQL::Types::String, null: true, description: 'Description of the chart.'
field :home, GraphQL::Types::String, null: true, description: 'URL of the home page.'
field :icon, GraphQL::Types::String, null: true, description: 'URL to an SVG or PNG image for the chart.'
field :keywords, [GraphQL::Types::String], null: true, description: 'Keywords for the chart.'
field :kube_version, GraphQL::Types::String, null: true, description: 'Kubernetes versions for the chart.', hash_key: :kubeVersion
field :maintainers, [Types::Packages::Helm::MaintainerType], null: true, description: 'Maintainers of the chart.'
field :kube_version,
GraphQL::Types::String,
null: true,
description: 'Kubernetes versions for the chart.',
hash_key: :kubeVersion
field :maintainers, [Types::Packages::Helm::MaintainerType], null: true,
description: 'Maintainers of the chart.'
field :name, GraphQL::Types::String, null: false, description: 'Name of the chart.'
field :sources, [GraphQL::Types::String], null: true, description: 'URLs of the source code for the chart.'
field :tags, GraphQL::Types::String, null: true, description: 'Tags for the chart.'

View File

@ -9,8 +9,10 @@ module Types
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Nuget::DependencyLinkMetadatum], null: false, description: 'ID of the metadatum.'
field :target_framework, GraphQL::Types::String, null: false, description: 'Target framework of the dependency link package.'
field :id, ::Types::GlobalIDType[::Packages::Nuget::DependencyLinkMetadatum], null: false,
description: 'ID of the metadatum.'
field :target_framework, GraphQL::Types::String, null: false,
description: 'Target framework of the dependency link package.'
end
end
end

View File

@ -9,7 +9,8 @@ module Types
field :dependency, Types::Packages::PackageDependencyType, null: true, description: 'Dependency.'
field :dependency_type, Types::Packages::PackageDependencyTypeEnum, null: false, description: 'Dependency type.'
field :id, ::Types::GlobalIDType[::Packages::DependencyLink], null: false, description: 'ID of the dependency link.'
field :id, ::Types::GlobalIDType[::Packages::DependencyLink], null: false,
description: 'ID of the dependency link.'
field :metadata, Types::Packages::DependencyLinkMetadataType, null: true, description: 'Dependency link metadata.'
# NOTE: This method must be kept in sync with the union

View File

@ -13,11 +13,17 @@ module Types
field :versions, ::Types::Packages::PackageBaseType.connection_type, null: true,
description: 'Other versions of the package.'
field :package_files, Types::Packages::PackageFileType.connection_type, null: true, method: :installable_package_files, description: 'Package files.'
field :package_files,
Types::Packages::PackageFileType.connection_type,
null: true,
method: :installable_package_files,
description: 'Package files.'
field :dependency_links, Types::Packages::PackageDependencyLinkType.connection_type, null: true, description: 'Dependency link.'
field :dependency_links, Types::Packages::PackageDependencyLinkType.connection_type, null: true,
description: 'Dependency link.'
field :composer_config_repository_url, GraphQL::Types::String, null: true, description: 'Url of the Composer setup endpoint.'
field :composer_config_repository_url, GraphQL::Types::String, null: true,
description: 'Url of the Composer setup endpoint.'
field :composer_url, GraphQL::Types::String, null: true, description: 'Url of the Composer endpoint.'
field :conan_url, GraphQL::Types::String, null: true, description: 'Url of the Conan project endpoint.'
field :maven_url, GraphQL::Types::String, null: true, description: 'Url of the Maven project endpoint.'
@ -26,9 +32,11 @@ module Types
field :pypi_setup_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project setup endpoint.'
field :pypi_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project endpoint.'
field :last_downloaded_at, Types::TimeType, null: true, description: 'Last time that a file of this package was downloaded.'
field :last_downloaded_at, Types::TimeType, null: true,
description: 'Last time that a file of this package was downloaded.'
field :public_package, GraphQL::Types::Boolean, null: true, description: 'Indicates if there is public access to the package.'
field :public_package, GraphQL::Types::Boolean, null: true,
description: 'Indicates if there is public access to the package.'
def composer_config_repository_url
composer_config_repository_name(object.project.group&.id)

View File

@ -11,7 +11,9 @@ module Types
::Packages::Package.package_types.keys.each do |package_type|
type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
value package_type.to_s.upcase, description: "Packages from the #{type_name} package manager", value: package_type.to_s
value package_type.to_s.upcase,
description: "Packages from the #{type_name} package manager",
value: package_type.to_s
end
end
end

View File

@ -18,7 +18,8 @@ module Types
field :id, ::Types::GlobalIDType[::Packages::Pypi::Metadatum], null: false, description: 'ID of the metadatum.'
field :keywords, GraphQL::Types::String, null: true, description: 'List of keywords, separated by commas.'
field :metadata_version, GraphQL::Types::String, null: true, description: 'Metadata version.'
field :required_python, GraphQL::Types::String, null: true, description: 'Required Python version of the Pypi package.'
field :required_python, GraphQL::Types::String, null: true,
description: 'Required Python version of the Pypi package.'
field :summary, GraphQL::Types::String, null: true, description: 'One-line summary of the description.'
end
end

View File

@ -190,7 +190,8 @@ module Types
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::Types::Boolean,
null: true,
description: 'Indicates if merge requests of the project can only be merged when all the discussions are resolved.'
description: 'Indicates if merge requests of the project can only be merged ' \
'when all the discussions are resolved.'
field :only_allow_merge_if_pipeline_succeeds, GraphQL::Types::Boolean,
null: true,
@ -658,7 +659,8 @@ module Types
field :data_transfer, Types::DataTransfer::ProjectDataTransferType,
null: true, # disallow null once data_transfer_monitoring feature flag is rolled-out! https://gitlab.com/gitlab-org/gitlab/-/issues/391682
resolver: Resolvers::DataTransfer::ProjectDataTransferResolver,
description: 'Data transfer data point for a specific period. This is mocked data under a development feature flag.'
description: 'Data transfer data point for a specific period. ' \
'This is mocked data under a development feature flag.'
field :visible_forks, Types::ProjectType.connection_type,
null: true,

View File

@ -336,7 +336,13 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.order_id_desc.search(term, use_minimum_char_limit: false).limit(limit).map do |group|
scope = if Feature.enabled?(:autocomplete_group_search_optimization, current_user)
current_user.search_on_authorized_groups(term, use_minimum_char_limit: false)
else
current_user.authorized_groups.search(term, use_minimum_char_limit: false)
end
scope.order_id_desc.limit(limit).map do |group|
{
category: "Groups",
id: group.id,

View File

@ -0,0 +1,821 @@
# frozen_string_literal: true
module Integrations
module Base
module Integration
extend ActiveSupport::Concern
UnknownType = Class.new(StandardError)
INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker
datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki
gitlab_slack_application hangouts_chat harbor irker jira matrix
mattermost mattermost_slash_commands microsoft_teams packagist phorge pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram
unify_circuit webex_teams youtrack zentao
].freeze
# Integrations that can only be enabled on the instance-level
INSTANCE_LEVEL_ONLY_INTEGRATION_NAMES = %w[
beyond_identity
].freeze
# Integrations that can only be enabled on the project-level
PROJECT_LEVEL_ONLY_INTEGRATION_NAMES = %w[
apple_app_store google_play jenkins
].freeze
# Integrations that cannot be enabled on the instance-level
PROJECT_AND_GROUP_LEVEL_ONLY_INTEGRATION_NAMES = %w[
jira_cloud_app
].freeze
# Fake integrations to help with local development.
DEV_INTEGRATION_NAMES = %w[
mock_ci mock_monitoring
].freeze
# Base classes which aren't actual integrations.
BASE_CLASSES = %w[
Integrations::BaseChatNotification
Integrations::BaseCi
Integrations::BaseIssueTracker
Integrations::BaseMonitoring
Integrations::BaseSlackNotification
Integrations::BaseSlashCommands
Integrations::BaseThirdPartyWiki
].freeze
BASE_ATTRIBUTES = %w[id instance project_id group_id created_at updated_at
encrypted_properties encrypted_properties_iv properties].freeze
SECTION_TYPE_CONFIGURATION = 'configuration'
SECTION_TYPE_CONNECTION = 'connection'
SECTION_TYPE_TRIGGER = 'trigger'
SNOWPLOW_EVENT_ACTION = 'perform_integrations_action'
SNOWPLOW_EVENT_LABEL = 'redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly'
included do
include Sortable
include Importable
include Integrations::Loggable
include Integrations::HasDataFields
include Integrations::ResetSecretFields
include FromUnion
include EachBatch
extend SafeFormatHelper
extend ::Gitlab::Utils::Override
self.allow_legacy_sti_class = true
self.inheritance_column = :type_new # rubocop:disable Database/AvoidInheritanceColumn -- existing code moved as is
attr_encrypted :properties,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
marshal: true,
marshaler: ::Gitlab::Json,
encode: false,
encode_iv: false
alias_attribute :name, :title
# Handle assignment of props with symbol keys.
# To do this correctly, we need to call the method generated by attr_encrypted.
alias_method :attr_encrypted_props=, :properties=
private :attr_encrypted_props=
def properties=(props)
self.attr_encrypted_props = props&.with_indifferent_access&.freeze
end
alias_attribute :type, :type_new
attribute :active, default: false
attribute :alert_events, default: true
attribute :incident_events, default: false
attribute :category, default: 'common'
attribute :commit_events, default: true
attribute :confidential_issues_events, default: true
attribute :confidential_note_events, default: true
attribute :deployment_events, default: false
attribute :issues_events, default: true
attribute :job_events, default: true
attribute :merge_requests_events, default: true
attribute :note_events, default: true
attribute :pipeline_events, default: true
attribute :push_events, default: true
attribute :tag_push_events, default: true
attribute :wiki_page_events, default: true
attribute :group_mention_events, default: false
attribute :group_confidential_mention_events, default: false
after_initialize :initialize_properties
after_commit :reset_updated_properties
belongs_to :project, inverse_of: :integrations
belongs_to :group, inverse_of: :integrations
validates :project_id, presence: true, unless: -> { instance_level? || group_level? }
validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
validates :project_id, :group_id, absence: true, if: -> { instance_level? }
validates :type, presence: true, exclusion: BASE_CLASSES
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
scope :third_party_wikis, -> { where(category: 'third_party_wiki').active }
scope :by_name, ->(name) { by_type(integration_name_to_type(name)) }
scope :external_wikis, -> { by_name(:external_wiki).active }
scope :active, -> { where(active: true) }
scope :by_type, ->(type) { where(type: type) } # INTERNAL USE ONLY: use by_name instead
scope :by_active_flag, ->(flag) { where(active: flag) }
scope :inherit_from_id, ->(id) { where(inherit_from_id: id) }
scope :with_default_settings, -> { where.not(inherit_from_id: nil) }
scope :with_custom_settings, -> { where(inherit_from_id: nil) }
scope :for_group, ->(group) {
types = available_integration_types(include_project_specific: false)
where(group_id: group, type: types)
}
scope :for_instance, -> {
types = available_integration_types(include_project_specific: false, include_group_specific: false)
where(instance: true, type: types)
}
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :archive_trace_hooks, -> { where(archive_trace_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :incident_hooks, -> { where(incident_events: true, active: true) }
scope :deployment, -> { where(category: 'deployment') }
scope :group_mention_hooks, -> { where(group_mention_events: true, active: true) }
scope :group_confidential_mention_hooks, -> { where(group_confidential_mention_events: true, active: true) }
scope :exclusions_for_project, ->(project) { where(project: project, active: false) }
class << self
private
attr_writer :field_storage
def field_storage
@field_storage || :properties
end
end
# rubocop:disable Gitlab/NoCodeCoverageComment -- existing code moved as is
# :nocov: Tested on subclasses.
def self.field(name, storage: field_storage, **attrs)
fields << ::Integrations::Field.new(name: name, integration_class: self, **attrs)
case storage
when :attribute
# noop
when :properties
prop_accessor(name)
when :data_fields
data_field(name)
else
raise ArgumentError, "Unknown field storage: #{storage}"
end
boolean_accessor(name) if attrs[:type] == :checkbox && storage != :attribute
end
# :nocov:
# rubocop:enable Gitlab/NoCodeCoverageComment
def self.fields
@fields ||= []
end
def fields
self.class.fields.dup
end
# Provide convenient accessor methods for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.prop_accessor(*args)
args.each do |arg|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
unless method_defined?(arg)
def #{arg}
properties['#{arg}'] if properties.present?
end
end
def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties = self.properties.merge('#{arg}' => value)
end
def #{arg}_changed?
#{arg}_touched? && #{arg} != #{arg}_was
end
def #{arg}_touched?
updated_properties.include?('#{arg}')
end
def #{arg}_was
updated_properties['#{arg}']
end
RUBY
end
end
# Provide convenient boolean accessor methods for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.boolean_accessor(*args)
args.each do |arg|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
# Make the original getter available as a private method.
alias_method :#{arg}_before_type_cast, :#{arg}
private(:#{arg}_before_type_cast)
def #{arg}
Gitlab::Utils.to_boolean(#{arg}_before_type_cast)
end
def #{arg}?
# '!!' is used because nil or empty string is converted to nil
!!#{arg}
end
RUBY
end
end
private_class_method :boolean_accessor
def self.title
raise NotImplementedError
end
def self.description
raise NotImplementedError
end
def self.help
# no-op
end
def self.to_param
raise NotImplementedError
end
def self.attribution_notice
# no-op
end
def self.event_names
supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
end
def self.supported_events
%w[commit push tag_push issue confidential_issue merge_request wiki_page]
end
def self.default_test_event
'push'
end
def self.event_description(event)
IntegrationsHelper.integration_event_description(event)
end
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
return unless name.in?(available_integration_names(
include_project_specific: false,
include_group_specific: group_id.present?,
include_instance_specific: instance))
integration_name_to_model(name).find_or_initialize_by(instance: instance, group_id: group_id)
end
def self.find_or_initialize_all_non_project_specific(scope, include_instance_specific: false)
scope + build_nonexistent_integrations_for(scope,
include_group_specific: !include_instance_specific,
include_instance_specific: include_instance_specific)
end
def self.build_nonexistent_integrations_for(...)
nonexistent_integration_types_for(...).map do |type|
integration_type_to_model(type).new
end
end
private_class_method :build_nonexistent_integrations_for
# Returns a list of integration types that do not exist in the given scope.
# Example: ["AsanaService", ...]
def self.nonexistent_integration_types_for(
scope,
include_group_specific: false,
include_instance_specific: false)
# Using #map instead of #pluck to save one query count. This is because
# ActiveRecord loaded the object here, so we don't need to query again later.
available_integration_types(
include_project_specific: false,
include_group_specific: include_group_specific,
include_instance_specific: include_instance_specific
) - scope.map(&:type)
end
private_class_method :nonexistent_integration_types_for
# Returns a list of available integration names.
# Example: ["asana", ...]
def self.available_integration_names(
include_project_specific: true,
include_group_specific: true,
include_instance_specific: true,
include_dev: true,
include_disabled: false)
names = integration_names.dup
names.concat(project_specific_integration_names) if include_project_specific
names.concat(dev_integration_names) if include_dev
names.concat(instance_specific_integration_names) if include_instance_specific
if include_project_specific || include_group_specific
names.concat(project_and_group_specific_integration_names)
end
names -= disabled_integration_names unless include_disabled
names.sort_by(&:downcase)
end
def self.integration_names
names = INTEGRATION_NAMES.dup
unless Feature.enabled?(:gitlab_for_slack_app_instance_and_group_level, type: :beta) && # rubocop:disable Gitlab/FeatureFlagWithoutActor -- existing code moved as is
(Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?)
names.delete('gitlab_slack_application')
end
names
end
def self.instance_specific_integration_names
INSTANCE_LEVEL_ONLY_INTEGRATION_NAMES
end
def self.instance_specific_integration_types
instance_specific_integration_names.map { |name| integration_name_to_type(name) }
end
def self.dev_integration_names
return [] unless Gitlab.dev_or_test_env?
DEV_INTEGRATION_NAMES
end
def self.project_specific_integration_names
names = PROJECT_LEVEL_ONLY_INTEGRATION_NAMES.dup
if Feature.disabled?(:gitlab_for_slack_app_instance_and_group_level, type: :beta) && # rubocop:disable Gitlab/FeatureFlagWithoutActor -- existing code moved as is
(Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?)
names << 'gitlab_slack_application'
end
names
end
def self.project_and_group_specific_integration_names
PROJECT_AND_GROUP_LEVEL_ONLY_INTEGRATION_NAMES.dup
end
private_class_method :project_and_group_specific_integration_names
# Returns a list of available integration types.
# Example: ["Integrations::Asana", ...]
def self.available_integration_types(...)
available_integration_names(...).map do # rubocop:disable Style/NumberedParameters -- existing code moved as is
integration_name_to_type(_1)
end
end
# Returns a list of disabled integration names.
# Example: ["gitlab_slack_application", ...]
def self.disabled_integration_names
# The GitLab for Slack app integration is only available when enabled through settings.
# The Slack Slash Commands integration is only available for customers
# who cannot use the GitLab for Slack app.
disabled = Gitlab::CurrentSettings.slack_app_enabled ? ['slack_slash_commands'] : ['gitlab_slack_application']
disabled += ['jira_cloud_app'] unless Gitlab::CurrentSettings.jira_connect_application_key.present?
disabled
end
private_class_method :disabled_integration_names
# Returns the model for the given integration name.
# Example: :asana => Integrations::Asana
def self.integration_name_to_model(name)
type = integration_name_to_type(name)
integration_type_to_model(type)
end
# Returns the STI type for the given integration name.
# Example: "asana" => "Integrations::Asana"
def self.integration_name_to_type(name)
name = name.to_s
if available_integration_names(include_disabled: true).exclude?(name)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownType.new(name.inspect))
else
"Integrations::#{name.camelize}"
end
end
# Returns the model for the given STI type.
# Example: "Integrations::Asana" => Integrations::Asana
def self.integration_type_to_model(type)
type.constantize
end
private_class_method :integration_type_to_model
def self.build_from_integration(integration, project_id: nil, group_id: nil)
new_integration = integration.dup
new_integration.instance = false
new_integration.project_id = project_id
new_integration.group_id = group_id
new_integration.inherit_from_id = integration.id if integration.inheritable?
new_integration
end
# Duplicating an integration also duplicates the data fields. Duped records have different ciphertexts.
override :dup
def dup
new_integration = super
new_integration.assign_attributes(reencrypt_properties)
if supports_data_fields?
fields = data_fields.dup
fields.integration = new_integration
end
new_integration
end
def inheritable?
instance_level? || group_level?
end
def self.instance_exists_for?(type)
exists?(instance: true, type: type)
end
def self.default_integration(type, scope)
closest_group_integration(type, scope) || instance_level_integration(type)
end
def self.closest_group_integration(type, scope)
group_ids = scope.ancestors(hierarchy_order: :asc).reselect(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
.order(Arel.sql("array_position(#{array}::bigint[], #{table_name}.group_id)"))
.first
end
private_class_method :closest_group_integration
def self.instance_level_integration(type)
find_by(type: type, instance: true)
end
private_class_method :instance_level_integration
def self.default_integrations(owner, scope)
group_ids = sorted_ancestors(owner).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
order = Arel.sql("type_new ASC, array_position(#{array}::bigint[], #{table_name}.group_id), instance DESC")
from_union([scope.where(instance: true), scope.where(group_id: group_ids, inherit_from_id: nil)])
.order(order)
.group_by(&:type)
.transform_values(&:first)
end
private_class_method :default_integrations
def self.create_from_default_integrations(owner, association)
active_default_count = create_from_active_default_integrations(owner, association)
default_instance_specific_count = create_from_default_instance_specific_integrations(owner, association)
active_default_count + default_instance_specific_count
end
# Returns the number of successfully saved integrations
# Duplicate integrations are excluded from this count by their validations.
def self.create_from_active_default_integrations(owner, association)
default_integrations(
owner,
active.where.not(type: instance_specific_integration_types)
).count { |_type, integration| build_from_integration(integration, association => owner.id).save }
end
def self.create_from_default_instance_specific_integrations(owner, association)
default_integrations(
owner,
where(type: instance_specific_integration_types)
).count { |_type, integration| build_from_integration(integration, association => owner.id).save }
end
def self.descendants_from_self_or_ancestors_from(integration)
scope = where(type: integration.type)
from_union([
scope.where(group: integration.group.descendants),
scope.where(project: Project.in_namespace(integration.group.self_and_descendants))
])
end
def self.inherited_descendants_from_self_or_ancestors_from(integration)
inherit_from_ids =
where(type: integration.type, group: integration.group.self_and_ancestors)
.or(where(type: integration.type, instance: true)).select(:id)
from_union([
where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
where(
type: integration.type,
inherit_from_id: inherit_from_ids,
project: Project.in_namespace(integration.group.self_and_descendants)
)
])
end
def activated?
active
end
def operating?
active && persisted?
end
def manual_activation?
true
end
def editable?
true
end
def activate_disabled_reason
nil
end
def category
read_attribute(:category).to_sym
end
def initialize_properties
self.properties = {} if has_attribute?(:encrypted_properties) && encrypted_properties.nil?
end
def title
self.class.title
end
def description
self.class.description
end
def help
self.class.help
end
def to_param
self.class.to_param
end
def attribution_notice
self.class.attribution_notice
end
def sections
[]
end
def secret_fields
fields.select(&:secret?).pluck(:name) # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- existing code moved as is
end
# Expose a list of fields in the JSON endpoint.
#
# This list is used in `Integration#as_json(only: json_fields)`.
def json_fields
%w[active]
end
# properties is always nil - ignore it.
override :attributes
def attributes
super.except('properties')
end
# Returns a hash of attributes (columns => values) used for inserting into the database.
def to_database_hash
column = self.class.attribute_aliases.fetch('type', 'type')
attributes_for_database.except(*BASE_ATTRIBUTES)
.merge(column => type)
.merge(reencrypt_properties)
end
def reencrypt_properties
unless properties.nil? || properties.empty?
alg = self.class.attr_encrypted_attributes[:properties][:algorithm]
iv = generate_iv(alg)
ep = self.class.attr_encrypt(:properties, properties, { iv: iv })
end
{ 'encrypted_properties' => ep, 'encrypted_properties_iv' => iv }
end
def event_channel_names
[]
end
def event_names
self.class.event_names
end
def api_field_names
fields # rubocop:disable Style/NumberedParameters -- existing code moved as is
.reject { _1[:type] == :password || _1[:name] == 'webhook' || (_1.key?(:if) && _1[:if] != true) } # rubocop:disable Style/NumberedParameters -- existing code moved as is
.pluck(:name) # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- existing code moved as is
end
def self.api_arguments
fields.filter_map do |field|
next if field.if != true
{
required: field.required?,
name: field.name.to_sym,
type: field.api_type,
desc: field.description
}
end
end
def self.instance_specific?
false
end
def self.pluck_group_id
pluck(:group_id) # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- existing code moved as is
end
def form_fields
fields.reject { _1[:api_only] == true || (_1.key?(:if) && _1[:if] != true) } # rubocop:disable Style/NumberedParameters -- existing code moved as is
end
def configurable_events
events = supported_events
# No need to disable individual triggers when there is only one
if events.count == 1
[]
else
events
end
end
def supported_events
self.class.supported_events
end
def default_test_event
self.class.default_test_event
end
def execute(data)
# implement inside child
end
def test(data)
# default implementation
result = execute(data)
{ success: result.present?, result: result }
end
# Disable test for instance-level and group-level integrations.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def testable?
project_level?
end
def project_level?
project_id.present?
end
def group_level?
group_id.present?
end
def instance_level?
instance?
end
def parent
project || group
end
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
# ActiveRecord does not provide a mechanism to track changes in serialized keys,
# so we need a specific implementation for integration properties.
# This allows to track changes to properties set with the accessor methods,
# but not direct manipulation of properties hash.
def updated_properties
@updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new
end
def reset_updated_properties
@updated_properties = nil
end
def async_execute(data)
return if ::Gitlab::SilentMode.enabled?
# Temporarily log when we return within this method to gather data for
# https://gitlab.com/gitlab-org/gitlab/-/issues/382999
unless supported_events.include?(data[:object_kind])
log_info(
'async_execute did nothing due to event not being supported',
event: data[:object_kind]
)
return
end
Integrations::ExecuteWorker.perform_async(id, data.deep_stringify_keys)
end
# override if needed
def supports_data_fields?
false
end
def chat?
category == :chat
end
def ci?
category == :ci
end
def deactivate!
update(active: false)
end
def activate!
update(active: true)
end
def toggle!
active? ? deactivate! : activate!
end
private
def self.build_help_page_url(url_path, help_text, link_text = _("Learn More"), options = {})
docs_link = ActionController::Base.helpers.link_to(
'',
Rails.application.routes.url_helpers.help_page_url(url_path, **options), # rubocop:disable Gitlab/DocumentationLinks/Link: -- existing code moved as is
target: '_blank',
rel: 'noopener noreferrer'
)
tag_pair_docs_link = tag_pair(docs_link, :link_start, :link_end)
safe_format(help_text + " %{link_start}#{link_text}%{link_end}.", tag_pair_docs_link)
end
# Ancestors sorted by hierarchy depth in bottom-top order.
def self.sorted_ancestors(scope)
if scope.root_ancestor.use_traversal_ids?
Namespace.from(scope.ancestors(hierarchy_order: :asc))
else
scope.ancestors
end
end
def validate_belongs_to_project_or_group
return unless project_level? && group_level?
errors.add(:project_id, 'The integration cannot belong to both a project and a group')
end
def validate_recipients?
activated? && !importing?
end
end
end
end
end
Integration.prepend_mod_with('Integration')

View File

@ -3,796 +3,5 @@
# To add new integration you should build a class inherited from Integration
# and implement a set of methods
class Integration < ApplicationRecord
include Sortable
include Importable
include Integrations::Loggable
include Integrations::HasDataFields
include Integrations::ResetSecretFields
include FromUnion
include EachBatch
extend SafeFormatHelper
extend ::Gitlab::Utils::Override
UnknownType = Class.new(StandardError)
self.allow_legacy_sti_class = true
self.inheritance_column = :type_new
INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker
datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki
gitlab_slack_application hangouts_chat harbor irker jira matrix
mattermost mattermost_slash_commands microsoft_teams packagist phorge pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram
unify_circuit webex_teams youtrack zentao
].freeze
# Integrations that can only be enabled on the instance-level
INSTANCE_LEVEL_ONLY_INTEGRATION_NAMES = %w[
beyond_identity
].freeze
# Integrations that can only be enabled on the project-level
PROJECT_LEVEL_ONLY_INTEGRATION_NAMES = %w[
apple_app_store google_play jenkins
].freeze
# Integrations that cannot be enabled on the instance-level
PROJECT_AND_GROUP_LEVEL_ONLY_INTEGRATION_NAMES = %w[
jira_cloud_app
].freeze
# Fake integrations to help with local development.
DEV_INTEGRATION_NAMES = %w[
mock_ci mock_monitoring
].freeze
# Base classes which aren't actual integrations.
BASE_CLASSES = %w[
Integrations::BaseChatNotification
Integrations::BaseCi
Integrations::BaseIssueTracker
Integrations::BaseMonitoring
Integrations::BaseSlackNotification
Integrations::BaseSlashCommands
Integrations::BaseThirdPartyWiki
].freeze
BASE_ATTRIBUTES = %w[id instance project_id group_id created_at updated_at
encrypted_properties encrypted_properties_iv properties].freeze
SECTION_TYPE_CONFIGURATION = 'configuration'
SECTION_TYPE_CONNECTION = 'connection'
SECTION_TYPE_TRIGGER = 'trigger'
SNOWPLOW_EVENT_ACTION = 'perform_integrations_action'
SNOWPLOW_EVENT_LABEL = 'redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly'
attr_encrypted :properties,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
marshal: true,
marshaler: ::Gitlab::Json,
encode: false,
encode_iv: false
alias_attribute :name, :title
# Handle assignment of props with symbol keys.
# To do this correctly, we need to call the method generated by attr_encrypted.
alias_method :attr_encrypted_props=, :properties=
private :attr_encrypted_props=
def properties=(props)
self.attr_encrypted_props = props&.with_indifferent_access&.freeze
end
alias_attribute :type, :type_new
attribute :active, default: false
attribute :alert_events, default: true
attribute :incident_events, default: false
attribute :category, default: 'common'
attribute :commit_events, default: true
attribute :confidential_issues_events, default: true
attribute :confidential_note_events, default: true
attribute :deployment_events, default: false
attribute :issues_events, default: true
attribute :job_events, default: true
attribute :merge_requests_events, default: true
attribute :note_events, default: true
attribute :pipeline_events, default: true
attribute :push_events, default: true
attribute :tag_push_events, default: true
attribute :wiki_page_events, default: true
attribute :group_mention_events, default: false
attribute :group_confidential_mention_events, default: false
after_initialize :initialize_properties
after_commit :reset_updated_properties
belongs_to :project, inverse_of: :integrations
belongs_to :group, inverse_of: :integrations
validates :project_id, presence: true, unless: -> { instance_level? || group_level? }
validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
validates :project_id, :group_id, absence: true, if: -> { instance_level? }
validates :type, presence: true, exclusion: BASE_CLASSES
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
scope :third_party_wikis, -> { where(category: 'third_party_wiki').active }
scope :by_name, ->(name) { by_type(integration_name_to_type(name)) }
scope :external_wikis, -> { by_name(:external_wiki).active }
scope :active, -> { where(active: true) }
scope :by_type, ->(type) { where(type: type) } # INTERNAL USE ONLY: use by_name instead
scope :by_active_flag, ->(flag) { where(active: flag) }
scope :inherit_from_id, ->(id) { where(inherit_from_id: id) }
scope :with_default_settings, -> { where.not(inherit_from_id: nil) }
scope :with_custom_settings, -> { where(inherit_from_id: nil) }
scope :for_group, ->(group) {
types = available_integration_types(include_project_specific: false)
where(group_id: group, type: types)
}
scope :for_instance, -> {
types = available_integration_types(include_project_specific: false, include_group_specific: false)
where(instance: true, type: types)
}
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :archive_trace_hooks, -> { where(archive_trace_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :incident_hooks, -> { where(incident_events: true, active: true) }
scope :deployment, -> { where(category: 'deployment') }
scope :group_mention_hooks, -> { where(group_mention_events: true, active: true) }
scope :group_confidential_mention_hooks, -> { where(group_confidential_mention_events: true, active: true) }
scope :exclusions_for_project, ->(project) { where(project: project, active: false) }
class << self
private
attr_writer :field_storage
def field_storage
@field_storage || :properties
end
end
# :nocov: Tested on subclasses.
def self.field(name, storage: field_storage, **attrs)
fields << ::Integrations::Field.new(name: name, integration_class: self, **attrs)
case storage
when :attribute
# noop
when :properties
prop_accessor(name)
when :data_fields
data_field(name)
else
raise ArgumentError, "Unknown field storage: #{storage}"
end
boolean_accessor(name) if attrs[:type] == :checkbox && storage != :attribute
end
# :nocov:
def self.fields
@fields ||= []
end
def fields
self.class.fields.dup
end
# Provide convenient accessor methods for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.prop_accessor(*args)
args.each do |arg|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
unless method_defined?(arg)
def #{arg}
properties['#{arg}'] if properties.present?
end
end
def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties = self.properties.merge('#{arg}' => value)
end
def #{arg}_changed?
#{arg}_touched? && #{arg} != #{arg}_was
end
def #{arg}_touched?
updated_properties.include?('#{arg}')
end
def #{arg}_was
updated_properties['#{arg}']
end
RUBY
end
end
# Provide convenient boolean accessor methods for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.boolean_accessor(*args)
args.each do |arg|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
# Make the original getter available as a private method.
alias_method :#{arg}_before_type_cast, :#{arg}
private(:#{arg}_before_type_cast)
def #{arg}
Gitlab::Utils.to_boolean(#{arg}_before_type_cast)
end
def #{arg}?
# '!!' is used because nil or empty string is converted to nil
!!#{arg}
end
RUBY
end
end
private_class_method :boolean_accessor
def self.title
raise NotImplementedError
end
def self.description
raise NotImplementedError
end
def self.help
# no-op
end
def self.to_param
raise NotImplementedError
end
def self.attribution_notice
# no-op
end
def self.event_names
supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
end
def self.supported_events
%w[commit push tag_push issue confidential_issue merge_request wiki_page]
end
def self.default_test_event
'push'
end
def self.event_description(event)
IntegrationsHelper.integration_event_description(event)
end
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
return unless name.in?(available_integration_names(
include_project_specific: false,
include_group_specific: group_id.present?,
include_instance_specific: instance))
integration_name_to_model(name).find_or_initialize_by(instance: instance, group_id: group_id)
end
def self.find_or_initialize_all_non_project_specific(scope, include_instance_specific: false)
scope + build_nonexistent_integrations_for(scope,
include_group_specific: !include_instance_specific,
include_instance_specific: include_instance_specific)
end
def self.build_nonexistent_integrations_for(...)
nonexistent_integration_types_for(...).map do |type|
integration_type_to_model(type).new
end
end
private_class_method :build_nonexistent_integrations_for
# Returns a list of integration types that do not exist in the given scope.
# Example: ["AsanaService", ...]
def self.nonexistent_integration_types_for(scope, include_group_specific: false, include_instance_specific: false)
# Using #map instead of #pluck to save one query count. This is because
# ActiveRecord loaded the object here, so we don't need to query again later.
available_integration_types(
include_project_specific: false,
include_group_specific: include_group_specific,
include_instance_specific: include_instance_specific
) - scope.map(&:type)
end
private_class_method :nonexistent_integration_types_for
# Returns a list of available integration names.
# Example: ["asana", ...]
def self.available_integration_names(
include_project_specific: true, include_group_specific: true, include_instance_specific: true, include_dev: true,
include_disabled: false
)
names = integration_names.dup
names.concat(project_specific_integration_names) if include_project_specific
names.concat(dev_integration_names) if include_dev
names.concat(instance_specific_integration_names) if include_instance_specific
names.concat(project_and_group_specific_integration_names) if include_project_specific || include_group_specific
names -= disabled_integration_names unless include_disabled
names.sort_by(&:downcase)
end
def self.integration_names
names = INTEGRATION_NAMES.dup
unless Feature.enabled?(:gitlab_for_slack_app_instance_and_group_level, type: :beta) &&
(Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?)
names.delete('gitlab_slack_application')
end
names
end
def self.instance_specific_integration_names
INSTANCE_LEVEL_ONLY_INTEGRATION_NAMES
end
def self.instance_specific_integration_types
instance_specific_integration_names.map { |name| integration_name_to_type(name) }
end
def self.dev_integration_names
return [] unless Gitlab.dev_or_test_env?
DEV_INTEGRATION_NAMES
end
def self.project_specific_integration_names
names = PROJECT_LEVEL_ONLY_INTEGRATION_NAMES.dup
if Feature.disabled?(:gitlab_for_slack_app_instance_and_group_level, type: :beta) &&
(Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?)
names << 'gitlab_slack_application'
end
names
end
def self.project_and_group_specific_integration_names
PROJECT_AND_GROUP_LEVEL_ONLY_INTEGRATION_NAMES.dup
end
private_class_method :project_and_group_specific_integration_names
# Returns a list of available integration types.
# Example: ["Integrations::Asana", ...]
def self.available_integration_types(...)
available_integration_names(...).map do
integration_name_to_type(_1)
end
end
# Returns a list of disabled integration names.
# Example: ["gitlab_slack_application", ...]
def self.disabled_integration_names
# The GitLab for Slack app integration is only available when enabled through settings.
# The Slack Slash Commands integration is only available for customers who cannot use the GitLab for Slack app.
disabled = Gitlab::CurrentSettings.slack_app_enabled ? ['slack_slash_commands'] : ['gitlab_slack_application']
disabled += ['jira_cloud_app'] unless Gitlab::CurrentSettings.jira_connect_application_key.present?
disabled
end
private_class_method :disabled_integration_names
# Returns the model for the given integration name.
# Example: :asana => Integrations::Asana
def self.integration_name_to_model(name)
type = integration_name_to_type(name)
integration_type_to_model(type)
end
# Returns the STI type for the given integration name.
# Example: "asana" => "Integrations::Asana"
def self.integration_name_to_type(name)
name = name.to_s
if available_integration_names(include_disabled: true).exclude?(name)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownType.new(name.inspect))
else
"Integrations::#{name.camelize}"
end
end
# Returns the model for the given STI type.
# Example: "Integrations::Asana" => Integrations::Asana
def self.integration_type_to_model(type)
type.constantize
end
private_class_method :integration_type_to_model
def self.build_from_integration(integration, project_id: nil, group_id: nil)
new_integration = integration.dup
new_integration.instance = false
new_integration.project_id = project_id
new_integration.group_id = group_id
new_integration.inherit_from_id = integration.id if integration.inheritable?
new_integration
end
# Duplicating an integration also duplicates the data fields. Duped records have different ciphertexts.
override :dup
def dup
new_integration = super
new_integration.assign_attributes(reencrypt_properties)
if supports_data_fields?
fields = data_fields.dup
fields.integration = new_integration
end
new_integration
end
def inheritable?
instance_level? || group_level?
end
def self.instance_exists_for?(type)
exists?(instance: true, type: type)
end
def self.default_integration(type, scope)
closest_group_integration(type, scope) || instance_level_integration(type)
end
def self.closest_group_integration(type, scope)
group_ids = scope.ancestors(hierarchy_order: :asc).reselect(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
.order(Arel.sql("array_position(#{array}::bigint[], #{table_name}.group_id)"))
.first
end
private_class_method :closest_group_integration
def self.instance_level_integration(type)
find_by(type: type, instance: true)
end
private_class_method :instance_level_integration
def self.default_integrations(owner, scope)
group_ids = sorted_ancestors(owner).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
order = Arel.sql("type_new ASC, array_position(#{array}::bigint[], #{table_name}.group_id), instance DESC")
from_union([scope.where(instance: true), scope.where(group_id: group_ids, inherit_from_id: nil)])
.order(order)
.group_by(&:type)
.transform_values(&:first)
end
private_class_method :default_integrations
def self.create_from_default_integrations(owner, association)
active_default_count = create_from_active_default_integrations(owner, association)
default_instance_specific_count = create_from_default_instance_specific_integrations(owner, association)
active_default_count + default_instance_specific_count
end
# Returns the number of successfully saved integrations
# Duplicate integrations are excluded from this count by their validations.
def self.create_from_active_default_integrations(owner, association)
default_integrations(
owner,
active.where.not(type: instance_specific_integration_types)
).count { |_type, integration| build_from_integration(integration, association => owner.id).save }
end
def self.create_from_default_instance_specific_integrations(owner, association)
default_integrations(
owner,
where(type: instance_specific_integration_types)
).count { |_type, integration| build_from_integration(integration, association => owner.id).save }
end
def self.descendants_from_self_or_ancestors_from(integration)
scope = where(type: integration.type)
from_union([
scope.where(group: integration.group.descendants),
scope.where(project: Project.in_namespace(integration.group.self_and_descendants))
])
end
def self.inherited_descendants_from_self_or_ancestors_from(integration)
inherit_from_ids =
where(type: integration.type, group: integration.group.self_and_ancestors)
.or(where(type: integration.type, instance: true)).select(:id)
from_union([
where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
where(type: integration.type, inherit_from_id: inherit_from_ids,
project: Project.in_namespace(integration.group.self_and_descendants))
])
end
def activated?
active
end
def operating?
active && persisted?
end
def manual_activation?
true
end
def editable?
true
end
def activate_disabled_reason
nil
end
def category
read_attribute(:category).to_sym
end
def initialize_properties
self.properties = {} if has_attribute?(:encrypted_properties) && encrypted_properties.nil?
end
def title
self.class.title
end
def description
self.class.description
end
def help
self.class.help
end
def to_param
self.class.to_param
end
def attribution_notice
self.class.attribution_notice
end
def sections
[]
end
def secret_fields
fields.select(&:secret?).pluck(:name)
end
# Expose a list of fields in the JSON endpoint.
#
# This list is used in `Integration#as_json(only: json_fields)`.
def json_fields
%w[active]
end
# properties is always nil - ignore it.
override :attributes
def attributes
super.except('properties')
end
# Returns a hash of attributes (columns => values) used for inserting into the database.
def to_database_hash
column = self.class.attribute_aliases.fetch('type', 'type')
attributes_for_database.except(*BASE_ATTRIBUTES)
.merge(column => type)
.merge(reencrypt_properties)
end
def reencrypt_properties
unless properties.nil? || properties.empty?
alg = self.class.attr_encrypted_attributes[:properties][:algorithm]
iv = generate_iv(alg)
ep = self.class.attr_encrypt(:properties, properties, { iv: iv })
end
{ 'encrypted_properties' => ep, 'encrypted_properties_iv' => iv }
end
def event_channel_names
[]
end
def event_names
self.class.event_names
end
def api_field_names
fields.reject { _1[:type] == :password || _1[:name] == 'webhook' || (_1.key?(:if) && _1[:if] != true) }.pluck(:name)
end
def self.api_arguments
fields.filter_map do |field|
next if field.if != true
{
required: field.required?,
name: field.name.to_sym,
type: field.api_type,
desc: field.description
}
end
end
def self.instance_specific?
false
end
def self.pluck_group_id
pluck(:group_id)
end
def form_fields
fields.reject { _1[:api_only] == true || (_1.key?(:if) && _1[:if] != true) }
end
def configurable_events
events = supported_events
# No need to disable individual triggers when there is only one
if events.count == 1
[]
else
events
end
end
def supported_events
self.class.supported_events
end
def default_test_event
self.class.default_test_event
end
def execute(data)
# implement inside child
end
def test(data)
# default implementation
result = execute(data)
{ success: result.present?, result: result }
end
# Disable test for instance-level and group-level integrations.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def testable?
project_level?
end
def project_level?
project_id.present?
end
def group_level?
group_id.present?
end
def instance_level?
instance?
end
def parent
project || group
end
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
# ActiveRecord does not provide a mechanism to track changes in serialized keys,
# so we need a specific implementation for integration properties.
# This allows to track changes to properties set with the accessor methods,
# but not direct manipulation of properties hash.
def updated_properties
@updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new
end
def reset_updated_properties
@updated_properties = nil
end
def async_execute(data)
return if ::Gitlab::SilentMode.enabled?
# Temporarily log when we return within this method to gather data for
# https://gitlab.com/gitlab-org/gitlab/-/issues/382999
unless supported_events.include?(data[:object_kind])
log_info(
'async_execute did nothing due to event not being supported',
event: data[:object_kind]
)
return
end
Integrations::ExecuteWorker.perform_async(id, data.deep_stringify_keys)
end
# override if needed
def supports_data_fields?
false
end
def chat?
category == :chat
end
def ci?
category == :ci
end
def deactivate!
update(active: false)
end
def activate!
update(active: true)
end
def toggle!
active? ? deactivate! : activate!
end
private
def self.build_help_page_url(url_path, help_text, link_text = _("Learn More"), options = {})
docs_link = ActionController::Base.helpers.link_to(
'',
Rails.application.routes.url_helpers.help_page_url(url_path, **options),
target: '_blank',
rel: 'noopener noreferrer'
)
tag_pair_docs_link = tag_pair(docs_link, :link_start, :link_end)
safe_format(help_text + " %{link_start}#{link_text}%{link_end}.", tag_pair_docs_link)
end
# Ancestors sorted by hierarchy depth in bottom-top order.
def self.sorted_ancestors(scope)
if scope.root_ancestor.use_traversal_ids?
Namespace.from(scope.ancestors(hierarchy_order: :asc))
else
scope.ancestors
end
end
def validate_belongs_to_project_or_group
return unless project_level? && group_level?
errors.add(:project_id, 'The integration cannot belong to both a project and a group')
end
def validate_recipients?
activated? && !importing?
end
include Integrations::Base::Integration
end
Integration.prepend_mod_with('Integration')

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Integrations
module Instance
class Integration < ApplicationRecord
include IgnorableColumns
include Integrations::Base::Integration
self.table_name = 'instance_integrations'
self.inheritance_column = :type_new # rubocop:disable Database/AvoidInheritanceColumn -- supporting instance integrations migration
ignore_column :type, remove_with: '17.7', remove_after: '2024-12-02'
def instance_level?
true
end
def group_level?
false
end
def project_level?
false
end
end
end
end

View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
module Integrations
class InstanceIntegration < Integration
include IgnorableColumns
self.table_name = 'instance_integrations'
self.inheritance_column = :type_new # rubocop:disable Database/AvoidInheritanceColumn -- supporting instance integrations migration
ignore_column :type, remove_with: '17.7', remove_after: '2024-12-02'
end
end

View File

@ -29,6 +29,7 @@ class User < ApplicationRecord
include RestrictedSignup
include StripAttribute
include EachBatch
include IgnorableColumns
include CrossDatabaseIgnoredTables
include UseSqlFunctionForPrimaryKeyLookups
@ -45,6 +46,8 @@ class User < ApplicationRecord
on: :destroy
)
ignore_column :last_access_from_pipl_country_at, remove_after: '2024-11-17', remove_with: '17.7'
DEFAULT_NOTIFICATION_LEVEL = :participating
INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@ -1305,6 +1308,16 @@ class User < ApplicationRecord
end
end
# Used to search on the user's authorized_groups effeciently by using a CTE
def search_on_authorized_groups(query, use_minimum_char_limit: true)
authorized_groups_cte = Gitlab::SQL::CTE.new(:authorized_groups, authorized_groups)
authorized_groups_cte_alias = authorized_groups_cte.table.alias(Group.table_name)
Group
.with(authorized_groups_cte.to_arel)
.from(authorized_groups_cte_alias)
.search(query, use_minimum_char_limit: use_minimum_char_limit)
end
# Returns the groups a user is a member of, either directly or through a parent group
def membership_groups
groups.self_and_descendants

View File

@ -15,6 +15,7 @@ module Users
def noteable_owner
return [] unless noteable && noteable.author.present?
return [] if noteable.author.placeholder? || noteable.author.import_user?
[noteable.author].tap do |users|
preload_status(users)
@ -25,6 +26,7 @@ module Users
return [] unless noteable
users = noteable.participants(current_user)
users = users.reject { |user| user.placeholder? || user.import_user? }
sorted(users)
end

View File

@ -21,6 +21,7 @@
.alert-wrapper.alert-wrapper-top-space.gl-flex.gl-flex-col.gl-gap-3.container-fluid{ class: alert_class }
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold_block_seat_overages"
= dispensable_render_if_exists "layouts/header/token_expiry_notification"
-# This is needed by [GitLab JH](https://gitlab.com/gitlab-jh/jh-team/gitlab/-/issues/13)
= dispensable_render_if_exists "layouts/header/account_notification"

View File

@ -0,0 +1,9 @@
---
name: autocomplete_group_search_optimization
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472011
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170948
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/501202
milestone: '17.6'
group: group::global search
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
load_license = ->(dir:, license_name:) do
begin
public_key_file = File.read(Rails.root.join(dir, ".license_encryption_key.pub"))
public_key = OpenSSL::PKey::RSA.new(public_key_file)
Gitlab::License.encryption_key = public_key
rescue StandardError
warn "WARNING: No valid #{license_name} encryption key provided."
end
begin
if Rails.env.development? || Rails.env.test? || ENV['GITLAB_LICENSE_MODE'] == 'test'
fallback_key_file = File.read(Rails.root.join(dir, ".test_license_encryption_key.pub"))
fallback_key = OpenSSL::PKey::RSA.new(fallback_key_file)
Gitlab::License.fallback_decryption_keys = [fallback_key]
end
rescue StandardError
warn "WARNING: No fallback #{license_name} decryption key provided."
end
end
Gitlab.ee do
load_license.call(dir: '.', license_name: 'license')
end
Gitlab.jh do
load_license.call(dir: 'jh', license_name: 'JH license')
end

View File

@ -233,6 +233,10 @@
- 1
- - compliance_management_standards_gitlab_at_least_two_approvals_group
- 1
- - compliance_management_standards_gitlab_dast
- 1
- - compliance_management_standards_gitlab_dast_group
- 1
- - compliance_management_standards_gitlab_prevent_approval_by_author
- 1
- - compliance_management_standards_gitlab_prevent_approval_by_author_group

View File

@ -1,7 +1,7 @@
---
table_name: instance_integrations
classes:
- Integrations::InstanceIntegration
- Integrations::Instance::Integration
feature_categories:
- integrations
description: Support 3rd party instance-wide integrations

11
db/docs/pipl_users.yml Normal file
View File

@ -0,0 +1,11 @@
---
table_name: pipl_users
classes:
- ComplianceManagement::PiplUser
feature_categories:
- compliance_management
description: Stores user accesses and notifications from PIPL countries
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169769
milestone: '17.6'
gitlab_schema: gitlab_main_cell
exempt_from_sharding: true # Not related to projects or groups

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class CreatePiplUsersTable < Gitlab::Database::Migration[2.2]
milestone '17.6'
def up
create_table :pipl_users, id: false do |t|
t.references :user, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
t.timestamps_with_timezone null: false
t.datetime_with_timezone :initial_email_sent_at, index: true
t.datetime_with_timezone :last_access_from_pipl_country_at, null: false
end
end
def down
drop_table :pipl_users
end
end

View File

@ -0,0 +1 @@
a90f86f8f7ccc8bd9d1032a0ae161a413b6a4d6f2bd85e4198b224dca8483a68

View File

@ -16584,6 +16584,14 @@ CREATE SEQUENCE personal_access_tokens_id_seq
ALTER SEQUENCE personal_access_tokens_id_seq OWNED BY personal_access_tokens.id;
CREATE TABLE pipl_users (
user_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
initial_email_sent_at timestamp with time zone,
last_access_from_pipl_country_at timestamp with time zone NOT NULL
);
CREATE TABLE plan_limits (
id bigint NOT NULL,
plan_id bigint NOT NULL,
@ -25820,6 +25828,9 @@ ALTER TABLE ONLY path_locks
ALTER TABLE ONLY personal_access_tokens
ADD CONSTRAINT personal_access_tokens_pkey PRIMARY KEY (id);
ALTER TABLE ONLY pipl_users
ADD CONSTRAINT pipl_users_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY plan_limits
ADD CONSTRAINT plan_limits_pkey PRIMARY KEY (id);
@ -31072,6 +31083,8 @@ CREATE UNIQUE INDEX p_ci_pipeline_variables_pipeline_id_key_partition_id_idx ON
CREATE UNIQUE INDEX index_pipeline_variables_on_pipeline_id_key_partition_id_unique ON ci_pipeline_variables USING btree (pipeline_id, key, partition_id);
CREATE INDEX index_pipl_users_on_initial_email_sent_at ON pipl_users USING btree (initial_email_sent_at);
CREATE UNIQUE INDEX index_plan_limits_on_plan_id ON plan_limits USING btree (plan_id);
CREATE UNIQUE INDEX index_plans_on_name ON plans USING btree (name);
@ -37798,6 +37811,9 @@ ALTER TABLE p_ci_build_trace_metadata
ALTER TABLE ONLY bulk_import_trackers
ADD CONSTRAINT fk_rails_aed566d3f3 FOREIGN KEY (bulk_import_entity_id) REFERENCES bulk_import_entities(id) ON DELETE CASCADE;
ALTER TABLE ONLY pipl_users
ADD CONSTRAINT fk_rails_af0eb8b205 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY pool_repositories
ADD CONSTRAINT fk_rails_af3f8c5d62 FOREIGN KEY (shard_id) REFERENCES shards(id) ON DELETE RESTRICT;

View File

@ -16,16 +16,15 @@ This page explains how an administrator can configure issue closing patterns.
For user documentation about the feature, see
[Closing issues automatically](../user/project/issues/managing_issues.md#closing-issues-automatically).
When a commit or merge request resolves one or more issues, it is possible to
automatically close these issues when the commit or merge request lands
in the project's default branch.
When a commit or merge request resolves one or more issues, GitLab can close those issues when the
commit or merge request lands in the project's default branch.
## Change the issue closing pattern
The [default issue closing pattern](../user/project/issues/managing_issues.md#default-closing-pattern)
covers a wide range of words. You can change the pattern to suit your needs.
covers a wide range of words.
To change the default issue closing pattern:
To change the default issue closing pattern to suit your needs:
::Tabs
@ -108,6 +107,6 @@ To change the default issue closing pattern:
::EndTabs
To test the issue closing pattern, use <https://rubular.com>.
However, Rubular doesn't understand `%{issue_ref}`. When testing your patterns,
To test the issue closing pattern, use [Rubular](https://rubular.com).
Rubular does not understand `%{issue_ref}`. When you test your patterns,
replace this string with `#\d+`, which matches only local issue references like `#123`.

View File

@ -37582,6 +37582,7 @@ Name of the check for the compliance standard.
| ----- | ----------- |
| <a id="compliancestandardsadherencechecknameat_least_one_non_author_approval"></a>`AT_LEAST_ONE_NON_AUTHOR_APPROVAL` | At least one non author approval. |
| <a id="compliancestandardsadherencechecknameat_least_two_approvals"></a>`AT_LEAST_TWO_APPROVALS` | At least two approvals. |
| <a id="compliancestandardsadherencechecknamedast"></a>`DAST` | Dast. |
| <a id="compliancestandardsadherencechecknameprevent_approval_by_merge_request_author"></a>`PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR` | Prevent approval by merge request author. |
| <a id="compliancestandardsadherencechecknameprevent_approval_by_merge_request_committers"></a>`PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS` | Prevent approval by merge request committers. |
| <a id="compliancestandardsadherencechecknamesast"></a>`SAST` | Sast. |

View File

@ -46,8 +46,8 @@ specific to the worker in the shared examples block.
```ruby
it_behaves_like 'an idempotent worker' do
it 'checks the side-effects for multiple calls' do
# `subject` will call the job's perform method 2 times
subject
# `perform_idempotent_work` will call the job's perform method 2 times
perform_idempotent_work
expect(model.state).to eq('state')
end

View File

@ -158,7 +158,8 @@ Billable users count toward the number of subscription seats purchased in your s
A user is not counted as a billable user if:
- They are pending approval.
- They have the [Guest role on an Ultimate subscription](#free-guest-users).
- They have only the [Guest role on an Ultimate subscription](#free-guest-users).
- They have only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) for any GitLab.com subscriptions.
- They are a [banned member](../../user/group/moderate_users.md#ban-a-user).
- They are a [blocked user](../../administration/moderate_users.md#block-a-user).
- The account is a GitLab-created service account:

View File

@ -51,8 +51,8 @@ A user is not counted as a billable user if:
- They are [deactivated](../../administration/moderate_users.md#deactivate-a-user) or
[blocked](../../administration/moderate_users.md#block-a-user).
- They are [pending approval](../../administration/moderate_users.md#users-pending-approval).
- They have only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) on self-managed Ultimate subscriptions or any GitLab.com subscriptions.
- They have only the [Guest or Minimal Access roles on an Ultimate subscription](#free-guest-users).
- They have only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) on self-managed Ultimate subscriptions.
- They have only the [Guest role on an Ultimate subscription](#free-guest-users).
- They do not have project or group memberships on an Ultimate subscription.
- The account is a GitLab-created account:
- [Ghost User](../../user/profile/account/delete_account.md#associated-records).

View File

@ -18,16 +18,16 @@ A single feature can span several merge requests, spread out across multiple pro
and the order in which the work merges can be significant. Use merge request dependencies
when it's important to merge work in a specific order. Some examples:
- Ensure changes to a required library are merged before changes to a project that
- Ensure changes to a required library merge before changes to a project that
imports the library.
- Prevent a documentation-only merge request from merging before the feature work
is itself merged.
- Require a merge request updating a permissions matrix to merge, before merging work
from someone who hasn't yet been granted permissions.
from someone who doesn't yet have the correct permissions.
If your project `me/myexample` imports a library from `myfriend/library`,
you might want to update your project to use a new feature in `myfriend/library`.
However, if you merge changes to your project before the external library adds the
you should update your project when `myfriend/library` releases a new feature.
If you merge your changes to `me/myexample` before `myfriend/library` adds the
new feature, you would break the default branch in your project. A merge request
dependency prevents your work from merging too soon:
@ -47,23 +47,23 @@ graph TB
C-.->|blocks| D
```
You could mark your `me/myexample` merge request as a [draft](drafts.md)
and explain why in the comments. However, this approach is manual and does not scale, especially
if your merge request relies on several others in multiple projects. Instead,
use the draft (or ready) state to track the readiness of an individual
merge request, and a merge request dependency to enforce merge order.
It's possible to mark your `me/myexample` merge request as a [draft](drafts.md)
and explain why in the comments. This approach is manual and does not scale, especially
if your merge request relies on several others in different projects. Instead, you should:
NOTE:
Merge request dependencies are a **PREMIUM** feature, but this restriction is
enforced only for the dependent merge request. A merge request in a **PREMIUM**
project can depend on a merge request in a **FREE** project, but a merge request
in a **FREE** project cannot be marked as dependent.
- Track the readiness of an individual merge request with **Draft** or **Ready** status.
- Enforce the order merge requests merge with a merge request dependency.
Merge request dependencies are a **PREMIUM** feature, but GitLab enforces this restriction
only for the *dependent* merge request:
- A **PREMIUM** project's merge request can depend on any other merge request, even in a **FREE** project.
- A **FREE** project's merge request cannot depend on other merge requests.
## Nested dependencies
Indirect, nested dependencies are supported in GitLab 16.7 and later.
A single merge request can be blocked by up to 10 merge requests, and,
in turn, can block up to 10 merge requests. In this example, `myfriend/library!10`
GitLab versions 16.7 and later support indirect, nested dependencies. A merge request can have up to 10 blockers,
and in turn it can block up to 10 other merge requests. In this example, `myfriend/library!10`
depends on `herfriend/another-lib!1`, which in turn depends on `mycorp/example!100`:
```mermaid
@ -76,12 +76,11 @@ graph LR;
B-->|depends on| C[mycorp/example!100]
```
Nested dependencies do not display in the GitLab UI, but UI support has
been proposed in [epic 5308](https://gitlab.com/groups/gitlab-org/-/epics/5308).
Nested dependencies do not display in the GitLab UI, but UI support is
proposed in [epic 5308](https://gitlab.com/groups/gitlab-org/-/epics/5308).
NOTE:
A merge request can't be made dependent on itself (self-referential), but
it's possible to create circular dependencies.
A merge request cannot depend on itself (self-referential), but it's possible to create circular dependencies.
## View dependencies for a merge request
@ -100,26 +99,23 @@ To view dependency information on a merge request:
1. Select **Expand** to view the title, milestone, assignee, and pipeline status
of each dependency.
Until your merge request's dependencies all merge, your merge request
cannot be merged. The message
Until your merge request's dependencies all merge, your merge request cannot merge. The message
**Merge blocked: you can only merge after the above items are resolved** displays.
### Closed merge requests
Closed merge requests still prevent their dependents from being merged, because
a merge request can close regardless of whether or not the planned work actually merged.
If a merge request closes and the dependency is no longer relevant,
Closed merge requests still prevent their dependents from merging, because a merge request can close
without merging its planned work. If a merge request closes and the dependency is no longer relevant,
remove it as a dependency to unblock the dependent merge request.
## Create a new dependent merge request
When you create a new merge request, you can prevent it from merging until after
other specific work merges, even if the merge request is in a different project.
other specific work merges. This dependency works even if the merge request is in a different project.
Prerequisites:
- You must have at least the Developer role or be allowed to create merge requests in the project.
- You must have at least the Developer role, or have permission to create merge requests in the project.
- The dependent merge request must be in a project in the Premium or Ultimate tier.
To create a new merge request and mark it as dependent on another:
@ -136,7 +132,7 @@ You can edit an existing merge request and mark it as dependent on another.
Prerequisites:
- You must have at least the Developer role or be allowed to edit merge requests in the project.
- You must have at least the Developer role or have permission to edit merge requests in the project.
To do this:
@ -162,13 +158,13 @@ Prerequisites:
for each dependency you want to remove.
NOTE:
Dependencies for merge requests you don't have access to are displayed as
**1 inaccessible merge request**, and can be removed the same way.
Merge request dependencies you do not have permission to view are shown as
**1 inaccessible merge request**. You can still remove the dependency.
1. Select **Save changes**.
## Troubleshooting
### Preserving dependencies on project import or export
### Preserve dependencies on project import or export
Dependencies are not preserved when projects are imported or exported. For more
information, read [issue #12549](https://gitlab.com/gitlab-org/gitlab/-/issues/12549).
Dependencies are not preserved when you import or export a project. For more
information, see [issue #12549](https://gitlab.com/gitlab-org/gitlab/-/issues/12549).

View File

@ -25,9 +25,9 @@ To delete a merge request:
1. Select **Edit**.
1. Scroll to the bottom of the page, and select **Delete merge request**.
## Bulk edit merge requests at the project level
## Bulk edit merge requests in a project
You can update these attributes on multiple merge requests at the same time:
These attributes are editable when bulk editing merge requests:
- Status (open/closed)
- Assignee
@ -48,13 +48,13 @@ To do this:
1. Select the appropriate fields and their values from the sidebar.
1. Select **Update selected**.
## Bulk edit merge requests at the group level
## Bulk edit merge requests in a group
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
When bulk editing merge requests in a group, you can edit the following attributes:
These attributes are editable when you bulk edit merge requests for a group:
- Milestone
- Labels

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -37,8 +37,7 @@ To do this:
1. Select **Code > Merge requests** and find your merge request.
1. Select the title of the merge request to view it.
1. Scroll to the [merge request widget](../widgets.md) to see the mergeability and
approval status for the merge request. For example, this merge request is blocked
because it hasn't received the approvals it needs:
approval status for the merge request. For example, the lack of required approvals blocks this merge request:
![The merge request widget displays 'All required approvals must be given'.](img/reviews_missing_v17_3.png)
@ -134,7 +133,7 @@ To resolve or unresolve a thread when replying to a comment:
1. Select or clear **Resolve thread**.
1. Select **Add comment now** or **Add to review**.
Pending comments display information about the actions that are delayed until comment is published:
Pending comments display information about delayed actions. GitLab does not perform these actions until you publish the comment:
- **{check-circle-filled}** Thread is resolved.
- **{check-circle}** Thread stays unresolved.
@ -179,9 +178,8 @@ the reviewer who requested changes should [re-review and approve](#re-request-a-
### Bypass a request for changes
If the user who requested changes is unable to re-review or provide an approval,
another user with permission to merge the merge request can override this check in the
merge request reports area by selecting **Bypass**:
If the user who requested changes is unavailable to re-review or approve,
another user with permission to merge the merge request can override this check:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Merge requests** and find your merge request.
@ -191,8 +189,8 @@ merge request reports area by selecting **Bypass**:
![A merge request that is blocked because a user requested changes](img/bypass_v17_2.png)
1. The merge reports area shows `Merge with caution: Override added`. To see which check
was bypassed, select **Expand merge checks** (**{chevron-lg-down}**) and find the
1. The merge reports area shows `Merge with caution: Override added`. To see which check a user
bypassed, select **Expand merge checks** (**{chevron-lg-down}**) and find the
check that contains a warning (**{status_warning}**) icon. In this example, the
author bypassed **The change requests must be completed or resolved**:
@ -209,7 +207,7 @@ subject matter experts for the changes you're making. To decrease the number of
review cycles for your merge request, consider requesting reviews from users
listed in the project's approval rules.
When you edit the **Reviewers** field in a new or existing merge request, GitLab shows you
When you edit the **Reviewers** field in a merge request, GitLab shows you
the matching [approval rule](../approvals/rules.md) below the name of each reviewer.
[Code Owners](../../codeowners/index.md) display as `Codeowner` without any group detail.
@ -313,7 +311,7 @@ For more information, see [Data usage in Suggested Reviewers](data_usage.md).
Enabling Suggested Reviewers triggers GitLab to create the machine learning model your
project uses to generate reviewers. The larger your project, the longer
this process can take. Usually, the model is ready to generate suggestions
this process can take. The model is usually ready to generate suggestions
after a few hours.
Prerequisites:

View File

@ -24,7 +24,7 @@ merge request, authored by the user who suggested the changes.
1. Find the lines of code you want to change.
- To select a single line, hover over the line number and
select **Add a comment to this line** (**{comment}**).
- To select multiple lines:
- To select more lines:
1. Hover over the line number, and select **Add a comment to this line** (**{comment}**):
![Comment on any diff file line](img/comment_on_any_diff_line_v16_6.png)
1. Select and drag your selection to include all desired lines. To
@ -73,13 +73,13 @@ When applied, the suggestion replaces from 2 lines above to 2 lines below the co
![Multi-line suggestion preview](img/multi-line-suggestion-preview_v16_6.png)
Suggestions for multiple lines are limited to 100 lines _above_ and 100
GitLab limits multi-line suggestions to 100 lines _above_ and 100
lines _below_ the commented diff line. This allows for up to 201 changed lines per
suggestion.
Multiline comments display the comment's line numbers above the body of the comment:
![Multiline comment selection displayed above comment](img/multiline-comment-saved_v13_3.png)
![Multiline comment selection displayed above comment](img/multiline-comment-saved_v17_5.png)
#### Using the rich text editor
@ -87,8 +87,7 @@ Multiline comments display the comment's line numbers above the body of the comm
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/375172) in GitLab 16.2.
> - Feature flag `content_editor_on_issues` removed in GitLab 16.5.
When you insert suggestions, you can use the WYSIWYG
[rich text editor](../../../rich_text_editor.md) to move
When you insert suggestions, use the WYSIWYG [rich text editor](../../../rich_text_editor.md) to move
up and down the source file's line numbers in the UI.
To add or subtract changed lines, next to **From line**, select **+** or **-**.

View File

@ -31,7 +31,7 @@ If an application is successfully deployed to an
NOTE:
When the pipeline fails in a merge request but it can still merge,
GitLab shows the **Merge** button in red.
GitLab shows **Merge** in red.
## Post-merge pipeline status

View File

@ -31,7 +31,7 @@ module Gitlab
)
Rack::Response.new(e.message, 403).finish
rescue Gitlab::Auth::MissingPersonalAccessTokenError
Rack::Response.new('', 401).finish
not_found_response
end
private

View File

@ -64271,6 +64271,9 @@ msgid_plural "Your free group is now limited to %d members"
msgstr[0] ""
msgstr[1] ""
msgid "Your instance has %{remaining_user_count} users remaining of the %{total_user_count} in your subscription. When there are no more seats, users cannot be invited or added to the instance."
msgstr ""
msgid "Your instance has %{remaining_user_count} users remaining of the %{total_user_count} included in your subscription. You can add more users than the number included in your license, and we will include the overage in your next bill."
msgstr ""

View File

@ -80,7 +80,7 @@
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-4",
"@rails/ujs": "7.0.8-4",
"@sentry/browser": "8.35.0",
"@sentry/browser": "8.36.0",
"@snowplow/browser-plugin-client-hints": "^3.24.2",
"@snowplow/browser-plugin-form-tracking": "^3.24.2",
"@snowplow/browser-plugin-ga-cookies": "^3.24.2",

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :instance_integration, class: 'Integrations::Instance::Integration' do
type { 'Integrations::Instance::Integration' }
end
end

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
FactoryBot.define do
factory :instance_integration, class: 'Integrations::InstanceIntegration' do
type { 'Integrations::InstanceIntegration' }
end
end

View File

@ -9,6 +9,8 @@ RSpec.describe Autocomplete::UsersFinder do
describe '#execute' do
let_it_be(:user1) { create(:user, name: 'zzzzzname', username: 'johndoe') }
let_it_be(:blocked_user) { create(:user, :blocked, username: 'blocked_user') }
let_it_be(:import_user) { create(:user, :import_user, username: 'import_user') }
let_it_be(:placeholder_user) { create(:user, :placeholder, username: 'placeholder_user') }
let_it_be(:banned_user) { create(:user, :banned, username: 'banned_user') }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
@ -50,6 +52,18 @@ RSpec.describe Autocomplete::UsersFinder do
it { is_expected.to match_array([project.first_owner]) }
end
context 'and author is a placeholder user' do
let(:params) { { author_id: placeholder_user.id } }
it { is_expected.to match_array([project.first_owner]) }
end
context 'and author is a import_user' do
let(:params) { { author_id: import_user.id } }
it { is_expected.to match_array([project.first_owner]) }
end
end
context 'searching with less than 3 characters' do

File diff suppressed because it is too large Load Diff

View File

@ -536,6 +536,71 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
end
describe 'groups_autocomplete' do
let_it_be(:user) { create(:user) }
let_it_be(:group_1) { create(:group, name: 'test 1') }
let_it_be(:group_2) { create(:group, name: 'test 2') }
let(:search_term) { 'test' }
before do
allow(self).to receive(:current_user).and_return(user)
end
context 'when the user does not have access to groups' do
it 'does not return any results' do
expect(groups_autocomplete(search_term)).to eq([])
end
end
context 'when the user has access to one group' do
before do
group_2.add_developer(user)
end
it 'returns the group' do
expect(groups_autocomplete(search_term).pluck(:id)).to eq([group_2.id])
end
context 'when the search term is Gitlab::Search::Params::MIN_TERM_LENGTH characters long' do
let(:search_term) { 'te' }
it 'returns the group' do
expect(groups_autocomplete(search_term).pluck(:id)).to eq([group_2.id])
end
end
end
context 'with feature flag autocomplete_group_search_optimization disabled' do
before do
stub_feature_flags(autocomplete_group_search_optimization: false)
end
context 'when the user does not have access to groups' do
it 'does not return any results' do
expect(groups_autocomplete(search_term)).to eq([])
end
end
context 'when the user has access to one group' do
before do
group_2.add_developer(user)
end
it 'returns the group' do
expect(groups_autocomplete(search_term).pluck(:id)).to eq([group_2.id])
end
context 'when the search term is Gitlab::Search::Params::MIN_TERM_LENGTH characters long' do
let(:search_term) { 'te' }
it 'returns the group' do
expect(groups_autocomplete(search_term).pluck(:id)).to eq([group_2.id])
end
end
end
end
end
describe 'projects_autocomplete' do
let_it_be(:user) { create(:user) }
let_it_be(:project_1) { create(:project, name: 'test 1') }

View File

@ -205,13 +205,10 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(current_user.username, 'dummy_password')
end
it 'returns unauthorized' do
it 'returns 404' do
expect(Gitlab::Auth).to receive(:find_for_git_client).and_raise(Gitlab::Auth::MissingPersonalAccessTokenError)
response = go
expect(response[0]).to eq(401)
expect(response[1]['Content-Length']).to be_nil
expect(response[2]).to eq([''])
expect_404_response(go)
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Integrations::Instance::Integration, feature_category: :integrations do
subject(:instance_integration) { build(:instance_integration) }
describe '#instance_level?' do
it 'returns true' do
expect(instance_integration.instance_level?).to be(true)
end
end
describe '#group_level?' do
it 'returns false' do
expect(instance_integration.group_level?).to be(false)
end
end
describe '#project_level?' do
it 'returns false' do
expect(instance_integration.project_level?).to be(false)
end
end
describe '.table_name' do
it 'returns instance_integrations' do
expect(described_class.table_name).to eq('instance_integrations')
end
end
end

View File

@ -5025,6 +5025,59 @@ RSpec.describe User, feature_category: :user_profile do
end
end
describe '#search_on_authorized_groups' do
let_it_be(:user) { create(:user) }
let_it_be(:group_1) { create(:group, name: 'test', path: 'blah') }
let_it_be(:group_2) { create(:group, name: 'blah', path: 'test') }
let(:search_term) { 'test' }
subject { user.search_on_authorized_groups(search_term) }
context 'when the user does not have any authorized groups' do
before do
allow(user).to receive(:authorized_groups).and_return(Group.none)
end
it 'does not return anything' do
expect(subject).to be_empty
end
end
context 'when the user has two authorized groups with name or path matching the search term' do
before do
allow(user).to receive(:authorized_groups).and_return(Group.id_in([group_1.id, group_2.id]))
end
it 'returns the groups' do
expect(subject).to match_array([group_1, group_2])
end
context 'if the search term does not match on name or path' do
let(:search_term) { 'unknown' }
it 'does not return anything' do
expect(subject).to be_empty
end
end
context 'if the search term is less than MIN_CHARS_FOR_PARTIAL_MATCHING' do
let(:search_term) { 'te' }
it 'does not return anything' do
expect(subject).to be_empty
end
context 'if use_minimum_char_limit is false' do
subject { user.search_on_authorized_groups(search_term, use_minimum_char_limit: false) }
it 'returns the groups' do
expect(subject).to match_array([group_1, group_2])
end
end
end
end
end
describe '#membership_groups' do
let_it_be(:user) { create(:user) }

View File

@ -162,7 +162,7 @@ RSpec.describe Server, feature_category: :service_ping do
}
end
it 'successfully parses event' do
it 'successfully parses event', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/498773' do
expect(response.code).to eq('200')
expect(events).to contain_exactly(expected_event)
end
@ -206,7 +206,7 @@ RSpec.describe Server, feature_category: :service_ping do
await { Net::HTTP.get url_for("/i?#{query_params}") }
end
it 'successfully returns tracked events' do
it 'successfully returns tracked events', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/498778' do
expect(response.code).to eq('200')
expect(response.body).to eq([{
event: {

View File

@ -73,6 +73,20 @@ RSpec.describe Projects::ParticipantsService, feature_category: :groups_and_proj
expect(participants.count { |p| p[:username] == noteable.author.username }).to eq 1
end
context 'when noteable.participants contains placeholder or import users' do
let(:placeholder_user) { create(:user, :placeholder) }
let(:import_user) { create(:user, :import_user) }
it 'does not return the placeholder and import users' do
allow(noteable).to receive(:participants).and_return([user, placeholder_user, import_user])
participant_usernames = run_service.map { |user| user[:username] }
expect(participant_usernames).not_to include(placeholder_user.username, import_user.username)
expect(participant_usernames).to include(user.username)
end
end
describe 'group items' do
subject(:group_items) { run_service.select { |hash| hash[:type].eql?('Group') } }

View File

@ -8,7 +8,7 @@
# it_behaves_like 'an idempotent worker' do
# it 'checks the side-effects for multiple calls' do
# # it'll call the job's perform method 2 times
# subject
# perform_idempotent_work
#
# expect(model.state).to eq('state')
# end
@ -31,7 +31,7 @@ RSpec.shared_examples 'an idempotent worker' do
end
it 'performs multiple times sequentially without raising an exception' do
expect { subject }.not_to raise_error
expect { perform_idempotent_work }.not_to raise_error
end
def event_worker

112
yarn.lock
View File

@ -2466,76 +2466,76 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@sentry-internal/browser-utils@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.35.0.tgz#92602f8dd2bb777af2994eb446cb3cf71bf0cfad"
integrity sha512-uj9nwERm7HIS13f/Q52hF/NUS5Al8Ma6jkgpfYGeppYvU0uSjPkwMogtqoJQNbOoZg973tV8qUScbcWY616wNA==
"@sentry-internal/browser-utils@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.36.0.tgz#c6a1b6d7eb2ccfc2f9c189504d94b3996367ccbf"
integrity sha512-AVJ9GmQW7jYxaal6hjQnnktsDNype01ajVC4q1RyOn1SfzSnXg6mXwj4xm4ovuJV+aBI7fAZJ55vEX5ASuP0ZA==
dependencies:
"@sentry/core" "8.35.0"
"@sentry/types" "8.35.0"
"@sentry/utils" "8.35.0"
"@sentry/core" "8.36.0"
"@sentry/types" "8.36.0"
"@sentry/utils" "8.36.0"
"@sentry-internal/feedback@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.35.0.tgz#b31fb7fbec8ecd9cc683948a0d1af2b87731b0a1"
integrity sha512-7bjSaUhL0bDArozre6EiIhhdWdT/1AWNWBC1Wc5w1IxEi5xF7nvF/FfvjQYrONQzZAI3HRxc45J2qhLUzHBmoQ==
"@sentry-internal/feedback@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.36.0.tgz#2cda799f921543522aa5cbf8f9c3300209b54825"
integrity sha512-aAMTm3uDBj8Ta7FwoohpLmJOpWzpWXvvtTbtmSgkeCtPJLUS8DZDCTZ9uCILUkpuYrv2savRUHsdPkxNjgL8FA==
dependencies:
"@sentry/core" "8.35.0"
"@sentry/types" "8.35.0"
"@sentry/utils" "8.35.0"
"@sentry/core" "8.36.0"
"@sentry/types" "8.36.0"
"@sentry/utils" "8.36.0"
"@sentry-internal/replay-canvas@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.35.0.tgz#de7849e0d4212ee37a9225b1fc346188d9b05072"
integrity sha512-TUrH6Piv19kvHIiRyIuapLdnuwxk/Un/l1WDCQfq7mK9p1Pac0FkQ7Uufjp6zY3lyhDDZQ8qvCS4ioCMibCwQg==
"@sentry-internal/replay-canvas@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.36.0.tgz#557c8677f6c386cf79b5f8ab7485b31f4347a6cb"
integrity sha512-KJPLf+qYdrQdmouoAqIPZ2KeapIBlHWbzNdQqNxJFWLHFFjpLUtt0b+87ruvbA/q3NYy2fDwD7EB0tGS1RHBaA==
dependencies:
"@sentry-internal/replay" "8.35.0"
"@sentry/core" "8.35.0"
"@sentry/types" "8.35.0"
"@sentry/utils" "8.35.0"
"@sentry-internal/replay" "8.36.0"
"@sentry/core" "8.36.0"
"@sentry/types" "8.36.0"
"@sentry/utils" "8.36.0"
"@sentry-internal/replay@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.35.0.tgz#f71abae95cb492a54b43885386adbc5c639486c7"
integrity sha512-3wkW03vXYMyWtTLxl9yrtkV+qxbnKFgfASdoGWhXzfLjycgT6o4/04eb3Gn71q9aXqRwH17ISVQbVswnRqMcmA==
"@sentry-internal/replay@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.36.0.tgz#8f5dccab0bcc3429650a461c46a01085f2f8b782"
integrity sha512-lbic98GsSkDeinQDix54tBFEgHUlmBtO+HjXECk9jIE0vOzR4As20/s5ta46t1rKMLlnxOtJuT5jKXeUYogBUw==
dependencies:
"@sentry-internal/browser-utils" "8.35.0"
"@sentry/core" "8.35.0"
"@sentry/types" "8.35.0"
"@sentry/utils" "8.35.0"
"@sentry-internal/browser-utils" "8.36.0"
"@sentry/core" "8.36.0"
"@sentry/types" "8.36.0"
"@sentry/utils" "8.36.0"
"@sentry/browser@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.35.0.tgz#67820951fd092ef72ee1a4897464bc7c8d317d77"
integrity sha512-WHfI+NoZzpCsmIvtr6ChOe7yWPLQyMchPnVhY3Z4UeC70bkYNdKcoj/4XZbX3m0D8+71JAsm0mJ9s9OC3Ue6MQ==
"@sentry/browser@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.36.0.tgz#da8977b37e56e5436cf3ea2c2e4a098f480ab115"
integrity sha512-bLrQNe+wD4DkCfB8OD5TF3Rr8KA2+aTo5wF3t3Bf6KVn8//iX1ia1hhtptYiRnbRkG/0AEPxlqL6XfPZYVPQ5A==
dependencies:
"@sentry-internal/browser-utils" "8.35.0"
"@sentry-internal/feedback" "8.35.0"
"@sentry-internal/replay" "8.35.0"
"@sentry-internal/replay-canvas" "8.35.0"
"@sentry/core" "8.35.0"
"@sentry/types" "8.35.0"
"@sentry/utils" "8.35.0"
"@sentry-internal/browser-utils" "8.36.0"
"@sentry-internal/feedback" "8.36.0"
"@sentry-internal/replay" "8.36.0"
"@sentry-internal/replay-canvas" "8.36.0"
"@sentry/core" "8.36.0"
"@sentry/types" "8.36.0"
"@sentry/utils" "8.36.0"
"@sentry/core@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.35.0.tgz#17090f4d2d3bb983d9d99ecd2d27f4e9e107e0b0"
integrity sha512-Ci0Nmtw5ETWLqQJGY4dyF+iWh7PWKy6k303fCEoEmqj2czDrKJCp7yHBNV0XYbo00prj2ZTbCr6I7albYiyONA==
"@sentry/core@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.36.0.tgz#34276354f0cd2298803c2f8d86ba571473435ff1"
integrity sha512-cbq1WQyRqc/+YpPhjwQxfniUM3ZxmO3Pm1oisTB8dw6mlbgQfGD6aznEIjXWWJY6k6acewJlMUx09N7DnprtBw==
dependencies:
"@sentry/types" "8.35.0"
"@sentry/utils" "8.35.0"
"@sentry/types" "8.36.0"
"@sentry/utils" "8.36.0"
"@sentry/types@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.35.0.tgz#535c807800f7e378f61416f30177c0ef81b95012"
integrity sha512-AVEZjb16MlYPifiDDvJ19dPQyDn0jlrtC1PHs6ZKO+Rzyz+2EX2BRdszvanqArldexPoU1p5Bn2w81XZNXThBA==
"@sentry/types@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.36.0.tgz#b58397eb672d896b65b06103feb59dba74da9d39"
integrity sha512-K1pVFfdGHw115RzGHpwSOqoEPeayn4N1F9IfM0kxrYpQSbFT1X29eak88GBfC8gPiLEF0iFGlSaQ4ERmF7oRcA==
"@sentry/utils@8.35.0":
version "8.35.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.35.0.tgz#1e099fcbc60040091c79f028a83226c145d588ee"
integrity sha512-MdMb6+uXjqND7qIPWhulubpSeHzia6HtxeJa8jYI09OCvIcmNGPydv/Gx/LZBwosfMHrLdTWcFH7Y7aCxrq7cg==
"@sentry/utils@8.36.0":
version "8.36.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.36.0.tgz#e733042ae231fdeeafe6970e49283dcd9ac9700f"
integrity sha512-oJ3EDPj0I00z+AwC3EWBpSidXYUoKW0Id8MfMQP5Hflniz3gif7UEReblT+FJgPEVo6+6uNzAncY0MuNMxmDKQ==
dependencies:
"@sentry/types" "8.35.0"
"@sentry/types" "8.36.0"
"@sinclair/typebox@^0.27.8":
version "0.27.8"