Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
492ef8d5bb
commit
62d12a8a8d
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
# Cop supports --autocorrect.
|
||||
Lint/UselessNumericOperation:
|
||||
Details: grace period
|
||||
Exclude:
|
||||
- 'ee/spec/lib/gitlab/geo_spec.rb'
|
||||
|
|
@ -1 +1 @@
|
|||
e8ecda472f3b2d7ea74fda18e64d9bf77e2e1487
|
||||
2bb6448d821d9436d6b74b064a4cb48965603182
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(':')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
table_name: instance_integrations
|
||||
classes:
|
||||
- Integrations::InstanceIntegration
|
||||
- Integrations::Instance::Integration
|
||||
feature_categories:
|
||||
- integrations
|
||||
description: Support 3rd party instance-wide integrations
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
a90f86f8f7ccc8bd9d1032a0ae161a413b6a4d6f2bd85e4198b224dca8483a68
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
|
|
@ -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**:
|
|||
|
||||

|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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}**):
|
||||

|
||||
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
|
|||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||

|
||||
|
||||
#### 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 **-**.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :instance_integration, class: 'Integrations::InstanceIntegration' do
|
||||
type { 'Integrations::InstanceIntegration' }
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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') } }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
112
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue