Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e7ab7aaa8d
commit
91e8c3a6ef
|
@ -122,7 +122,7 @@ export default {
|
|||
fullPath: {
|
||||
default: '',
|
||||
},
|
||||
groupEpicsPath: {
|
||||
groupPath: {
|
||||
default: '',
|
||||
},
|
||||
hasAnyIssues: {
|
||||
|
@ -371,16 +371,18 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.groupEpicsPath) {
|
||||
if (this.groupPath) {
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_EPIC,
|
||||
title: TOKEN_TITLE_EPIC,
|
||||
icon: 'epic',
|
||||
token: EpicToken,
|
||||
unique: true,
|
||||
symbol: '&',
|
||||
idProperty: 'id',
|
||||
useIdValue: true,
|
||||
fetchEpics: this.fetchEpics,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-epic_id`,
|
||||
fullPath: this.groupPath,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -450,16 +452,6 @@ export default {
|
|||
fetchEmojis(search) {
|
||||
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
|
||||
},
|
||||
async fetchEpics({ search }) {
|
||||
const epics = await this.fetchWithCache(this.groupEpicsPath, 'epics');
|
||||
if (!search) {
|
||||
return epics.slice(0, MAX_LIST_SIZE);
|
||||
}
|
||||
const number = Number(search);
|
||||
return Number.isNaN(number)
|
||||
? fuzzaldrinPlus.filter(epics, search, { key: 'title' })
|
||||
: epics.filter((epic) => epic.id === number);
|
||||
},
|
||||
fetchLabels(search) {
|
||||
return this.$apollo
|
||||
.query({
|
||||
|
|
|
@ -119,7 +119,7 @@ export function mountIssuesListApp() {
|
|||
emptyStateSvgPath,
|
||||
exportCsvPath,
|
||||
fullPath,
|
||||
groupEpicsPath,
|
||||
groupPath,
|
||||
hasAnyIssues,
|
||||
hasAnyProjects,
|
||||
hasBlockedIssuesFeature,
|
||||
|
@ -152,7 +152,7 @@ export function mountIssuesListApp() {
|
|||
canBulkUpdate: parseBoolean(canBulkUpdate),
|
||||
emptyStateSvgPath,
|
||||
fullPath,
|
||||
groupEpicsPath,
|
||||
groupPath,
|
||||
hasAnyIssues: parseBoolean(hasAnyIssues),
|
||||
hasAnyProjects: parseBoolean(hasAnyProjects),
|
||||
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
|
||||
|
|
|
@ -181,13 +181,14 @@ export default {
|
|||
</template>
|
||||
<template #right-action>
|
||||
<gl-dropdown
|
||||
v-if="!isDeleteDisabled"
|
||||
:disabled="isDeleteDisabled"
|
||||
icon="ellipsis_v"
|
||||
:text="$options.i18n.MORE_ACTIONS_TEXT"
|
||||
:text-sr-only="true"
|
||||
category="tertiary"
|
||||
no-caret
|
||||
right
|
||||
:class="{ 'gl-opacity-0 gl-pointer-events-none': isDeleteDisabled }"
|
||||
data-testid="additional-actions"
|
||||
data-qa-selector="more_actions_menu"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
fragment EpicNode on Epic {
|
||||
id
|
||||
iid
|
||||
group {
|
||||
fullPath
|
||||
}
|
||||
title
|
||||
state
|
||||
reference
|
||||
referencePath: reference(full: true)
|
||||
webPath
|
||||
webUrl
|
||||
createdAt
|
||||
closedAt
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
#import "./epic.fragment.graphql"
|
||||
|
||||
query searchEpics($fullPath: ID!, $search: String, $state: EpicState) {
|
||||
group(fullPath: $fullPath) {
|
||||
epics(
|
||||
search: $search
|
||||
state: $state
|
||||
includeAncestorGroups: true
|
||||
includeDescendantGroups: false
|
||||
) {
|
||||
nodes {
|
||||
...EpicNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -67,6 +67,11 @@ export default {
|
|||
required: false,
|
||||
default: 'id',
|
||||
},
|
||||
searchBy: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -112,16 +117,18 @@ export default {
|
|||
);
|
||||
},
|
||||
showDefaultSuggestions() {
|
||||
return this.availableDefaultSuggestions.length;
|
||||
return this.availableDefaultSuggestions.length > 0;
|
||||
},
|
||||
showRecentSuggestions() {
|
||||
return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
|
||||
return (
|
||||
this.isRecentSuggestionsEnabled && this.recentSuggestions.length > 0 && !this.searchKey
|
||||
);
|
||||
},
|
||||
showPreloadedSuggestions() {
|
||||
return this.preloadedSuggestions.length && !this.searchKey;
|
||||
return this.preloadedSuggestions.length > 0 && !this.searchKey;
|
||||
},
|
||||
showAvailableSuggestions() {
|
||||
return this.availableSuggestions.length;
|
||||
return this.availableSuggestions.length > 0;
|
||||
},
|
||||
showSuggestions() {
|
||||
// These conditions must match the template under `#suggestions` slot
|
||||
|
@ -134,13 +141,19 @@ export default {
|
|||
this.showAvailableSuggestions
|
||||
);
|
||||
},
|
||||
searchTerm() {
|
||||
return this.searchBy && this.activeTokenValue
|
||||
? this.activeTokenValue[this.searchBy]
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
active: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (!newValue && !this.suggestions.length) {
|
||||
this.$emit('fetch-suggestions', this.value.data);
|
||||
const search = this.searchTerm ? this.searchTerm : this.value.data;
|
||||
this.$emit('fetch-suggestions', search);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -150,7 +163,8 @@ export default {
|
|||
this.searchKey = data;
|
||||
|
||||
if (!this.suggestionsLoading && !this.activeTokenValue) {
|
||||
this.$emit('fetch-suggestions', data);
|
||||
const search = this.searchTerm ? this.searchTerm : data;
|
||||
this.$emit('fetch-suggestions', search);
|
||||
}
|
||||
}, DEBOUNCE_DELAY),
|
||||
handleTokenValueSelected(activeTokenValue) {
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
<script>
|
||||
import {
|
||||
GlDropdownDivider,
|
||||
GlFilteredSearchSuggestion,
|
||||
GlFilteredSearchToken,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { __ } from '~/locale';
|
||||
import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
|
||||
import { DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
|
||||
import searchEpicsQuery from '../queries/search_epics.query.graphql';
|
||||
|
||||
import BaseToken from './base_token.vue';
|
||||
|
||||
export default {
|
||||
separator: '::&',
|
||||
prefix: '&',
|
||||
separator: '::',
|
||||
components: {
|
||||
GlDropdownDivider,
|
||||
GlFilteredSearchToken,
|
||||
BaseToken,
|
||||
GlFilteredSearchSuggestion,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
|
@ -27,11 +24,15 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
epics: this.config.initialEpics || [],
|
||||
loading: true,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -56,98 +57,73 @@ export default {
|
|||
}
|
||||
return this.defaultEpics;
|
||||
},
|
||||
activeEpic() {
|
||||
if (this.currentValue && this.epics.length) {
|
||||
// Check if current value is an epic ID.
|
||||
if (typeof this.currentValue === 'number') {
|
||||
return this.epics.find((epic) => epic[this.idProperty] === this.currentValue);
|
||||
}
|
||||
|
||||
// Current value is a string.
|
||||
const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator);
|
||||
return this.epics.find(
|
||||
(epic) =>
|
||||
epic.group_full_path === groupPath &&
|
||||
epic[this.idProperty] === parseInt(idProperty, 10),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
displayText() {
|
||||
return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
active: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (!newValue && !this.epics.length) {
|
||||
this.searchEpics({ data: this.currentValue });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchEpicsBySearchTerm({ epicPath = '', search = '' }) {
|
||||
fetchEpics(search = '') {
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: searchEpicsQuery,
|
||||
variables: { fullPath: this.config.fullPath, search },
|
||||
})
|
||||
.then(({ data }) => data.group?.epics.nodes);
|
||||
},
|
||||
fetchEpicsBySearchTerm(search) {
|
||||
this.loading = true;
|
||||
this.config
|
||||
.fetchEpics({ epicPath, search })
|
||||
this.fetchEpics(search)
|
||||
.then((response) => {
|
||||
this.epics = Array.isArray(response) ? response : response.data;
|
||||
this.epics = Array.isArray(response) ? response : response?.data;
|
||||
})
|
||||
.catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
searchEpics: debounce(function debouncedSearch({ data }) {
|
||||
let epicPath = this.activeEpic?.web_url;
|
||||
|
||||
// When user visits the page with token value already included in filters
|
||||
// We don't have any information about selected token except for its
|
||||
// group path and iid joined by separator, so we need to manually
|
||||
// compose epic path from it.
|
||||
if (data.includes?.(this.$options.separator)) {
|
||||
const [groupPath, epicIid] = data.split(this.$options.separator);
|
||||
epicPath = `/groups/${groupPath}/-/epics/${epicIid}`;
|
||||
getActiveEpic(epics, data) {
|
||||
if (data && epics.length) {
|
||||
return epics.find((epic) => this.getValue(epic) === data);
|
||||
}
|
||||
this.fetchEpicsBySearchTerm({ epicPath, search: data });
|
||||
}, DEBOUNCE_DELAY),
|
||||
|
||||
return undefined;
|
||||
},
|
||||
getValue(epic) {
|
||||
return this.config.useIdValue
|
||||
? String(epic[this.idProperty])
|
||||
: `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`;
|
||||
return this.getEpicIdProperty(epic).toString();
|
||||
},
|
||||
displayValue(epic) {
|
||||
return `${this.$options.prefix}${this.getEpicIdProperty(epic)}${this.$options.separator}${
|
||||
epic?.title
|
||||
}`;
|
||||
},
|
||||
getEpicIdProperty(epic) {
|
||||
return getIdFromGraphQLId(epic[this.idProperty]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-filtered-search-token
|
||||
<base-token
|
||||
:config="config"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:value="value"
|
||||
:active="active"
|
||||
:suggestions-loading="loading"
|
||||
:suggestions="epics"
|
||||
:get-active-token-value="getActiveEpic"
|
||||
:default-suggestions="availableDefaultEpics"
|
||||
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
|
||||
search-by="title"
|
||||
@fetch-suggestions="fetchEpicsBySearchTerm"
|
||||
v-on="$listeners"
|
||||
@input="searchEpics"
|
||||
>
|
||||
<template #view="{ inputValue }">
|
||||
{{ activeEpic ? displayText : inputValue }}
|
||||
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
|
||||
{{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
|
||||
</template>
|
||||
<template #suggestions>
|
||||
<template #suggestions-list="{ suggestions }">
|
||||
<gl-filtered-search-suggestion
|
||||
v-for="epic in availableDefaultEpics"
|
||||
:key="epic.value"
|
||||
:value="epic.value"
|
||||
v-for="epic in suggestions"
|
||||
:key="epic.id"
|
||||
:value="getValue(epic)"
|
||||
>
|
||||
{{ epic.text }}
|
||||
</gl-filtered-search-suggestion>
|
||||
<gl-dropdown-divider v-if="availableDefaultEpics.length" />
|
||||
<gl-loading-icon v-if="loading" size="sm" />
|
||||
<template v-else>
|
||||
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
|
||||
{{ epic.title }}
|
||||
</gl-filtered-search-suggestion>
|
||||
</template>
|
||||
</template>
|
||||
</gl-filtered-search-token>
|
||||
</base-token>
|
||||
</template>
|
||||
|
|
|
@ -28,7 +28,7 @@ module Mutations
|
|||
def authenticate_delete_runner!(runner)
|
||||
return if current_user.can_admin_all_resources?
|
||||
|
||||
"Runner #{runner.to_global_id} associated with more than one project" if runner.projects.count > 1
|
||||
"Runner #{runner.to_global_id} associated with more than one project" if runner.runner_projects.count > 1
|
||||
end
|
||||
|
||||
def find_object(id)
|
||||
|
|
|
@ -40,7 +40,7 @@ module Mutations
|
|||
|
||||
argument :description, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Description or notes for the contact.'
|
||||
description: 'Description of or notes for the contact.'
|
||||
|
||||
authorize :admin_contact
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module CustomerRelations
|
||||
module Contacts
|
||||
class Update < Mutations::BaseMutation
|
||||
include ResolvesIds
|
||||
|
||||
graphql_name 'CustomerRelationsContactUpdate'
|
||||
|
||||
authorize :admin_contact
|
||||
|
||||
field :contact,
|
||||
Types::CustomerRelations::ContactType,
|
||||
null: true,
|
||||
description: 'Contact after the mutation.'
|
||||
|
||||
argument :id, ::Types::GlobalIDType[::CustomerRelations::Contact],
|
||||
required: true,
|
||||
description: 'Global ID of the contact.'
|
||||
|
||||
argument :organization_id, ::Types::GlobalIDType[::CustomerRelations::Organization],
|
||||
required: false,
|
||||
description: 'Organization of the contact.'
|
||||
|
||||
argument :first_name, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'First name of the contact.'
|
||||
|
||||
argument :last_name, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Last name of the contact.'
|
||||
|
||||
argument :phone, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Phone number of the contact.'
|
||||
|
||||
argument :email, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Email address of the contact.'
|
||||
|
||||
argument :description, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Description of or notes for the contact.'
|
||||
|
||||
def resolve(args)
|
||||
contact = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Contact))
|
||||
raise_resource_not_available_error! unless contact
|
||||
|
||||
group = contact.group
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
|
||||
|
||||
authorize!(group)
|
||||
|
||||
result = ::CustomerRelations::Contacts::UpdateService.new(group: group, current_user: current_user, params: args).execute(contact)
|
||||
{ contact: result.payload, errors: result.errors }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -31,7 +31,7 @@ module Mutations
|
|||
argument :description,
|
||||
GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Description or notes for the organization.'
|
||||
description: 'Description of or notes for the organization.'
|
||||
|
||||
authorize :admin_organization
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ module Mutations
|
|||
argument :description,
|
||||
GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Description or notes for the organization.'
|
||||
description: 'Description of or notes for the organization.'
|
||||
|
||||
def resolve(args)
|
||||
organization = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Organization))
|
||||
|
|
|
@ -39,7 +39,7 @@ module Types
|
|||
field :description,
|
||||
GraphQL::Types::String,
|
||||
null: true,
|
||||
description: 'Description or notes for the contact.'
|
||||
description: 'Description of or notes for the contact.'
|
||||
|
||||
field :created_at,
|
||||
Types::TimeType,
|
||||
|
|
|
@ -25,7 +25,7 @@ module Types
|
|||
field :description,
|
||||
GraphQL::Types::String,
|
||||
null: true,
|
||||
description: 'Description or notes for the organization.'
|
||||
description: 'Description of or notes for the organization.'
|
||||
|
||||
field :created_at,
|
||||
Types::TimeType,
|
||||
|
|
|
@ -39,6 +39,7 @@ module Types
|
|||
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
|
||||
mount_mutation Mutations::CustomEmoji::Destroy, feature_flag: :custom_emoji
|
||||
mount_mutation Mutations::CustomerRelations::Contacts::Create
|
||||
mount_mutation Mutations::CustomerRelations::Contacts::Update
|
||||
mount_mutation Mutations::CustomerRelations::Organizations::Create
|
||||
mount_mutation Mutations::CustomerRelations::Organizations::Update
|
||||
mount_mutation Mutations::Discussions::ToggleResolve
|
||||
|
|
|
@ -246,7 +246,7 @@ module Ci
|
|||
|
||||
begin
|
||||
transaction do
|
||||
self.projects << project
|
||||
self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self)
|
||||
self.save!
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
|
@ -280,9 +280,7 @@ module Ci
|
|||
end
|
||||
|
||||
def belongs_to_more_than_one_project?
|
||||
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
|
||||
self.projects.limit(2).count(:all) > 1
|
||||
end
|
||||
runner_projects.limit(2).count(:all) > 1
|
||||
end
|
||||
|
||||
def assigned_to_group?
|
||||
|
@ -432,12 +430,10 @@ module Ci
|
|||
end
|
||||
|
||||
def no_projects
|
||||
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
|
||||
if projects.any?
|
||||
if runner_projects.any?
|
||||
errors.add(:runner, 'cannot have projects assigned')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def no_groups
|
||||
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
|
||||
|
@ -448,12 +444,10 @@ module Ci
|
|||
end
|
||||
|
||||
def any_project
|
||||
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
|
||||
unless projects.any?
|
||||
unless runner_projects.any?
|
||||
errors.add(:runner, 'needs to be assigned to at least one project')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exactly_one_group
|
||||
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
|
||||
|
|
|
@ -72,7 +72,7 @@ module Clusters
|
|||
if cluster.group_type?
|
||||
attributes[:groups] = [group]
|
||||
elsif cluster.project_type?
|
||||
attributes[:projects] = [project]
|
||||
attributes[:runner_projects] = [::Ci::RunnerProject.new(project: project)]
|
||||
end
|
||||
|
||||
attributes
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module CustomerRelations
|
||||
module Contacts
|
||||
class UpdateService < BaseService
|
||||
def execute(contact)
|
||||
return error_no_permissions unless allowed?
|
||||
return error_updating(contact) unless contact.update(params)
|
||||
|
||||
ServiceResponse.success(payload: contact)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def error_no_permissions
|
||||
error('You have insufficient permissions to update a contact for this group')
|
||||
end
|
||||
|
||||
def error_updating(contact)
|
||||
error(contact&.errors&.full_messages || 'Failed to update contact')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@
|
|||
.row
|
||||
.col-md-6
|
||||
%h4= _('Restrict projects for this runner')
|
||||
- if @runner.projects.any?
|
||||
- if @runner.runner_projects.any?
|
||||
%table.table{ data: { testid: 'assigned-projects' } }
|
||||
%thead
|
||||
%tr
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
- if runner.group_type?
|
||||
= _('n/a')
|
||||
- else
|
||||
= runner.projects.count(:all)
|
||||
= runner.runner_projects.count(:all)
|
||||
|
||||
.table-section.section-5
|
||||
.table-mobile-header{ role: 'rowheader' }= _('Jobs')
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: gitaly_user_merge_branch_access_error
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3705
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitaly/-/issues/3757
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
|
@ -1425,7 +1425,7 @@ Input type: `CustomerRelationsContactCreateInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationcustomerrelationscontactcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationcustomerrelationscontactcreatedescription"></a>`description` | [`String`](#string) | Description or notes for the contact. |
|
||||
| <a id="mutationcustomerrelationscontactcreatedescription"></a>`description` | [`String`](#string) | Description of or notes for the contact. |
|
||||
| <a id="mutationcustomerrelationscontactcreateemail"></a>`email` | [`String`](#string) | Email address of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactcreatefirstname"></a>`firstName` | [`String!`](#string) | First name of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactcreategroupid"></a>`groupId` | [`GroupID!`](#groupid) | Group for the contact. |
|
||||
|
@ -1441,6 +1441,31 @@ Input type: `CustomerRelationsContactCreateInput`
|
|||
| <a id="mutationcustomerrelationscontactcreatecontact"></a>`contact` | [`CustomerRelationsContact`](#customerrelationscontact) | Contact after the mutation. |
|
||||
| <a id="mutationcustomerrelationscontactcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.customerRelationsContactUpdate`
|
||||
|
||||
Input type: `CustomerRelationsContactUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationcustomerrelationscontactupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationcustomerrelationscontactupdatedescription"></a>`description` | [`String`](#string) | Description of or notes for the contact. |
|
||||
| <a id="mutationcustomerrelationscontactupdateemail"></a>`email` | [`String`](#string) | Email address of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactupdatefirstname"></a>`firstName` | [`String`](#string) | First name of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactupdateid"></a>`id` | [`CustomerRelationsContactID!`](#customerrelationscontactid) | Global ID of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactupdatelastname"></a>`lastName` | [`String`](#string) | Last name of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactupdateorganizationid"></a>`organizationId` | [`CustomerRelationsOrganizationID`](#customerrelationsorganizationid) | Organization of the contact. |
|
||||
| <a id="mutationcustomerrelationscontactupdatephone"></a>`phone` | [`String`](#string) | Phone number of the contact. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationcustomerrelationscontactupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationcustomerrelationscontactupdatecontact"></a>`contact` | [`CustomerRelationsContact`](#customerrelationscontact) | Contact after the mutation. |
|
||||
| <a id="mutationcustomerrelationscontactupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.customerRelationsOrganizationCreate`
|
||||
|
||||
Input type: `CustomerRelationsOrganizationCreateInput`
|
||||
|
@ -1451,7 +1476,7 @@ Input type: `CustomerRelationsOrganizationCreateInput`
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationcustomerrelationsorganizationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationcustomerrelationsorganizationcreatedefaultrate"></a>`defaultRate` | [`Float`](#float) | Standard billing rate for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationcreatedescription"></a>`description` | [`String`](#string) | Description or notes for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationcreatedescription"></a>`description` | [`String`](#string) | Description of or notes for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationcreategroupid"></a>`groupId` | [`GroupID!`](#groupid) | Group for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationcreatename"></a>`name` | [`String!`](#string) | Name of the organization. |
|
||||
|
||||
|
@ -1473,7 +1498,7 @@ Input type: `CustomerRelationsOrganizationUpdateInput`
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationcustomerrelationsorganizationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationcustomerrelationsorganizationupdatedefaultrate"></a>`defaultRate` | [`Float`](#float) | Standard billing rate for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationupdatedescription"></a>`description` | [`String`](#string) | Description or notes for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationupdatedescription"></a>`description` | [`String`](#string) | Description of or notes for the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationupdateid"></a>`id` | [`CustomerRelationsOrganizationID!`](#customerrelationsorganizationid) | Global ID of the organization. |
|
||||
| <a id="mutationcustomerrelationsorganizationupdatename"></a>`name` | [`String`](#string) | Name of the organization. |
|
||||
|
||||
|
@ -8881,7 +8906,7 @@ A custom emoji uploaded by user.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="customerrelationscontactcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the contact was created. |
|
||||
| <a id="customerrelationscontactdescription"></a>`description` | [`String`](#string) | Description or notes for the contact. |
|
||||
| <a id="customerrelationscontactdescription"></a>`description` | [`String`](#string) | Description of or notes for the contact. |
|
||||
| <a id="customerrelationscontactemail"></a>`email` | [`String`](#string) | Email address of the contact. |
|
||||
| <a id="customerrelationscontactfirstname"></a>`firstName` | [`String!`](#string) | First name of the contact. |
|
||||
| <a id="customerrelationscontactid"></a>`id` | [`ID!`](#id) | Internal ID of the contact. |
|
||||
|
@ -8898,7 +8923,7 @@ A custom emoji uploaded by user.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="customerrelationsorganizationcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the organization was created. |
|
||||
| <a id="customerrelationsorganizationdefaultrate"></a>`defaultRate` | [`Float`](#float) | Standard billing rate for the organization. |
|
||||
| <a id="customerrelationsorganizationdescription"></a>`description` | [`String`](#string) | Description or notes for the organization. |
|
||||
| <a id="customerrelationsorganizationdescription"></a>`description` | [`String`](#string) | Description of or notes for the organization. |
|
||||
| <a id="customerrelationsorganizationid"></a>`id` | [`ID!`](#id) | Internal ID of the organization. |
|
||||
| <a id="customerrelationsorganizationname"></a>`name` | [`String!`](#string) | Name of the organization. |
|
||||
| <a id="customerrelationsorganizationupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the organization was last updated. |
|
||||
|
@ -16941,6 +16966,12 @@ A `CustomEmojiID` is a global ID. It is encoded as a string.
|
|||
|
||||
An example `CustomEmojiID` is: `"gid://gitlab/CustomEmoji/1"`.
|
||||
|
||||
### `CustomerRelationsContactID`
|
||||
|
||||
A `CustomerRelationsContactID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `CustomerRelationsContactID` is: `"gid://gitlab/CustomerRelations::Contact/1"`.
|
||||
|
||||
### `CustomerRelationsOrganizationID`
|
||||
|
||||
A `CustomerRelationsOrganizationID` is a global ID. It is encoded as a string.
|
||||
|
|
|
@ -467,6 +467,40 @@ Get Custom Issue Tracker integration settings for a project.
|
|||
GET /projects/:id/integrations/custom-issue-tracker
|
||||
```
|
||||
|
||||
## Discord
|
||||
|
||||
Send notifications about project events to a Discord channel.
|
||||
|
||||
### Create/Edit Discord integration
|
||||
|
||||
Set Discord integration for a project.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/integrations/discord
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `webhook` | string | true | Discord webhook. For example, `https://discord.com/api/webhooks/…` |
|
||||
|
||||
### Delete Discord integration
|
||||
|
||||
Delete Discord integration for a project.
|
||||
|
||||
```plaintext
|
||||
DELETE /projects/:id/integrations/discord
|
||||
```
|
||||
|
||||
### Get Discord integration settings
|
||||
|
||||
Get Discord integration settings for a project.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/integrations/discord
|
||||
```
|
||||
|
||||
## Drone CI
|
||||
|
||||
Drone is a Continuous Integration platform built on Docker, written in Go
|
||||
|
|
|
@ -1014,12 +1014,15 @@ The on-demand DAST scan runs, and the project's dashboard shows the results.
|
|||
|
||||
#### Schedule an on-demand scan
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.3. [Deployed behind the `dast_on_demand_scans_scheduler` flag](../../../administration/feature_flags.md), disabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.3. [Deployed behind the `dast_on_demand_scans_scheduler` flag](../../../administration/feature_flags.md), disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.4.
|
||||
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.4.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available per user,
|
||||
ask an administrator to [disable the `dast_on_demand_scans_scheduler` flag](../../../administration/feature_flags.md).
|
||||
The feature is not ready for production use.
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an
|
||||
administrator to [disable the feature flag](../../../administration/feature_flags.md) named
|
||||
`dast_on_demand_scans_scheduler`.
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
To schedule a scan:
|
||||
|
||||
|
|
|
@ -204,7 +204,7 @@ module API
|
|||
not_found!('Runner') unless runner_project
|
||||
|
||||
runner = runner_project.runner
|
||||
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
|
||||
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.runner_projects.count == 1
|
||||
|
||||
destroy_conditionally!(runner_project)
|
||||
end
|
||||
|
@ -331,7 +331,7 @@ module API
|
|||
def authenticate_delete_runner!(runner)
|
||||
return if current_user.admin?
|
||||
|
||||
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
|
||||
forbidden!("Runner associated with more than one project") if runner.runner_projects.count > 1
|
||||
forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
|
||||
end
|
||||
|
||||
|
|
|
@ -340,7 +340,7 @@ module API
|
|||
required: true,
|
||||
name: :webhook,
|
||||
type: String,
|
||||
desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…'
|
||||
desc: 'Discord webhook. For example, https://discord.com/api/webhooks/…'
|
||||
}
|
||||
],
|
||||
'drone-ci' => [
|
||||
|
|
|
@ -45,10 +45,19 @@ module Gitlab
|
|||
cte.issue_id = issue_metrics.issue_id
|
||||
UPDATE_METRICS
|
||||
end
|
||||
|
||||
mark_job_as_succeeded(start_id, end_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_job_as_succeeded(*arguments)
|
||||
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
|
||||
'FixFirstMentionedInCommitAt',
|
||||
arguments
|
||||
)
|
||||
end
|
||||
|
||||
def scope(start_id, end_id)
|
||||
TmpIssueMetrics.from_2020.where(issue_id: start_id..end_id)
|
||||
end
|
||||
|
|
|
@ -162,6 +162,14 @@ module Gitlab
|
|||
raise Gitlab::Git::CommitError, 'failed to apply merge to branch' unless branch_update.commit_id.present?
|
||||
|
||||
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
|
||||
|
||||
rescue GRPC::BadStatus => e
|
||||
decoded_error = decode_detailed_error(e)
|
||||
|
||||
raise unless decoded_error.present?
|
||||
|
||||
raise decoded_error
|
||||
|
||||
ensure
|
||||
request_enum.close
|
||||
end
|
||||
|
@ -470,6 +478,31 @@ module Gitlab
|
|||
rescue RangeError
|
||||
raise ArgumentError, "Unknown action '#{action[:action]}'"
|
||||
end
|
||||
|
||||
def decode_detailed_error(err)
|
||||
# details could have more than one in theory, but we only have one to worry about for now.
|
||||
detailed_error = err.to_rpc_status&.details&.first
|
||||
|
||||
return unless detailed_error.present?
|
||||
|
||||
prefix = %r{type\.googleapis\.com\/gitaly\.(?<error_type>.+)}
|
||||
error_type = prefix.match(detailed_error.type_url)[:error_type]
|
||||
|
||||
detailed_error = Gitaly.const_get(error_type, false).decode(detailed_error.value)
|
||||
|
||||
case detailed_error.error
|
||||
when :access_check
|
||||
access_check_error = detailed_error.access_check
|
||||
# These messages were returned from internal/allowed API calls
|
||||
Gitlab::Git::PreReceiveError.new(fallback_message: access_check_error.error_message)
|
||||
else
|
||||
# We're handling access_check only for now, but we'll add more detailed error types
|
||||
nil
|
||||
end
|
||||
rescue NameError, NoMethodError
|
||||
# Error Class might not be known to ruby yet
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -78,6 +78,22 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
# Get group members
|
||||
#
|
||||
# @return [Array<QA::Resource::User>]
|
||||
def members
|
||||
parse_body(api_get_from("#{api_get_path}/members")).map do |member|
|
||||
User.init do |resource|
|
||||
resource.api_client = api_client
|
||||
resource.id = member[:id]
|
||||
resource.name = member[:name]
|
||||
resource.username = member[:username]
|
||||
resource.email = member[:email]
|
||||
resource.access_level = member[:access_level]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# API get path
|
||||
#
|
||||
# @return [String]
|
||||
|
|
|
@ -7,13 +7,18 @@ module QA
|
|||
|
||||
attr_reader :unique_id
|
||||
attr_writer :username, :password
|
||||
attr_accessor :admin, :provider, :extern_uid, :expect_fabrication_success, :hard_delete_on_api_removal
|
||||
attr_accessor :admin,
|
||||
:provider,
|
||||
:extern_uid,
|
||||
:expect_fabrication_success,
|
||||
:hard_delete_on_api_removal,
|
||||
:access_level
|
||||
|
||||
attribute :id
|
||||
attribute :name
|
||||
attribute :first_name
|
||||
attribute :last_name
|
||||
attribute :email
|
||||
attributes :id,
|
||||
:name,
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email
|
||||
|
||||
def initialize
|
||||
@admin = false
|
||||
|
|
|
@ -126,6 +126,33 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
context 'with group members' do
|
||||
let(:member) do
|
||||
Resource::User.fabricate_via_api! do |usr|
|
||||
usr.api_client = admin_api_client
|
||||
usr.hard_delete_on_api_removal = true
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
member.set_public_email
|
||||
source_group.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
|
||||
end
|
||||
|
||||
after do
|
||||
member.remove_via_api!
|
||||
end
|
||||
|
||||
it 'adds members for imported group' do
|
||||
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
|
||||
|
||||
imported_member = imported_group.reload!.members.find { |usr| usr.username == member.username }
|
||||
|
||||
expect(imported_member).not_to be_nil
|
||||
expect(imported_member.access_level).to eq(Resource::Members::AccessLevel::DEVELOPER)
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
user.remove_via_api!
|
||||
ensure
|
||||
|
|
|
@ -2,7 +2,14 @@
|
|||
|
||||
FactoryBot.define do
|
||||
factory :ci_runner_project, class: 'Ci::RunnerProject' do
|
||||
runner factory: [:ci_runner, :project]
|
||||
project
|
||||
|
||||
after(:build) do |runner_project, evaluator|
|
||||
unless runner_project.runner.present?
|
||||
runner_project.runner = build(
|
||||
:ci_runner, :project, runner_projects: [runner_project]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,16 @@ FactoryBot.define do
|
|||
|
||||
runner_type { :instance_type }
|
||||
|
||||
transient do
|
||||
projects { [] }
|
||||
end
|
||||
|
||||
after(:build) do |runner, evaluator|
|
||||
evaluator.projects.each do |proj|
|
||||
runner.runner_projects << build(:ci_runner_project, project: proj)
|
||||
end
|
||||
end
|
||||
|
||||
trait :online do
|
||||
contacted_at { Time.now }
|
||||
end
|
||||
|
@ -30,7 +40,9 @@ FactoryBot.define do
|
|||
runner_type { :project_type }
|
||||
|
||||
after(:build) do |runner, evaluator|
|
||||
runner.projects << build(:project) if runner.projects.empty?
|
||||
if runner.runner_projects.empty?
|
||||
runner.runner_projects << build(:ci_runner_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -520,7 +520,7 @@ describe('IssuesListApp component', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
provide: {
|
||||
groupEpicsPath: '',
|
||||
groupPath: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -536,7 +536,7 @@ describe('IssuesListApp component', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
provide: {
|
||||
groupEpicsPath: '',
|
||||
groupPath: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -564,7 +564,7 @@ describe('IssuesListApp component', () => {
|
|||
provide: {
|
||||
isSignedIn: true,
|
||||
projectIterationsPath: 'project/iterations/path',
|
||||
groupEpicsPath: 'group/epics/path',
|
||||
groupPath: 'group/path',
|
||||
hasIssueWeightsFeature: true,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -283,18 +283,20 @@ describe('tags list row', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
canDelete | digest | disabled | visible
|
||||
${true} | ${null} | ${true} | ${false}
|
||||
${false} | ${'foo'} | ${true} | ${false}
|
||||
${false} | ${null} | ${true} | ${false}
|
||||
${true} | ${'foo'} | ${true} | ${false}
|
||||
${true} | ${'foo'} | ${false} | ${true}
|
||||
canDelete | digest | disabled | buttonDisabled
|
||||
${true} | ${null} | ${true} | ${true}
|
||||
${false} | ${'foo'} | ${true} | ${true}
|
||||
${false} | ${null} | ${true} | ${true}
|
||||
${true} | ${'foo'} | ${true} | ${true}
|
||||
${true} | ${'foo'} | ${false} | ${false}
|
||||
`(
|
||||
'is $visible that is visible when canDelete is $canDelete and digest is $digest and disabled is $disabled',
|
||||
({ canDelete, digest, disabled, visible }) => {
|
||||
({ canDelete, digest, disabled, buttonDisabled }) => {
|
||||
mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled });
|
||||
|
||||
expect(findAdditionalActionsMenu().exists()).toBe(visible);
|
||||
expect(findAdditionalActionsMenu().props('disabled')).toBe(buttonDisabled);
|
||||
expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(buttonDisabled);
|
||||
expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(buttonDisabled);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -141,7 +141,62 @@ export const mockEpicToken = {
|
|||
token: EpicToken,
|
||||
operators: OPERATOR_IS_ONLY,
|
||||
idProperty: 'iid',
|
||||
fetchEpics: () => Promise.resolve({ data: mockEpics }),
|
||||
fullPath: 'gitlab-org',
|
||||
};
|
||||
|
||||
export const mockEpicNode1 = {
|
||||
__typename: 'Epic',
|
||||
parent: null,
|
||||
id: 'gid://gitlab/Epic/40',
|
||||
iid: '2',
|
||||
title: 'Marketing epic',
|
||||
description: 'Mock epic description',
|
||||
state: 'opened',
|
||||
startDate: '2017-12-25',
|
||||
dueDate: '2018-02-15',
|
||||
webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1',
|
||||
hasChildren: false,
|
||||
hasParent: false,
|
||||
confidential: false,
|
||||
};
|
||||
|
||||
export const mockEpicNode2 = {
|
||||
__typename: 'Epic',
|
||||
parent: null,
|
||||
id: 'gid://gitlab/Epic/41',
|
||||
iid: '3',
|
||||
title: 'Another marketing',
|
||||
startDate: '2017-12-26',
|
||||
dueDate: '2018-03-10',
|
||||
state: 'opened',
|
||||
webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2',
|
||||
};
|
||||
|
||||
export const mockGroupEpicsQueryResponse = {
|
||||
data: {
|
||||
group: {
|
||||
id: 'gid://gitlab/Group/1',
|
||||
name: 'Gitlab Org',
|
||||
epics: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
...mockEpicNode1,
|
||||
},
|
||||
__typename: 'EpicEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
...mockEpicNode2,
|
||||
},
|
||||
__typename: 'EpicEdge',
|
||||
},
|
||||
],
|
||||
__typename: 'EpicConnection',
|
||||
},
|
||||
__typename: 'Group',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockReactionEmojiToken = {
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
|
||||
import { GlFilteredSearchTokenSegment } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
import searchEpicsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql';
|
||||
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
|
||||
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
|
||||
|
||||
import { mockEpicToken, mockEpics } from '../mock_data';
|
||||
import { mockEpicToken, mockEpics, mockGroupEpicsQueryResponse } from '../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const defaultStubs = {
|
||||
Portal: true,
|
||||
|
@ -21,7 +27,18 @@ const defaultStubs = {
|
|||
},
|
||||
};
|
||||
|
||||
function createComponent(options = {}) {
|
||||
describe('EpicToken', () => {
|
||||
let mock;
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findBaseToken = () => wrapper.findComponent(BaseToken);
|
||||
|
||||
function createComponent(
|
||||
options = {},
|
||||
epicsQueryHandler = jest.fn().mockResolvedValue(mockGroupEpicsQueryResponse),
|
||||
) {
|
||||
fakeApollo = createMockApollo([[searchEpicsQuery, epicsQueryHandler]]);
|
||||
const {
|
||||
config = mockEpicToken,
|
||||
value = { data: '' },
|
||||
|
@ -29,6 +46,7 @@ function createComponent(options = {}) {
|
|||
stubs = defaultStubs,
|
||||
} = options;
|
||||
return mount(EpicToken, {
|
||||
apolloProvider: fakeApollo,
|
||||
propsData: {
|
||||
config,
|
||||
value,
|
||||
|
@ -37,16 +55,12 @@ function createComponent(options = {}) {
|
|||
provide: {
|
||||
portalName: 'fake target',
|
||||
alignSuggestions: function fakeAlignSuggestions() {},
|
||||
suggestionsListClass: () => 'custom-class',
|
||||
suggestionsListClass: 'custom-class',
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
}
|
||||
|
||||
describe('EpicToken', () => {
|
||||
let mock;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
wrapper = createComponent();
|
||||
|
@ -71,23 +85,20 @@ describe('EpicToken', () => {
|
|||
|
||||
describe('methods', () => {
|
||||
describe('fetchEpicsBySearchTerm', () => {
|
||||
it('calls `config.fetchEpics` with provided searchTerm param', () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchEpics');
|
||||
it('calls fetchEpics with provided searchTerm param', () => {
|
||||
jest.spyOn(wrapper.vm, 'fetchEpics');
|
||||
|
||||
wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
|
||||
findBaseToken().vm.$emit('fetch-suggestions', 'foo');
|
||||
|
||||
expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith({
|
||||
epicPath: '',
|
||||
search: 'foo',
|
||||
});
|
||||
expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
|
||||
it('sets response to `epics` when request is successful', async () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({
|
||||
jest.spyOn(wrapper.vm, 'fetchEpics').mockResolvedValue({
|
||||
data: mockEpics,
|
||||
});
|
||||
|
||||
wrapper.vm.fetchEpicsBySearchTerm({});
|
||||
findBaseToken().vm.$emit('fetch-suggestions');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -95,9 +106,9 @@ describe('EpicToken', () => {
|
|||
});
|
||||
|
||||
it('calls `createFlash` with flash error message when request fails', async () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
|
||||
jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
|
||||
|
||||
wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
|
||||
findBaseToken().vm.$emit('fetch-suggestions', 'foo');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -107,9 +118,9 @@ describe('EpicToken', () => {
|
|||
});
|
||||
|
||||
it('sets `loading` to false when request completes', async () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
|
||||
jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
|
||||
|
||||
wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
|
||||
findBaseToken().vm.$emit('fetch-suggestions', 'foo');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -123,15 +134,15 @@ describe('EpicToken', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
wrapper = createComponent({
|
||||
value: { data: `${mockEpics[0].group_full_path}::&${mockEpics[0].iid}` },
|
||||
value: { data: `${mockEpics[0].title}::&${mockEpics[0].iid}` },
|
||||
data: { epics: mockEpics },
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders gl-filtered-search-token component', () => {
|
||||
expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
|
||||
it('renders BaseToken component', () => {
|
||||
expect(findBaseToken().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders token item when value is selected', () => {
|
||||
|
@ -143,8 +154,8 @@ describe('EpicToken', () => {
|
|||
|
||||
it.each`
|
||||
value | valueType | tokenValueString
|
||||
${`${mockEpics[0].group_full_path}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
|
||||
${`${mockEpics[1].group_full_path}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
|
||||
${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
|
||||
${`${mockEpics[1].title}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
|
||||
`('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => {
|
||||
wrapper.setProps({
|
||||
value: { data: value },
|
||||
|
|
|
@ -4,8 +4,9 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Mutations::CustomerRelations::Contacts::Create do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:not_found_or_does_not_belong) { 'The specified organization was not found or does not belong to this group' }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:not_found_or_does_not_belong) { 'The specified organization was not found or does not belong to this group' }
|
||||
let(:valid_params) do
|
||||
attributes_for(:contact,
|
||||
group: group,
|
||||
|
@ -22,8 +23,6 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
|
|||
end
|
||||
|
||||
context 'when the user does not have permission' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
@ -35,8 +34,6 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
|
|||
end
|
||||
|
||||
context 'when the user has permission' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::CustomerRelations::Contacts::Update do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:first_name) { 'Lionel' }
|
||||
let(:last_name) { 'Smith' }
|
||||
let(:email) { 'ls@gitlab.com' }
|
||||
let(:description) { 'VIP' }
|
||||
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
let(:contact) { create(:contact, group: group) }
|
||||
let(:attributes) do
|
||||
{
|
||||
id: contact.to_global_id,
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
email: email,
|
||||
description: description
|
||||
}
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: user }, field: nil).resolve(
|
||||
attributes
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the user does not have permission to update a contact' do
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
.with_message(does_not_exist_or_no_permission)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the contact does not exist' do
|
||||
it 'raises an error' do
|
||||
attributes[:id] = "gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"
|
||||
|
||||
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
.with_message(does_not_exist_or_no_permission)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has permission to update a contact' do
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'updates the organization with correct values' do
|
||||
expect(resolve_mutation[:contact]).to have_attributes(attributes)
|
||||
end
|
||||
|
||||
context 'when the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(customer_relations: false)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
.with_message('Feature disabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:admin_contact) }
|
||||
end
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Mutations::CustomerRelations::Organizations::Create do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:valid_params) do
|
||||
attributes_for(:organization,
|
||||
|
@ -23,8 +24,6 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
|
|||
end
|
||||
|
||||
context 'when the user does not have permission' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
@ -36,8 +35,6 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
|
|||
end
|
||||
|
||||
context 'when the user has permission' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
|
|
@ -4,11 +4,12 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Mutations::CustomerRelations::Organizations::Update do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:name) { 'GitLab' }
|
||||
let_it_be(:default_rate) { 1000.to_f }
|
||||
let_it_be(:description) { 'VIP' }
|
||||
let_it_be(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:name) { 'GitLab' }
|
||||
let(:default_rate) { 1000.to_f }
|
||||
let(:description) { 'VIP' }
|
||||
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
let(:organization) { create(:organization, group: group) }
|
||||
let(:attributes) do
|
||||
{
|
||||
|
@ -27,8 +28,6 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
|
|||
end
|
||||
|
||||
context 'when the user does not have permission to update an organization' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
@ -40,8 +39,6 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
|
|||
end
|
||||
|
||||
context 'when the organization does not exist' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
it 'raises an error' do
|
||||
attributes[:id] = "gid://gitlab/CustomerRelations::Organization/#{non_existing_record_id}"
|
||||
|
||||
|
@ -51,8 +48,6 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
|
|||
end
|
||||
|
||||
context 'when the user has permission to update an organization' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
|
|
@ -99,6 +99,15 @@ RSpec.describe Gitlab::BackgroundMigration::FixFirstMentionedInCommitAt, :migrat
|
|||
.perform(issue_metrics.minimum(:issue_id), issue_metrics.maximum(:issue_id))
|
||||
end
|
||||
|
||||
it "marks successful slices as completed" do
|
||||
min_issue_id = issue_metrics.minimum(:issue_id)
|
||||
max_issue_id = issue_metrics.maximum(:issue_id)
|
||||
|
||||
expect(subject).to receive(:mark_job_as_succeeded).with(min_issue_id, max_issue_id)
|
||||
|
||||
subject.perform(min_issue_id, max_issue_id)
|
||||
end
|
||||
|
||||
context 'when the persisted first_mentioned_in_commit_at is later than the first commit authored_date' do
|
||||
it 'updates the issue_metrics record' do
|
||||
record1 = issue_metrics.create!(issue_id: issue1.id, first_mentioned_in_commit_at: Time.current)
|
||||
|
|
|
@ -169,6 +169,56 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#user_merge_branch' do
|
||||
let(:target_branch) { 'master' }
|
||||
let(:source_sha) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
|
||||
let(:message) { 'Merge a branch' }
|
||||
|
||||
subject { client.user_merge_branch(user, source_sha, target_branch, message) {} }
|
||||
|
||||
it 'sends a user_merge_branch message' do
|
||||
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
|
||||
expect(subject.newrev).to be_present
|
||||
expect(subject.repo_created).to be(false)
|
||||
expect(subject.branch_created).to be(false)
|
||||
end
|
||||
|
||||
context 'with an exception with the UserMergeBranchError' do
|
||||
let(:permission_error) do
|
||||
GRPC::PermissionDenied.new(
|
||||
"GitLab: You are not allowed to push code to this project.",
|
||||
{ "grpc-status-details-bin" =>
|
||||
"\b\a\x129GitLab: You are not allowed to push code to this project.\x1A\xDE\x01\n/type.googleapis.com/gitaly.UserMergeBranchError\x12\xAA\x01\n\xA7\x01\n1You are not allowed to push code to this project.\x12\x03web\x1A\auser-15\"df15b32277d2c55c6c595845a87109b09c913c556 5d6e0f935ad9240655f64e883cd98fad6f9a17ee refs/heads/master\n" }
|
||||
)
|
||||
end
|
||||
|
||||
it 'raises PreRecieveError with the error message' do
|
||||
expect_any_instance_of(Gitaly::OperationService::Stub)
|
||||
.to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash))
|
||||
.and_raise(permission_error)
|
||||
|
||||
expect { subject }.to raise_error do |error|
|
||||
expect(error).to be_a(Gitlab::Git::PreReceiveError)
|
||||
expect(error.message).to eq("You are not allowed to push code to this project.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an exception without the detailed error' do
|
||||
let(:permission_error) do
|
||||
GRPC::PermissionDenied.new
|
||||
end
|
||||
|
||||
it 'raises PermissionDenied' do
|
||||
expect_any_instance_of(Gitaly::OperationService::Stub)
|
||||
.to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash))
|
||||
.and_raise(permission_error)
|
||||
|
||||
expect { subject }.to raise_error(GRPC::PermissionDenied)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_ff_branch' do
|
||||
let(:target_branch) { 'my-branch' }
|
||||
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
|
||||
|
|
|
@ -271,7 +271,7 @@ RSpec.describe Ci::Runner do
|
|||
expect(subject).to be_truthy
|
||||
|
||||
expect(runner).to be_project_type
|
||||
expect(runner.projects).to eq([project])
|
||||
expect(runner.runner_projects.pluck(:project_id)).to match_array([project.id])
|
||||
expect(runner.only_for?(project)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
@ -735,7 +735,7 @@ RSpec.describe Ci::Runner do
|
|||
|
||||
context 'with invalid runner' do
|
||||
before do
|
||||
runner.projects = []
|
||||
runner.runner_projects.delete_all
|
||||
end
|
||||
|
||||
it 'still updates redis cache and database' do
|
||||
|
|
|
@ -96,8 +96,9 @@ RSpec.describe Clusters::Applications::Runner do
|
|||
it 'creates a project runner' do
|
||||
subject
|
||||
|
||||
runner_projects = Project.where(id: runner.runner_projects.pluck(:project_id))
|
||||
expect(runner).to be_project_type
|
||||
expect(runner.projects).to eq [project]
|
||||
expect(runner_projects).to match_array [project]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -250,7 +250,7 @@ RSpec.describe 'Query.runner(id)' do
|
|||
end
|
||||
|
||||
before do
|
||||
project_runner2.projects.clear
|
||||
project_runner2.runner_projects.clear
|
||||
|
||||
post_graphql(query, current_user: user)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe CustomerRelations::Contacts::UpdateService do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:contact) { create(:contact, first_name: 'Mark', group: group) }
|
||||
|
||||
subject(:update) { described_class.new(group: group, current_user: user, params: params).execute(contact) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'when the user has no permission' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:params) { { first_name: 'Gary' } }
|
||||
|
||||
it 'returns an error' do
|
||||
response = update
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array(['You have insufficient permissions to update a contact for this group'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has permission' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when first_name is changed' do
|
||||
let(:params) { { first_name: 'Gary' } }
|
||||
|
||||
it 'updates the contact' do
|
||||
response = update
|
||||
|
||||
expect(response).to be_success
|
||||
expect(response.payload.first_name).to eq('Gary')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the contact is invalid' do
|
||||
let(:params) { { first_name: nil } }
|
||||
|
||||
it 'returns an error' do
|
||||
response = update
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array(["First name can't be blank"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41,8 +41,8 @@
|
|||
- "./ee/spec/services/ee/merge_requests/create_pipeline_service_spec.rb"
|
||||
- "./ee/spec/services/ee/merge_requests/refresh_service_spec.rb"
|
||||
- "./ee/spec/workers/scan_security_report_secrets_worker_spec.rb"
|
||||
- "./spec/controllers/admin/runners_controller_spec.rb"
|
||||
- "./spec/controllers/groups/settings/ci_cd_controller_spec.rb"
|
||||
- "./spec/controllers/admin/runners_controller_spec.rb"
|
||||
- "./spec/controllers/projects/merge_requests_controller_spec.rb"
|
||||
- "./spec/controllers/projects/settings/ci_cd_controller_spec.rb"
|
||||
- "./spec/features/admin/admin_runners_spec.rb"
|
||||
|
@ -56,7 +56,6 @@
|
|||
- "./spec/finders/ci/pipelines_for_merge_request_finder_spec.rb"
|
||||
- "./spec/finders/ci/runners_finder_spec.rb"
|
||||
- "./spec/frontend/fixtures/runner.rb"
|
||||
- "./spec/graphql/mutations/ci/runner/delete_spec.rb"
|
||||
- "./spec/graphql/resolvers/ci/group_runners_resolver_spec.rb"
|
||||
- "./spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb"
|
||||
- "./spec/lib/api/entities/package_spec.rb"
|
||||
|
@ -71,13 +70,11 @@
|
|||
- "./spec/models/ci/job_artifact_spec.rb"
|
||||
- "./spec/models/ci/pipeline_spec.rb"
|
||||
- "./spec/models/ci/runner_spec.rb"
|
||||
- "./spec/models/clusters/applications/runner_spec.rb"
|
||||
- "./spec/models/deployment_spec.rb"
|
||||
- "./spec/models/environment_spec.rb"
|
||||
- "./spec/models/merge_request_spec.rb"
|
||||
- "./spec/models/project_spec.rb"
|
||||
- "./spec/models/user_spec.rb"
|
||||
- "./spec/presenters/ci/build_runner_presenter_spec.rb"
|
||||
- "./spec/presenters/ci/pipeline_presenter_spec.rb"
|
||||
- "./spec/presenters/packages/detail/package_presenter_spec.rb"
|
||||
- "./spec/requests/api/ci/runner/runners_post_spec.rb"
|
||||
|
|
|
@ -79,8 +79,17 @@ module Database
|
|||
|
||||
return if cross_database_context[:transaction_depth_by_db].values.all?(&:zero?)
|
||||
|
||||
# PgQuery might fail in some cases due to limited nesting:
|
||||
# https://github.com/pganalyze/pg_query/issues/209
|
||||
#
|
||||
# Also, we disable GC while parsing because of https://github.com/pganalyze/pg_query/issues/226
|
||||
begin
|
||||
GC.disable
|
||||
parsed_query = PgQuery.parse(sql)
|
||||
tables = sql.downcase.include?(' for update') ? parsed_query.tables : parsed_query.dml_tables
|
||||
ensure
|
||||
GC.enable
|
||||
end
|
||||
|
||||
return if tables.empty?
|
||||
|
||||
|
|
Loading…
Reference in New Issue