Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1aa9cd3080
commit
b808458daa
|
@ -373,6 +373,10 @@
|
|||
- ".dockerignore"
|
||||
- "qa/**/*"
|
||||
|
||||
.code-shell-patterns: &code-shell-patterns
|
||||
- "bin/**/*"
|
||||
- "tooling/**/*"
|
||||
|
||||
# .code-backstage-qa-patterns + .workhorse-patterns
|
||||
.setup-test-env-patterns: &setup-test-env-patterns
|
||||
- "{package.json,yarn.lock}"
|
||||
|
@ -1775,6 +1779,13 @@
|
|||
- changes: *code-backstage-qa-patterns
|
||||
- changes: *startup-css-patterns
|
||||
|
||||
###############
|
||||
# Shell rules #
|
||||
###############
|
||||
.shell:rules:
|
||||
rules:
|
||||
- changes: *code-shell-patterns
|
||||
|
||||
#######################
|
||||
# Test metadata rules #
|
||||
#######################
|
||||
|
|
|
@ -107,3 +107,15 @@ feature-flags-usage:
|
|||
when: always
|
||||
paths:
|
||||
- tmp/feature_flags/
|
||||
|
||||
shellcheck:
|
||||
extends:
|
||||
- .default-retry
|
||||
- .shell:rules
|
||||
stage: lint
|
||||
needs: []
|
||||
image:
|
||||
name: koalaman/shellcheck-alpine
|
||||
entrypoint: [""]
|
||||
script:
|
||||
- tooling/bin/shellcheck
|
||||
|
|
|
@ -1 +1 @@
|
|||
460a880c6993ab5f76cac951fccc02efd5cbd444
|
||||
06ec7a17f320497d13efdc06f7798b919f45fa9d
|
||||
|
|
|
@ -75,7 +75,7 @@ export default {
|
|||
return this.$options.i18n.valid;
|
||||
default:
|
||||
// Only display first error as a reason
|
||||
return this.ciConfig?.errors.length > 0
|
||||
return this.ciConfig?.errors?.length > 0
|
||||
? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
|
||||
: this.$options.i18n.invalid;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url
|
|||
import {
|
||||
CREATE_TAB,
|
||||
EDITOR_APP_STATUS_EMPTY,
|
||||
EDITOR_APP_STATUS_ERROR,
|
||||
EDITOR_APP_STATUS_INVALID,
|
||||
EDITOR_APP_STATUS_LOADING,
|
||||
EDITOR_APP_STATUS_VALID,
|
||||
|
@ -87,9 +86,8 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
hasAppError() {
|
||||
// Not an invalid config and with `mergedYaml` data missing
|
||||
return this.appStatus === EDITOR_APP_STATUS_ERROR;
|
||||
isMergedYamlAvailable() {
|
||||
return this.ciConfigData?.mergedYaml;
|
||||
},
|
||||
isEmpty() {
|
||||
return this.appStatus === EDITOR_APP_STATUS_EMPTY;
|
||||
|
@ -183,7 +181,7 @@ export default {
|
|||
@click="setCurrentTab($options.tabConstants.MERGED_TAB)"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
|
||||
<gl-alert v-else-if="hasAppError" variant="danger" :dismissible="false">
|
||||
<gl-alert v-else-if="!isMergedYamlAvailable" variant="danger" :dismissible="false">
|
||||
{{ $options.errorTexts.loadMergedYaml }}
|
||||
</gl-alert>
|
||||
<ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" />
|
||||
|
|
|
@ -5,11 +5,17 @@ export const CI_CONFIG_STATUS_VALID = 'VALID';
|
|||
// Values for EDITOR_APP_STATUS_* are frontend specifics and
|
||||
// represent the global state of the pipeline editor app.
|
||||
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
|
||||
export const EDITOR_APP_STATUS_ERROR = 'ERROR';
|
||||
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
|
||||
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
|
||||
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
|
||||
|
||||
export const EDITOR_APP_VALID_STATUSES = [
|
||||
EDITOR_APP_STATUS_EMPTY,
|
||||
EDITOR_APP_STATUS_INVALID,
|
||||
EDITOR_APP_STATUS_LOADING,
|
||||
EDITOR_APP_STATUS_VALID,
|
||||
];
|
||||
|
||||
export const COMMIT_FAILURE = 'COMMIT_FAILURE';
|
||||
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue
|
|||
import {
|
||||
COMMIT_SHA_POLL_INTERVAL,
|
||||
EDITOR_APP_STATUS_EMPTY,
|
||||
EDITOR_APP_STATUS_ERROR,
|
||||
EDITOR_APP_VALID_STATUSES,
|
||||
EDITOR_APP_STATUS_LOADING,
|
||||
LOAD_FAILURE_UNKNOWN,
|
||||
STARTER_TEMPLATE_NAME,
|
||||
|
@ -141,10 +141,10 @@ export default {
|
|||
return { ...ciConfig, stages };
|
||||
},
|
||||
result({ data }) {
|
||||
this.setAppStatus(data?.ciConfig?.status || EDITOR_APP_STATUS_ERROR);
|
||||
this.setAppStatus(data?.ciConfig?.status);
|
||||
},
|
||||
error() {
|
||||
this.reportFailure(LOAD_FAILURE_UNKNOWN);
|
||||
error(err) {
|
||||
this.reportFailure(LOAD_FAILURE_UNKNOWN, [String(err)]);
|
||||
},
|
||||
watchLoading(isLoading) {
|
||||
if (isLoading) {
|
||||
|
@ -242,8 +242,6 @@ export default {
|
|||
await this.$apollo.queries.initialCiFileContent.refetch();
|
||||
},
|
||||
reportFailure(type, reasons = []) {
|
||||
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
this.showFailure = true;
|
||||
this.failureType = type;
|
||||
|
@ -258,7 +256,9 @@ export default {
|
|||
this.currentCiFileContent = this.lastCommittedContent;
|
||||
},
|
||||
setAppStatus(appStatus) {
|
||||
this.$apollo.mutate({ mutation: updateAppStatus, variables: { appStatus } });
|
||||
if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
|
||||
this.$apollo.mutate({ mutation: updateAppStatus, variables: { appStatus } });
|
||||
}
|
||||
},
|
||||
setNewEmptyCiConfigFile() {
|
||||
this.isNewCiConfigFile = true;
|
||||
|
|
|
@ -31,6 +31,9 @@ export default {
|
|||
selectedTemplate: {
|
||||
default: '',
|
||||
},
|
||||
selectedFileTemplateProjectId: {
|
||||
default: null,
|
||||
},
|
||||
outgoingName: {
|
||||
default: '',
|
||||
},
|
||||
|
@ -80,7 +83,7 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
|
||||
onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
|
||||
this.isTemplateSaving = true;
|
||||
|
||||
const body = {
|
||||
|
@ -88,6 +91,7 @@ export default {
|
|||
outgoing_name: outgoingName,
|
||||
project_key: projectKey,
|
||||
service_desk_enabled: this.isEnabled,
|
||||
file_template_project_id: fileTemplateProjectId,
|
||||
};
|
||||
|
||||
return axios
|
||||
|
@ -132,6 +136,7 @@ export default {
|
|||
:custom-email="updatedCustomEmail"
|
||||
:custom-email-enabled="customEmailEnabled"
|
||||
:initial-selected-template="selectedTemplate"
|
||||
:initial-selected-file-template-project-id="selectedFileTemplateProjectId"
|
||||
:initial-outgoing-name="outgoingName"
|
||||
:initial-project-key="projectKey"
|
||||
:templates="templates"
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
<script>
|
||||
import {
|
||||
GlButton,
|
||||
GlFormSelect,
|
||||
GlToggle,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlFormInput,
|
||||
GlLink,
|
||||
} from '@gitlab/ui';
|
||||
import { GlButton, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
|
@ -18,12 +11,12 @@ export default {
|
|||
components: {
|
||||
ClipboardButton,
|
||||
GlButton,
|
||||
GlFormSelect,
|
||||
GlToggle,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlFormInput,
|
||||
GlLink,
|
||||
ServiceDeskTemplateDropdown,
|
||||
},
|
||||
props: {
|
||||
isEnabled: {
|
||||
|
@ -49,6 +42,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
initialSelectedFileTemplateProjectId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
initialOutgoingName: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -73,14 +71,13 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
selectedTemplate: this.initialSelectedTemplate,
|
||||
selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
|
||||
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
|
||||
projectKey: this.initialProjectKey,
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
templateOptions() {
|
||||
return [''].concat(this.templates);
|
||||
},
|
||||
hasProjectKeySupport() {
|
||||
return Boolean(this.customEmailEnabled);
|
||||
},
|
||||
|
@ -100,8 +97,13 @@ export default {
|
|||
selectedTemplate: this.selectedTemplate,
|
||||
outgoingName: this.outgoingName,
|
||||
projectKey: this.projectKey,
|
||||
fileTemplateProjectId: this.selectedFileTemplateProjectId,
|
||||
});
|
||||
},
|
||||
templateChange({ selectedFileTemplateProjectId, selectedTemplate }) {
|
||||
this.selectedFileTemplateProjectId = selectedFileTemplateProjectId;
|
||||
this.selectedTemplate = selectedTemplate;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -193,12 +195,13 @@ export default {
|
|||
<label for="service-desk-template-select" class="mt-3">
|
||||
{{ __('Template to append to all Service Desk issues') }}
|
||||
</label>
|
||||
<gl-form-select
|
||||
id="service-desk-template-select"
|
||||
v-model="selectedTemplate"
|
||||
data-qa-selector="service_desk_template_dropdown"
|
||||
:options="templateOptions"
|
||||
<service-desk-template-dropdown
|
||||
:selected-template="selectedTemplate"
|
||||
:selected-file-template-project-id="selectedFileTemplateProjectId"
|
||||
:templates="templates"
|
||||
@change="templateChange"
|
||||
/>
|
||||
|
||||
<label for="service-desk-email-from-name" class="mt-3">
|
||||
{{ __('Email display name') }}
|
||||
</label>
|
||||
|
@ -210,6 +213,7 @@ export default {
|
|||
<gl-button
|
||||
variant="success"
|
||||
class="gl-mt-5"
|
||||
data-testid="save_service_desk_settings_button"
|
||||
data-qa-selector="save_service_desk_settings_button"
|
||||
:disabled="isTemplateSaving"
|
||||
@click="onSaveTemplate"
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<script>
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
props: {
|
||||
selectedTemplate: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
templates: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedFileTemplateProjectId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
templateOptions() {
|
||||
if (this.searchTerm) {
|
||||
const filteredTemplates = [];
|
||||
for (let i = 0; i < this.templates.length; i += 2) {
|
||||
const sectionName = this.templates[i];
|
||||
const availableTemplates = this.templates[i + 1];
|
||||
|
||||
const matchedTemplates = fuzzaldrinPlus.filter(availableTemplates, this.searchTerm, {
|
||||
key: 'name',
|
||||
});
|
||||
|
||||
if (matchedTemplates.length > 0) {
|
||||
filteredTemplates.push(sectionName, matchedTemplates);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTemplates;
|
||||
}
|
||||
|
||||
return this.templates;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
templateClick(template) {
|
||||
// Clicking on the same template should unselect it
|
||||
if (
|
||||
template.name === this.selectedTemplate &&
|
||||
template.project_id === this.selectedFileTemplateProjectId
|
||||
) {
|
||||
this.$emit('change', {
|
||||
selectedFileTemplateProjectId: null,
|
||||
selectedTemplate: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('change', {
|
||||
selectedFileTemplateProjectId: template.project_id,
|
||||
selectedTemplate: template.key,
|
||||
});
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
defaultDropdownText: __('Choose a template'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-dropdown
|
||||
id="service-desk-template-select"
|
||||
:text="selectedTemplate || $options.i18n.defaultDropdownText"
|
||||
:header-text="$options.i18n.defaultDropdownText"
|
||||
data-qa-selector="service_desk_template_dropdown"
|
||||
:block="true"
|
||||
class="service-desk-template-select"
|
||||
toggle-class="gl-m-0"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type v-model.trim="searchTerm" />
|
||||
</template>
|
||||
<template v-for="item in templateOptions">
|
||||
<gl-dropdown-section-header v-if="!Array.isArray(item)" :key="item">
|
||||
{{ item }}
|
||||
</gl-dropdown-section-header>
|
||||
<template v-else>
|
||||
<gl-dropdown-item
|
||||
v-for="template in item"
|
||||
:key="template.key"
|
||||
:is-check-item="true"
|
||||
:is-checked="
|
||||
template.project_id === selectedFileTemplateProjectId &&
|
||||
template.name === selectedTemplate
|
||||
"
|
||||
@click="() => templateClick(template)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -18,6 +18,7 @@ export default () => {
|
|||
outgoingName,
|
||||
projectKey,
|
||||
selectedTemplate,
|
||||
selectedFileTemplateProjectId,
|
||||
templates,
|
||||
} = el.dataset;
|
||||
|
||||
|
@ -32,6 +33,7 @@ export default () => {
|
|||
outgoingName,
|
||||
projectKey,
|
||||
selectedTemplate,
|
||||
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
|
||||
templates: JSON.parse(templates),
|
||||
},
|
||||
render: (createElement) => createElement(ServiceDeskRoot),
|
||||
|
|
|
@ -122,3 +122,5 @@ class HelpController < ApplicationController
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
::HelpController.prepend_mod
|
||||
|
|
|
@ -11,6 +11,7 @@ class GitlabSchema < GraphQL::Schema
|
|||
AUTHENTICATED_MAX_DEPTH = 20
|
||||
|
||||
# Tracers (order is important)
|
||||
use Gitlab::Graphql::Tracers::ApplicationContextTracer
|
||||
use Gitlab::Graphql::Tracers::LoggerTracer
|
||||
use Gitlab::Graphql::GenericTracing # Old tracer which will be removed eventually
|
||||
use Gitlab::Graphql::Tracers::TimerTracer
|
||||
|
|
|
@ -32,14 +32,17 @@ module IssuablesDescriptionTemplatesHelper
|
|||
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names(project, issuable_type.pluralize)
|
||||
end
|
||||
|
||||
# Overriden on EE::IssuablesDescriptionTemplatesHelper to include inherited templates names
|
||||
def issuable_templates_names(issuable, include_inherited_templates = false)
|
||||
def selected_template(issuable)
|
||||
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
|
||||
all_templates.values.flatten.map { |tpl| tpl[:name] if tpl[:project_id] == ref_project.id }.compact.uniq
|
||||
|
||||
# Only local templates will be listed if licenses for inherited templates are not present
|
||||
all_templates = all_templates.values.flatten.map { |tpl| tpl[:name] }.compact.uniq
|
||||
|
||||
all_templates.find { |tmpl_name| tmpl_name == params[:issuable_template] }
|
||||
end
|
||||
|
||||
def selected_template(issuable)
|
||||
params[:issuable_template] if issuable_templates_names(issuable, true).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
|
||||
def available_service_desk_templates_for(project)
|
||||
issuable_templates(project, 'issue').flatten.to_json
|
||||
end
|
||||
|
||||
def template_names_path(parent, issuable)
|
||||
|
|
|
@ -31,7 +31,7 @@ module Routing
|
|||
end
|
||||
end
|
||||
|
||||
generate_url(masked_params.merge(masked_query_params))
|
||||
generate_url(masked_params.merge(params: masked_query_params))
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -45,7 +45,7 @@ module Routing
|
|||
elsif @request.path_parameters[:controller] == 'groups/insights'
|
||||
default_root_url + "#{Gitlab::Routing.url_helpers.group_insights_path(masked_params)}"
|
||||
else
|
||||
Gitlab::Routing.url_helpers.url_for(masked_params.merge(masked_query_params))
|
||||
Gitlab::Routing.url_helpers.url_for(masked_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DataList
|
||||
def initialize(batch, data_fields_hash, klass)
|
||||
def initialize(batch, data_fields_hash, data_fields_klass)
|
||||
@batch = batch
|
||||
@data_fields_hash = data_fields_hash
|
||||
@klass = klass
|
||||
@data_fields_klass = data_fields_klass
|
||||
end
|
||||
|
||||
def to_array
|
||||
[klass, columns, values]
|
||||
[data_fields_klass, columns, values]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :batch, :data_fields_hash, :klass
|
||||
attr_reader :batch, :data_fields_hash, :data_fields_klass
|
||||
|
||||
def columns
|
||||
data_fields_hash.keys << 'service_id'
|
||||
data_fields_hash.keys << data_fields_foreign_key
|
||||
end
|
||||
|
||||
def data_fields_foreign_key
|
||||
data_fields_klass.reflections['integration'].foreign_key
|
||||
end
|
||||
|
||||
def values
|
||||
|
|
|
@ -373,7 +373,7 @@ class Integration < ApplicationRecord
|
|||
end
|
||||
|
||||
def to_data_fields_hash
|
||||
data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id')
|
||||
data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id', 'integration_id')
|
||||
end
|
||||
|
||||
def event_channel_names
|
||||
|
|
|
@ -731,8 +731,8 @@ class Repository
|
|||
raw_repository.local_branches(sort_by: sort_by, pagination_params: pagination_params)
|
||||
end
|
||||
|
||||
def tags_sorted_by(value)
|
||||
return raw_repository.tags(sort_by: value) if Feature.enabled?(:tags_finder_gitaly, project, default_enabled: :yaml)
|
||||
def tags_sorted_by(value, pagination_params = nil)
|
||||
return raw_repository.tags(sort_by: value, pagination_params: pagination_params) if Feature.enabled?(:tags_finder_gitaly, project, default_enabled: :yaml)
|
||||
|
||||
tags_ruby_sort(value)
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ class BulkUpdateIntegrationService
|
|||
Integration.where(id: batch_ids).update_all(integration_hash)
|
||||
|
||||
if integration.data_fields_present?
|
||||
integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash)
|
||||
integration.data_fields.class.where(data_fields_foreign_key => batch_ids).update_all(data_fields_hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -22,6 +22,11 @@ class BulkUpdateIntegrationService
|
|||
|
||||
attr_reader :integration, :batch
|
||||
|
||||
# service_id or integration_id
|
||||
def data_fields_foreign_key
|
||||
integration.data_fields.class.reflections['integration'].foreign_key
|
||||
end
|
||||
|
||||
def integration_hash
|
||||
integration.to_integration_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
|
||||
end
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
|
||||
custom_email_enabled: "#{Gitlab::ServiceDeskEmail.enabled?}",
|
||||
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
|
||||
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
|
||||
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
|
||||
project_key: "#{@project.service_desk_setting&.project_key}",
|
||||
templates: issuable_templates_names(Issue.new) } }
|
||||
templates: available_service_desk_templates_for(@project) } }
|
||||
- elsif show_callout?('promote_service_desk_dismissed')
|
||||
= render 'shared/promotions/promote_servicedesk'
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
cd $(dirname $0)/..
|
||||
cd "$(dirname "$0")/.." || exit
|
||||
|
||||
app_root=$(pwd)
|
||||
sidekiq_workers=${SIDEKIQ_WORKERS:-1}
|
||||
sidekiq_queues=${SIDEKIQ_QUEUES:-*} # Queues to listen to; default to `*` (all)
|
||||
sidekiq_pidfile="$app_root/tmp/pids/sidekiq-cluster.pid"
|
||||
sidekiq_logfile="$app_root/log/sidekiq.log"
|
||||
gitlab_user=$(ls -l config.ru | awk '{print $3}')
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
|
@ -17,26 +17,26 @@ warn()
|
|||
|
||||
get_sidekiq_pid()
|
||||
{
|
||||
if [ ! -f $sidekiq_pidfile ]; then
|
||||
if [ ! -f "$sidekiq_pidfile" ]; then
|
||||
warn "No pidfile found at $sidekiq_pidfile; is Sidekiq running?"
|
||||
return
|
||||
fi
|
||||
|
||||
cat $sidekiq_pidfile
|
||||
cat "$sidekiq_pidfile"
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
sidekiq_pid=$(get_sidekiq_pid)
|
||||
|
||||
if [ $sidekiq_pid ]; then
|
||||
kill -TERM $sidekiq_pid
|
||||
if [ "$sidekiq_pid" ]; then
|
||||
kill -TERM "$sidekiq_pid"
|
||||
fi
|
||||
}
|
||||
|
||||
restart()
|
||||
{
|
||||
if [ -f $sidekiq_pidfile ]; then
|
||||
if [ -f "$sidekiq_pidfile" ]; then
|
||||
stop
|
||||
fi
|
||||
|
||||
|
@ -53,12 +53,12 @@ start_sidekiq()
|
|||
fi
|
||||
|
||||
# sidekiq-cluster expects an argument per process.
|
||||
for (( i=1; i<=$sidekiq_workers; i++ ))
|
||||
for (( i=1; i<=sidekiq_workers; i++ ))
|
||||
do
|
||||
processes_args+=("${sidekiq_queues}")
|
||||
done
|
||||
|
||||
${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P $sidekiq_pidfile -e $RAILS_ENV "$@" 2>&1 | tee -a $sidekiq_logfile
|
||||
${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P "$sidekiq_pidfile" -e "$RAILS_ENV" "$@" 2>&1 | tee -a "$sidekiq_logfile"
|
||||
}
|
||||
|
||||
cleanup()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
cd $(dirname $0)/.. || exit 1
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
app_root=$(pwd)
|
||||
|
||||
mail_room_pidfile="$app_root/tmp/pids/mail_room.pid"
|
||||
|
@ -9,8 +9,7 @@ mail_room_config="$app_root/config/mail_room.yml"
|
|||
|
||||
get_mail_room_pid()
|
||||
{
|
||||
local pid
|
||||
pid=$(cat $mail_room_pidfile)
|
||||
pid=$(cat "$mail_room_pidfile")
|
||||
if [ -z "$pid" ] ; then
|
||||
echo "Could not find a PID in $mail_room_pidfile"
|
||||
exit 1
|
||||
|
@ -20,13 +19,13 @@ get_mail_room_pid()
|
|||
|
||||
start()
|
||||
{
|
||||
bin/daemon_with_pidfile $mail_room_pidfile bundle exec mail_room --log-exit-as json -q -c $mail_room_config >> $mail_room_logfile 2>&1
|
||||
bin/daemon_with_pidfile "$mail_room_pidfile" bundle exec mail_room --log-exit-as json -q -c "$mail_room_config" >> "$mail_room_logfile" 2>&1
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
get_mail_room_pid
|
||||
kill -TERM $mail_room_pid
|
||||
kill -TERM "$mail_room_pid"
|
||||
}
|
||||
|
||||
restart()
|
||||
|
|
|
@ -32,20 +32,20 @@ if [ -z "$RSYNC" ] ; then
|
|||
RSYNC=rsync
|
||||
fi
|
||||
|
||||
if ! cd $SRC ; then
|
||||
if ! cd "$SRC" ; then
|
||||
echo "cd $SRC failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rsyncjob() {
|
||||
relative_dir="./${1#$SRC}"
|
||||
relative_dir="./${1#"$SRC"}"
|
||||
|
||||
if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then
|
||||
echo "rsync $1 failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$1" >> $LOGFILE
|
||||
echo "$1" >> "$LOGFILE"
|
||||
}
|
||||
|
||||
export LOGFILE SRC DEST RSYNC
|
||||
|
|
10
bin/web
10
bin/web
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
cd $(dirname $0)/..
|
||||
cd "$(dirname "$0")/.."
|
||||
app_root=$(pwd)
|
||||
|
||||
puma_pidfile="$app_root/tmp/pids/puma.pid"
|
||||
|
@ -25,12 +25,12 @@ get_puma_pid()
|
|||
|
||||
start()
|
||||
{
|
||||
spawn_puma &
|
||||
spawn_puma "$@" &
|
||||
}
|
||||
|
||||
start_foreground()
|
||||
{
|
||||
spawn_puma
|
||||
spawn_puma "$@"
|
||||
}
|
||||
|
||||
stop()
|
||||
|
@ -46,10 +46,10 @@ reload()
|
|||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
start "$@"
|
||||
;;
|
||||
start_foreground)
|
||||
start_foreground
|
||||
start_foreground "$@"
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
|
|
|
@ -10,6 +10,7 @@ shift
|
|||
|
||||
# Use set -a to export all variables defined in env_file.
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${env_file}"
|
||||
set +a
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: jira_issue_details_edit_status
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60092
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330628
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::integrations
|
||||
default_enabled: false
|
|
@ -360,6 +360,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
get 'details', on: :member
|
||||
end
|
||||
|
||||
get 'alert_management/:id', to: 'alert_management#details', as: 'alert_management_alert'
|
||||
|
||||
get 'work_items/*work_items_path' => 'work_items#index', as: :work_items
|
||||
|
||||
resource :tracing, only: [:show]
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropCiBuildTraceSections < Gitlab::Database::Migration[1.0]
|
||||
include Gitlab::Database::SchemaHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:dep_ci_build_trace_sections, column: :project_id)
|
||||
end
|
||||
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:dep_ci_build_trace_section_names, column: :project_id)
|
||||
end
|
||||
|
||||
if table_exists?(:dep_ci_build_trace_sections)
|
||||
with_lock_retries do
|
||||
drop_table :dep_ci_build_trace_sections
|
||||
end
|
||||
end
|
||||
|
||||
if table_exists?(:dep_ci_build_trace_section_names)
|
||||
with_lock_retries do
|
||||
drop_table :dep_ci_build_trace_section_names
|
||||
end
|
||||
end
|
||||
|
||||
drop_function('trigger_91dc388a5fe6')
|
||||
end
|
||||
|
||||
def down
|
||||
execute(<<~SQL)
|
||||
CREATE OR REPLACE FUNCTION trigger_91dc388a5fe6() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW."build_id_convert_to_bigint" := NEW."build_id";
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
SQL
|
||||
|
||||
execute_in_transaction(<<~SQL, !table_exists?(:dep_ci_build_trace_section_names))
|
||||
CREATE TABLE dep_ci_build_trace_section_names (
|
||||
id integer NOT NULL,
|
||||
project_id integer NOT NULL,
|
||||
name character varying NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE dep_ci_build_trace_section_names_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE dep_ci_build_trace_section_names_id_seq OWNED BY dep_ci_build_trace_section_names.id;
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_section_names ALTER COLUMN id SET DEFAULT nextval('dep_ci_build_trace_section_names_id_seq'::regclass);
|
||||
ALTER TABLE ONLY dep_ci_build_trace_section_names ADD CONSTRAINT dep_ci_build_trace_section_names_pkey PRIMARY KEY (id);
|
||||
SQL
|
||||
|
||||
execute_in_transaction(<<~SQL, !table_exists?(:dep_ci_build_trace_sections))
|
||||
CREATE TABLE dep_ci_build_trace_sections (
|
||||
project_id integer NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL,
|
||||
byte_start bigint NOT NULL,
|
||||
byte_end bigint NOT NULL,
|
||||
build_id integer NOT NULL,
|
||||
section_name_id integer NOT NULL,
|
||||
build_id_convert_to_bigint bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_sections ADD CONSTRAINT ci_build_trace_sections_pkey PRIMARY KEY (build_id, section_name_id);
|
||||
CREATE TRIGGER trigger_91dc388a5fe6 BEFORE INSERT OR UPDATE ON dep_ci_build_trace_sections FOR EACH ROW EXECUTE FUNCTION trigger_91dc388a5fe6();
|
||||
SQL
|
||||
|
||||
add_concurrent_index :dep_ci_build_trace_section_names, [:project_id, :name], unique: true, name: 'index_dep_ci_build_trace_section_names_on_project_id_and_name'
|
||||
add_concurrent_index :dep_ci_build_trace_sections, :project_id, name: 'index_dep_ci_build_trace_sections_on_project_id'
|
||||
add_concurrent_index :dep_ci_build_trace_sections, :section_name_id, name: 'index_dep_ci_build_trace_sections_on_section_name_id'
|
||||
|
||||
add_concurrent_foreign_key :dep_ci_build_trace_sections, :dep_ci_build_trace_section_names, column: :section_name_id, on_delete: :cascade, name: 'fk_264e112c66'
|
||||
add_concurrent_foreign_key :dep_ci_build_trace_sections, :projects, column: :project_id, on_delete: :cascade, name: 'fk_ab7c104e26'
|
||||
add_concurrent_foreign_key :dep_ci_build_trace_section_names, :projects, column: :project_id, on_delete: :cascade, name: 'fk_f8cd72cd26'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def execute_in_transaction(sql, condition)
|
||||
return unless condition
|
||||
|
||||
transaction do
|
||||
execute(sql)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
20f10ae28d439de1d07357ab7e977dae88feaaedb16770820350a9bf8242817f
|
|
@ -86,15 +86,6 @@ RETURN NULL;
|
|||
END
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION trigger_91dc388a5fe6() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW."build_id_convert_to_bigint" := NEW."build_id";
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE audit_events (
|
||||
id bigint NOT NULL,
|
||||
author_id integer NOT NULL,
|
||||
|
@ -13235,33 +13226,6 @@ CREATE SEQUENCE dast_sites_id_seq
|
|||
|
||||
ALTER SEQUENCE dast_sites_id_seq OWNED BY dast_sites.id;
|
||||
|
||||
CREATE TABLE dep_ci_build_trace_section_names (
|
||||
id integer NOT NULL,
|
||||
project_id integer NOT NULL,
|
||||
name character varying NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE dep_ci_build_trace_section_names_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE dep_ci_build_trace_section_names_id_seq OWNED BY dep_ci_build_trace_section_names.id;
|
||||
|
||||
CREATE TABLE dep_ci_build_trace_sections (
|
||||
project_id integer NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL,
|
||||
byte_start bigint NOT NULL,
|
||||
byte_end bigint NOT NULL,
|
||||
build_id integer NOT NULL,
|
||||
section_name_id integer NOT NULL,
|
||||
build_id_convert_to_bigint bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE dependency_proxy_blobs (
|
||||
id integer NOT NULL,
|
||||
group_id integer NOT NULL,
|
||||
|
@ -21410,8 +21374,6 @@ ALTER TABLE ONLY dast_site_validations ALTER COLUMN id SET DEFAULT nextval('dast
|
|||
|
||||
ALTER TABLE ONLY dast_sites ALTER COLUMN id SET DEFAULT nextval('dast_sites_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_section_names ALTER COLUMN id SET DEFAULT nextval('dep_ci_build_trace_section_names_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY dependency_proxy_blobs ALTER COLUMN id SET DEFAULT nextval('dependency_proxy_blobs_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY dependency_proxy_group_settings ALTER COLUMN id SET DEFAULT nextval('dependency_proxy_group_settings_id_seq'::regclass);
|
||||
|
@ -22705,9 +22667,6 @@ ALTER TABLE ONLY ci_build_trace_chunks
|
|||
ALTER TABLE ONLY ci_build_trace_metadata
|
||||
ADD CONSTRAINT ci_build_trace_metadata_pkey PRIMARY KEY (build_id);
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_sections
|
||||
ADD CONSTRAINT ci_build_trace_sections_pkey PRIMARY KEY (build_id, section_name_id);
|
||||
|
||||
ALTER TABLE ONLY ci_builds_metadata
|
||||
ADD CONSTRAINT ci_builds_metadata_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -22960,9 +22919,6 @@ ALTER TABLE ONLY dast_site_validations
|
|||
ALTER TABLE ONLY dast_sites
|
||||
ADD CONSTRAINT dast_sites_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_section_names
|
||||
ADD CONSTRAINT dep_ci_build_trace_section_names_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY dependency_proxy_blobs
|
||||
ADD CONSTRAINT dependency_proxy_blobs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -25675,12 +25631,6 @@ CREATE INDEX index_dast_sites_on_dast_site_validation_id ON dast_sites USING btr
|
|||
|
||||
CREATE UNIQUE INDEX index_dast_sites_on_project_id_and_url ON dast_sites USING btree (project_id, url);
|
||||
|
||||
CREATE UNIQUE INDEX index_dep_ci_build_trace_section_names_on_project_id_and_name ON dep_ci_build_trace_section_names USING btree (project_id, name);
|
||||
|
||||
CREATE INDEX index_dep_ci_build_trace_sections_on_project_id ON dep_ci_build_trace_sections USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_dep_ci_build_trace_sections_on_section_name_id ON dep_ci_build_trace_sections USING btree (section_name_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON dependency_proxy_manifests USING btree (group_id, file_name, status);
|
||||
|
||||
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependency_proxy_blobs USING btree (group_id, file_name);
|
||||
|
@ -28733,8 +28683,6 @@ CREATE TRIGGER chat_names_loose_fk_trigger AFTER DELETE ON chat_names REFERENCIN
|
|||
|
||||
CREATE TRIGGER ci_runners_loose_fk_trigger AFTER DELETE ON ci_runners REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
|
||||
|
||||
CREATE TRIGGER trigger_91dc388a5fe6 BEFORE INSERT OR UPDATE ON dep_ci_build_trace_sections FOR EACH ROW EXECUTE FUNCTION trigger_91dc388a5fe6();
|
||||
|
||||
CREATE TRIGGER trigger_delete_project_namespace_on_project_delete AFTER DELETE ON projects FOR EACH ROW WHEN ((old.project_namespace_id IS NOT NULL)) EXECUTE FUNCTION delete_associated_project_namespace();
|
||||
|
||||
CREATE TRIGGER trigger_has_external_issue_tracker_on_delete AFTER DELETE ON integrations FOR EACH ROW WHEN ((((old.category)::text = 'issue_tracker'::text) AND (old.active = true) AND (old.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker();
|
||||
|
@ -28888,9 +28836,6 @@ ALTER TABLE ONLY projects
|
|||
ALTER TABLE ONLY ci_pipelines
|
||||
ADD CONSTRAINT fk_262d4c2d19 FOREIGN KEY (auto_canceled_by_id) REFERENCES ci_pipelines(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_sections
|
||||
ADD CONSTRAINT fk_264e112c66 FOREIGN KEY (section_name_id) REFERENCES dep_ci_build_trace_section_names(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY geo_event_log
|
||||
ADD CONSTRAINT fk_27548c6db3 FOREIGN KEY (hashed_storage_migrated_event_id) REFERENCES geo_hashed_storage_migrated_events(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -29293,9 +29238,6 @@ ALTER TABLE ONLY boards
|
|||
ALTER TABLE ONLY member_tasks
|
||||
ADD CONSTRAINT fk_ab636303dd FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_sections
|
||||
ADD CONSTRAINT fk_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_sources_pipelines
|
||||
ADD CONSTRAINT fk_acd9737679 FOREIGN KEY (source_project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -29587,9 +29529,6 @@ ALTER TABLE ONLY cluster_agents
|
|||
ALTER TABLE ONLY protected_tag_create_access_levels
|
||||
ADD CONSTRAINT fk_f7dfda8c51 FOREIGN KEY (protected_tag_id) REFERENCES protected_tags(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY dep_ci_build_trace_section_names
|
||||
ADD CONSTRAINT fk_f8cd72cd26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_stages
|
||||
ADD CONSTRAINT fk_fb57e6cc56 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -135,6 +135,25 @@ link in the issue sidebar.
|
|||
|
||||
![containing epic](img/containing_epic.png)
|
||||
|
||||
## View epics list
|
||||
|
||||
In a group, the left sidebar displays the total count of open epics.
|
||||
This number indicates all epics associated with the group and its subgroups, including epics you
|
||||
might not have permission to view.
|
||||
|
||||
To view epics in a group:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Epics**.
|
||||
|
||||
### Cached epic count
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299540) in GitLab 13.11 [with a flag](../../../administration/feature_flags.md) named `cached_sidebar_open_epics_count`. Enabled by default.
|
||||
> - Enabled on self-managed and on GitLab.com in GitLab 14.0. [Feature flag `cached_sidebar_open_epics_count`](https://gitlab.com/gitlab-org/gitlab/-/issues/327320) removed.
|
||||
|
||||
The total count of open epics displayed in the sidebar is cached if higher
|
||||
than 1000. The cached value is rounded to thousands or millions and updated every 24 hours.
|
||||
|
||||
## Search for an epic from epics list page
|
||||
|
||||
> - Introduced in GitLab 10.5.
|
||||
|
@ -386,11 +405,3 @@ To remove a child epic from a parent epic:
|
|||
|
||||
1. Select the <kbd>x</kbd> button in the parent epic's list of epics.
|
||||
1. Select **Remove** in the **Remove epic** warning message.
|
||||
|
||||
## Cached epic count
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299540) in GitLab 13.11.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/327320) in GitLab 14.0.
|
||||
|
||||
In a group, the sidebar displays the total count of open epics and this value is cached if higher
|
||||
than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours.
|
||||
|
|
|
@ -137,15 +137,23 @@ You can use these placeholders to be automatically replaced in each email:
|
|||
|
||||
#### New Service Desk issues
|
||||
|
||||
You can select one [issue description template](description_templates.md#create-an-issue-template)
|
||||
You can select one [description template](description_templates.md#create-an-issue-template)
|
||||
**per project** to be appended to every new Service Desk issue's description.
|
||||
Issue description templates should reside in your repository's `.gitlab/issue_templates/` directory.
|
||||
|
||||
To use a custom issue template with Service Desk, in your project:
|
||||
You can set description templates at various levels:
|
||||
|
||||
1. [Create a description template](description_templates.md#create-an-issue-template)
|
||||
1. Go to **Settings > General > Service Desk**.
|
||||
1. From the dropdown **Template to append to all Service Desk issues**, select your template.
|
||||
- The entire [instance](description_templates.md#set-instance-level-description-templates).
|
||||
- A specific [group or subgroup](description_templates.md#set-group-level-description-templates).
|
||||
- A specific [project](description_templates.md#set-a-default-template-for-merge-requests-and-issues).
|
||||
|
||||
The templates are inherited. For example, in a project, you can also access templates set for the instance or the project’s parent groups.
|
||||
|
||||
To use a custom description template with Service Desk:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. [Create a description template](description_templates.md#create-an-issue-template).
|
||||
1. On the left sidebar, select **Settings > General > Service Desk**.
|
||||
1. From the dropdown **Template to append to all Service Desk issues**, search or select your template.
|
||||
|
||||
### Using a custom email display name
|
||||
|
||||
|
@ -156,7 +164,8 @@ this name in the `From` header. The default display name is `GitLab Support Bot`
|
|||
|
||||
To edit the custom email display name:
|
||||
|
||||
1. In a project, go to **Settings > General > Service Desk**.
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > General > Service Desk**.
|
||||
1. Enter a new name in **Email display name**.
|
||||
1. Select **Save Changes**.
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module AlertManagement
|
||||
class Alert < Grape::Entity
|
||||
expose :iid
|
||||
expose :title
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -33,7 +33,7 @@ module API
|
|||
def todo_target_url(todo)
|
||||
return design_todo_target_url(todo) if todo.for_design?
|
||||
|
||||
target_type = todo.target_type.underscore
|
||||
target_type = todo.target_type.gsub('::', '_').underscore
|
||||
target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
|
||||
|
||||
Gitlab::Routing
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Groups
|
||||
module Common
|
||||
module Pipelines
|
||||
class MilestonesPipeline
|
||||
include NdjsonPipeline
|
|
@ -28,7 +28,7 @@ module BulkImports
|
|||
stage: 1
|
||||
},
|
||||
milestones: {
|
||||
pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline,
|
||||
pipeline: BulkImports::Common::Pipelines::MilestonesPipeline,
|
||||
stage: 1
|
||||
},
|
||||
badges: {
|
||||
|
|
|
@ -19,6 +19,10 @@ module BulkImports
|
|||
pipeline: BulkImports::Common::Pipelines::LabelsPipeline,
|
||||
stage: 2
|
||||
},
|
||||
milestones: {
|
||||
pipeline: BulkImports::Common::Pipelines::MilestonesPipeline,
|
||||
stage: 2
|
||||
},
|
||||
issues: {
|
||||
pipeline: BulkImports::Projects::Pipelines::IssuesPipeline,
|
||||
stage: 3
|
||||
|
|
|
@ -156,8 +156,6 @@ dast_site_profiles_pipelines: :gitlab_main
|
|||
dast_sites: :gitlab_main
|
||||
dast_site_tokens: :gitlab_main
|
||||
dast_site_validations: :gitlab_main
|
||||
dep_ci_build_trace_section_names: :gitlab_main
|
||||
dep_ci_build_trace_sections: :gitlab_main
|
||||
dependency_proxy_blobs: :gitlab_main
|
||||
dependency_proxy_group_settings: :gitlab_main
|
||||
dependency_proxy_image_ttl_group_policies: :gitlab_main
|
||||
|
|
|
@ -198,9 +198,9 @@ module Gitlab
|
|||
|
||||
# Returns an Array of Tags
|
||||
#
|
||||
def tags(sort_by: nil)
|
||||
def tags(sort_by: nil, pagination_params: nil)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_ref_client.tags(sort_by: sort_by)
|
||||
gitaly_ref_client.tags(sort_by: sort_by, pagination_params: pagination_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -77,8 +77,8 @@ module Gitlab
|
|||
consume_find_local_branches_response(response)
|
||||
end
|
||||
|
||||
def tags(sort_by: nil)
|
||||
request = Gitaly::FindAllTagsRequest.new(repository: @gitaly_repo)
|
||||
def tags(sort_by: nil, pagination_params: nil)
|
||||
request = Gitaly::FindAllTagsRequest.new(repository: @gitaly_repo, pagination_params: pagination_params)
|
||||
request.sort_by = sort_tags_by_param(sort_by) if sort_by
|
||||
|
||||
response = GitalyClient.call(@storage, :ref_service, :find_all_tags, request, timeout: GitalyClient.medium_timeout)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Graphql
|
||||
module Tracers
|
||||
# This graphql-ruby tracer sets up `ApplicationContext` for certain operations.
|
||||
class ApplicationContextTracer
|
||||
def self.use(schema)
|
||||
schema.tracer(self.new)
|
||||
end
|
||||
|
||||
# See docs on expected interface for trace
|
||||
# https://graphql-ruby.org/api-doc/1.12.17/GraphQL/Tracing
|
||||
def trace(key, data)
|
||||
case key
|
||||
when "execute_query"
|
||||
operation = known_operation(data)
|
||||
|
||||
::Gitlab::ApplicationContext.with_context(caller_id: operation.to_caller_id) do
|
||||
yield
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def known_operation(data)
|
||||
# The library guarantees that we should have :query for execute_query, but we're being defensive here
|
||||
query = data.fetch(:query, nil)
|
||||
|
||||
return ::Gitlab::Graphql::KnownOperations.UNKNOWN unless query
|
||||
|
||||
::Gitlab::Graphql::KnownOperations.default.from_query(query)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19654,15 +19654,9 @@ msgstr ""
|
|||
msgid "JiraService|Events for %{noteable_model_name} are disabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Failed to load Jira issue statuses. View the issue in Jira, or reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Failed to load Jira issue. View the issue in Jira, or reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Failed to update Jira issue status. View the issue in Jira, or reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Fetch issue types for this Jira project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19711,9 +19705,6 @@ msgstr ""
|
|||
msgid "JiraService|Move to Done"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|No available statuses"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Open Jira"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -49,7 +49,12 @@ module QA
|
|||
click_element(:target_namespace_selector_dropdown)
|
||||
click_element(:target_group_dropdown_item, group_name: target_group_path)
|
||||
fill_element(:project_path_field, project_name)
|
||||
click_element(:import_button)
|
||||
|
||||
retry_until do
|
||||
click_element(:import_button)
|
||||
# Make sure import started before waiting for completion
|
||||
has_no_element?(:import_status_indicator, text: "Not started", wait: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
{"id":7642,"title":"v4.0","project_id":null,"description":"Et laudantium enim omnis ea reprehenderit iure.","due_date":null,"created_at":"2019-11-20T17:02:14.336Z","updated_at":"2019-11-20T17:02:14.336Z","state":"closed","iid":5,"start_date":null,"group_id":4351}
|
||||
{"id":7641,"title":"v3.0","project_id":null,"description":"Et repellat culpa nemo consequatur ut reprehenderit.","due_date":null,"created_at":"2019-11-20T17:02:14.323Z","updated_at":"2019-11-20T17:02:14.323Z","state":"active","iid":4,"start_date":null,"group_id":4351}
|
||||
{"id":7640,"title":"v2.0","project_id":null,"description":"Velit cupiditate est neque voluptates iste rem sunt.","due_date":null,"created_at":"2019-11-20T17:02:14.309Z","updated_at":"2019-11-20T17:02:14.309Z","state":"active","iid":3,"start_date":null,"group_id":4351}
|
||||
{"id":7639,"title":"v1.0","project_id":null,"description":"Amet velit repellat ut rerum aut cum.","due_date":null,"created_at":"2019-11-20T17:02:14.296Z","updated_at":"2019-11-20T17:02:14.296Z","state":"active","iid":2,"start_date":null,"group_id":4351}
|
||||
{"id":7638,"title":"v0.0","project_id":null,"description":"Ea quia asperiores ut modi dolorem sunt non numquam.","due_date":null,"created_at":"2019-11-20T17:02:14.282Z","updated_at":"2019-11-20T17:02:14.282Z","state":"active","iid":1,"start_date":null,"group_id":4351}
|
|
@ -9,7 +9,6 @@ import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
|
|||
import {
|
||||
CREATE_TAB,
|
||||
EDITOR_APP_STATUS_EMPTY,
|
||||
EDITOR_APP_STATUS_ERROR,
|
||||
EDITOR_APP_STATUS_LOADING,
|
||||
EDITOR_APP_STATUS_INVALID,
|
||||
EDITOR_APP_STATUS_VALID,
|
||||
|
@ -18,7 +17,7 @@ import {
|
|||
TABS_INDEX,
|
||||
} from '~/pipeline_editor/constants';
|
||||
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
||||
import { mockLintResponse, mockCiYml } from '../mock_data';
|
||||
import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
|
||||
|
||||
describe('Pipeline editor tabs component', () => {
|
||||
let wrapper;
|
||||
|
@ -143,7 +142,7 @@ describe('Pipeline editor tabs component', () => {
|
|||
|
||||
describe('when there is a fetch error', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ appStatus: EDITOR_APP_STATUS_ERROR });
|
||||
createComponent({ props: { ciConfigData: mockLintResponseWithoutMerged } });
|
||||
});
|
||||
|
||||
it('show an error message', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
|
||||
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
|
||||
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
|
||||
|
||||
export const mockProjectNamespace = 'user1';
|
||||
|
@ -393,6 +393,14 @@ export const mockLintResponse = {
|
|||
],
|
||||
};
|
||||
|
||||
export const mockLintResponseWithoutMerged = {
|
||||
valid: false,
|
||||
status: CI_CONFIG_STATUS_INVALID,
|
||||
errors: ['error'],
|
||||
warnings: [],
|
||||
jobs: [],
|
||||
};
|
||||
|
||||
export const mockJobs = [
|
||||
{
|
||||
name: 'job_1',
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export const TEMPLATES = [
|
||||
'Project #1',
|
||||
[
|
||||
{ name: 'Bug', project_id: 1 },
|
||||
{ name: 'Documentation', project_id: 1 },
|
||||
{ name: 'Security release', project_id: 1 },
|
||||
],
|
||||
];
|
|
@ -21,6 +21,7 @@ describe('ServiceDeskRoot', () => {
|
|||
outgoingName: 'GitLab Support Bot',
|
||||
projectKey: 'key',
|
||||
selectedTemplate: 'Bug',
|
||||
selectedFileTemplateProjectId: 42,
|
||||
templates: ['Bug', 'Documentation'],
|
||||
};
|
||||
|
||||
|
@ -52,6 +53,7 @@ describe('ServiceDeskRoot', () => {
|
|||
initialOutgoingName: provideData.outgoingName,
|
||||
initialProjectKey: provideData.projectKey,
|
||||
initialSelectedTemplate: provideData.selectedTemplate,
|
||||
initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
|
||||
isEnabled: provideData.initialIsEnabled,
|
||||
isTemplateSaving: false,
|
||||
templates: provideData.templates,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GlButton, GlFormSelect, GlLoadingIcon, GlToggle } from '@gitlab/ui';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
|
||||
|
@ -13,12 +13,12 @@ describe('ServiceDeskSetting', () => {
|
|||
const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
|
||||
const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-describer');
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findTemplateDropdown = () => wrapper.find(GlFormSelect);
|
||||
const findTemplateDropdown = () => wrapper.find(GlDropdown);
|
||||
const findToggle = () => wrapper.find(GlToggle);
|
||||
|
||||
const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
|
||||
const createComponent = ({ props = {} } = {}) =>
|
||||
extendedWrapper(
|
||||
mountFunction(ServiceDeskSetting, {
|
||||
shallowMount(ServiceDeskSetting, {
|
||||
propsData: {
|
||||
isEnabled: true,
|
||||
...props,
|
||||
|
@ -144,63 +144,6 @@ describe('ServiceDeskSetting', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('templates dropdown', () => {
|
||||
it('renders a dropdown to choose a template', () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
expect(findTemplateDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a dropdown with a default value of ""', () => {
|
||||
wrapper = createComponent({ mountFunction: mount });
|
||||
|
||||
expect(findTemplateDropdown().element.value).toEqual('');
|
||||
});
|
||||
|
||||
it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
|
||||
const templates = ['Bug', 'Documentation', 'Security release'];
|
||||
|
||||
wrapper = createComponent({
|
||||
props: { initialSelectedTemplate: 'Bug', templates },
|
||||
mountFunction: mount,
|
||||
});
|
||||
|
||||
expect(findTemplateDropdown().element.value).toEqual('Bug');
|
||||
});
|
||||
|
||||
it('renders a dropdown with no options when the project has no templates', () => {
|
||||
wrapper = createComponent({
|
||||
props: { templates: [] },
|
||||
mountFunction: mount,
|
||||
});
|
||||
|
||||
// The dropdown by default has one empty option
|
||||
expect(findTemplateDropdown().element.children).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a dropdown with options when the project has templates', () => {
|
||||
const templates = ['Bug', 'Documentation', 'Security release'];
|
||||
|
||||
wrapper = createComponent({
|
||||
props: { templates },
|
||||
mountFunction: mount,
|
||||
});
|
||||
|
||||
// An empty-named template is prepended so the user can select no template
|
||||
const expectedTemplates = [''].concat(templates);
|
||||
|
||||
const dropdown = findTemplateDropdown();
|
||||
const dropdownList = Array.from(dropdown.element.children).map(
|
||||
(option) => option.innerText,
|
||||
);
|
||||
|
||||
expect(dropdown.element.children).toHaveLength(expectedTemplates.length);
|
||||
expect(dropdownList.includes('Bug')).toEqual(true);
|
||||
expect(dropdownList.includes('Documentation')).toEqual(true);
|
||||
expect(dropdownList.includes('Security release')).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('save button', () => {
|
||||
|
@ -214,6 +157,7 @@ describe('ServiceDeskSetting', () => {
|
|||
wrapper = createComponent({
|
||||
props: {
|
||||
initialSelectedTemplate: 'Bug',
|
||||
initialSelectedFileTemplateProjectId: 42,
|
||||
initialOutgoingName: 'GitLab Support Bot',
|
||||
initialProjectKey: 'key',
|
||||
},
|
||||
|
@ -225,6 +169,7 @@ describe('ServiceDeskSetting', () => {
|
|||
|
||||
const payload = {
|
||||
selectedTemplate: 'Bug',
|
||||
fileTemplateProjectId: 42,
|
||||
outgoingName: 'GitLab Support Bot',
|
||||
projectKey: 'key',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import ServiceDeskTemplateDropdown from '~/projects/settings_service_desk/components/service_desk_setting.vue';
|
||||
import { TEMPLATES } from './mock_data';
|
||||
|
||||
describe('ServiceDeskTemplateDropdown', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTemplateDropdown = () => wrapper.find(GlDropdown);
|
||||
|
||||
const createComponent = ({ props = {} } = {}) =>
|
||||
extendedWrapper(
|
||||
mount(ServiceDeskTemplateDropdown, {
|
||||
propsData: {
|
||||
isEnabled: true,
|
||||
...props,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
describe('templates dropdown', () => {
|
||||
it('renders a dropdown to choose a template', () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
expect(findTemplateDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a dropdown with a default value of "Choose a template"', () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
expect(findTemplateDropdown().props('text')).toEqual('Choose a template');
|
||||
});
|
||||
|
||||
it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
|
||||
const templates = TEMPLATES;
|
||||
|
||||
wrapper = createComponent({
|
||||
props: { initialSelectedTemplate: 'Bug', initialSelectedTemplateProjectId: 1, templates },
|
||||
});
|
||||
|
||||
expect(findTemplateDropdown().props('text')).toEqual('Bug');
|
||||
});
|
||||
|
||||
it('renders a dropdown with header items', () => {
|
||||
wrapper = createComponent({
|
||||
props: { templates: TEMPLATES },
|
||||
});
|
||||
|
||||
const headerItems = wrapper.findAll(GlDropdownSectionHeader);
|
||||
|
||||
expect(headerItems).toHaveLength(1);
|
||||
expect(headerItems.at(0).text()).toBe(TEMPLATES[0]);
|
||||
});
|
||||
|
||||
it('renders a dropdown with options when the project has templates', () => {
|
||||
const templates = TEMPLATES;
|
||||
|
||||
wrapper = createComponent({
|
||||
props: { templates },
|
||||
});
|
||||
|
||||
const expectedTemplates = templates[1];
|
||||
|
||||
const items = wrapper.findAll(GlDropdownItem);
|
||||
const dropdownList = expectedTemplates.map((_, index) => items.at(index).text());
|
||||
|
||||
expect(items).toHaveLength(expectedTemplates.length);
|
||||
expect(dropdownList.includes('Bug')).toEqual(true);
|
||||
expect(dropdownList.includes('Documentation')).toEqual(true);
|
||||
expect(dropdownList.includes('Security release')).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -44,7 +44,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#issuable_templates_names' do
|
||||
describe '#selected_template' do
|
||||
let_it_be(:project) { build(:project) }
|
||||
|
||||
before do
|
||||
|
@ -63,7 +63,14 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
|
|||
end
|
||||
|
||||
it 'returns project templates' do
|
||||
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
|
||||
value = [
|
||||
"",
|
||||
[
|
||||
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
|
||||
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
|
||||
]
|
||||
].to_json
|
||||
expect(helper.available_service_desk_templates_for(@project)).to eq(value)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,7 +78,8 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
|
|||
let(:templates) { {} }
|
||||
|
||||
it 'returns empty array' do
|
||||
expect(helper.issuable_templates_names(Issue.new)).to eq([])
|
||||
value = [].to_json
|
||||
expect(helper.available_service_desk_templates_for(@project)).to eq(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -178,6 +178,26 @@ RSpec.describe ::Routing::PseudonymizationHelper do
|
|||
|
||||
it_behaves_like 'masked url'
|
||||
end
|
||||
|
||||
context 'when query string has keys with the same names as path params' do
|
||||
let(:masked_url) { "http://localhost/dashboard/issues?action=foobar&scope=all&state=opened" }
|
||||
let(:request) do
|
||||
double(:Request,
|
||||
path_parameters: {
|
||||
controller: 'dashboard',
|
||||
action: 'issues'
|
||||
},
|
||||
protocol: 'http',
|
||||
host: 'localhost',
|
||||
query_string: 'action=foobar&scope=all&state=opened')
|
||||
end
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:request).and_return(request)
|
||||
end
|
||||
|
||||
it_behaves_like 'masked url'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when url has no params to mask' do
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline do
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:bulk_import) { create(:bulk_import, user: user) }
|
||||
let(:tracker) { create(:bulk_import_tracker, entity: entity) }
|
||||
let(:context) { BulkImports::Pipeline::Context.new(tracker) }
|
||||
let(:source_project_id) { nil } # if set, then exported_milestone is a project milestone
|
||||
let(:source_group_id) { nil } # if set, then exported_milestone is a group milestone
|
||||
let(:exported_milestone_for_project) do
|
||||
exported_milestone_for_group.merge(
|
||||
'events' => [{
|
||||
'project_id' => source_project_id,
|
||||
'author_id' => 9,
|
||||
'created_at' => "2021-08-12T19:12:49.810Z",
|
||||
'updated_at' => "2021-08-12T19:12:49.810Z",
|
||||
'target_type' => "Milestone",
|
||||
'group_id' => source_group_id,
|
||||
'fingerprint' => 'f270eb9b27d0',
|
||||
'id' => 66,
|
||||
'action' => "created"
|
||||
}]
|
||||
)
|
||||
end
|
||||
|
||||
let(:exported_milestone_for_group) do
|
||||
{
|
||||
'id' => 1,
|
||||
'title' => "v1.0",
|
||||
'project_id' => source_project_id,
|
||||
'description' => "Amet velit repellat ut rerum aut cum.",
|
||||
'due_date' => "2019-11-22",
|
||||
'created_at' => "2019-11-20T17:02:14.296Z",
|
||||
'updated_at' => "2019-11-20T17:02:14.296Z",
|
||||
'state' => "active",
|
||||
'iid' => 2,
|
||||
'start_date' => "2019-11-21",
|
||||
'group_id' => source_group_id
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
|
||||
allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
|
||||
allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: exported_milestones))
|
||||
end
|
||||
end
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
shared_examples 'bulk_imports milestones pipeline' do
|
||||
let(:tested_entity) { nil }
|
||||
|
||||
describe '#run' do
|
||||
it 'imports milestones into destination' do
|
||||
expect { subject.run }.to change(Milestone, :count).by(1)
|
||||
|
||||
imported_milestone = tested_entity.milestones.first
|
||||
|
||||
expect(imported_milestone.title).to eq("v1.0")
|
||||
expect(imported_milestone.description).to eq("Amet velit repellat ut rerum aut cum.")
|
||||
expect(imported_milestone.due_date.to_s).to eq("2019-11-22")
|
||||
expect(imported_milestone.created_at).to eq("2019-11-20T17:02:14.296Z")
|
||||
expect(imported_milestone.updated_at).to eq("2019-11-20T17:02:14.296Z")
|
||||
expect(imported_milestone.start_date.to_s).to eq("2019-11-21")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load' do
|
||||
context 'when milestone is not persisted' do
|
||||
it 'saves the milestone' do
|
||||
milestone = build(:milestone, group: group)
|
||||
|
||||
expect(milestone).to receive(:save!)
|
||||
|
||||
subject.load(context, milestone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when milestone is persisted' do
|
||||
it 'does not save milestone' do
|
||||
milestone = create(:milestone, group: group)
|
||||
|
||||
expect(milestone).not_to receive(:save!)
|
||||
|
||||
subject.load(context, milestone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when milestone is missing' do
|
||||
it 'returns' do
|
||||
expect(subject.load(context, nil)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'group milestone' do
|
||||
let(:exported_milestones) { [[exported_milestone_for_group, 0]] }
|
||||
let(:entity) do
|
||||
create(
|
||||
:bulk_import_entity,
|
||||
group: group,
|
||||
bulk_import: bulk_import,
|
||||
source_full_path: 'source/full/path',
|
||||
destination_name: 'My Destination Group',
|
||||
destination_namespace: group.full_path
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'bulk_imports milestones pipeline' do
|
||||
let(:tested_entity) { group }
|
||||
let(:source_group_id) { 1 }
|
||||
end
|
||||
end
|
||||
|
||||
context 'project milestone' do
|
||||
let(:project) { create(:project, group: group) }
|
||||
let(:exported_milestones) { [[exported_milestone_for_project, 0]] }
|
||||
|
||||
let(:entity) do
|
||||
create(
|
||||
:bulk_import_entity,
|
||||
:project_entity,
|
||||
project: project,
|
||||
bulk_import: bulk_import,
|
||||
source_full_path: 'source/full/path',
|
||||
destination_name: 'My Destination Project',
|
||||
destination_namespace: group.full_path
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'bulk_imports milestones pipeline' do
|
||||
let(:tested_entity) { project }
|
||||
let(:source_project_id) { 1 }
|
||||
|
||||
it 'imports events' do
|
||||
subject.run
|
||||
|
||||
imported_event = tested_entity.milestones.first.events.first
|
||||
|
||||
expect(imported_event.created_at).to eq("2021-08-12T19:12:49.810Z")
|
||||
expect(imported_event.updated_at).to eq("2021-08-12T19:12:49.810Z")
|
||||
expect(imported_event.target_type).to eq("Milestone")
|
||||
expect(imported_event.fingerprint).to eq("f270eb9b27d0")
|
||||
expect(imported_event.action).to eq("created")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,73 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
|
||||
let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/milestones.ndjson.gz' }
|
||||
let_it_be(:entity) do
|
||||
create(
|
||||
:bulk_import_entity,
|
||||
group: group,
|
||||
bulk_import: bulk_import,
|
||||
source_full_path: 'source/full/path',
|
||||
destination_name: 'My Destination Group',
|
||||
destination_namespace: group.full_path
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
|
||||
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
|
||||
|
||||
let(:tmpdir) { Dir.mktmpdir }
|
||||
|
||||
before do
|
||||
FileUtils.copy_file(filepath, File.join(tmpdir, 'milestones.ndjson.gz'))
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
describe '#run' do
|
||||
it 'imports group milestones into destination group and removes tmpdir' do
|
||||
allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
|
||||
allow_next_instance_of(BulkImports::FileDownloadService) do |service|
|
||||
allow(service).to receive(:execute)
|
||||
end
|
||||
|
||||
expect { subject.run }.to change(Milestone, :count).by(5)
|
||||
expect(group.milestones.pluck(:title)).to contain_exactly('v4.0', 'v3.0', 'v2.0', 'v1.0', 'v0.0')
|
||||
expect(File.directory?(tmpdir)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load' do
|
||||
context 'when milestone is not persisted' do
|
||||
it 'saves the milestone' do
|
||||
milestone = build(:milestone, group: group)
|
||||
|
||||
expect(milestone).to receive(:save!)
|
||||
|
||||
subject.load(context, milestone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when milestone is persisted' do
|
||||
it 'does not save milestone' do
|
||||
milestone = create(:milestone, group: group)
|
||||
|
||||
expect(milestone).not_to receive(:save!)
|
||||
|
||||
subject.load(context, milestone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when milestone is missing' do
|
||||
it 'returns' do
|
||||
expect(subject.load(context, nil)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,7 +12,7 @@ RSpec.describe BulkImports::Groups::Stage do
|
|||
[1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline],
|
||||
[1, BulkImports::Groups::Pipelines::MembersPipeline],
|
||||
[1, BulkImports::Common::Pipelines::LabelsPipeline],
|
||||
[1, BulkImports::Groups::Pipelines::MilestonesPipeline],
|
||||
[1, BulkImports::Common::Pipelines::MilestonesPipeline],
|
||||
[1, BulkImports::Groups::Pipelines::BadgesPipeline],
|
||||
[2, BulkImports::Common::Pipelines::BoardsPipeline]
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe BulkImports::Projects::Stage do
|
|||
[0, BulkImports::Projects::Pipelines::ProjectPipeline],
|
||||
[1, BulkImports::Projects::Pipelines::RepositoryPipeline],
|
||||
[2, BulkImports::Common::Pipelines::LabelsPipeline],
|
||||
[2, BulkImports::Common::Pipelines::MilestonesPipeline],
|
||||
[3, BulkImports::Projects::Pipelines::IssuesPipeline],
|
||||
[4, BulkImports::Common::Pipelines::BoardsPipeline],
|
||||
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
|
||||
|
|
|
@ -125,7 +125,22 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
|
|||
|
||||
it 'gets tags from GitalyClient' do
|
||||
expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
|
||||
expect(service).to receive(:tags).with(sort_by: 'name_asc')
|
||||
expect(service).to receive(:tags).with(sort_by: 'name_asc', pagination_params: nil)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination option' do
|
||||
subject { repository.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }) }
|
||||
|
||||
it 'gets tags from GitalyClient' do
|
||||
expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
|
||||
expect(service).to receive(:tags).with(
|
||||
sort_by: nil,
|
||||
pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }
|
||||
)
|
||||
end
|
||||
|
||||
subject
|
||||
|
|
|
@ -190,6 +190,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do
|
|||
client.tags(sort_by: 'name_asc')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination option' do
|
||||
it 'sends a correct find_all_tags message' do
|
||||
expected_pagination = Gitaly::PaginationParameter.new(
|
||||
limit: 5,
|
||||
page_token: 'refs/tags/v1.0.0'
|
||||
)
|
||||
|
||||
expect_any_instance_of(Gitaly::RefService::Stub)
|
||||
.to receive(:find_all_tags)
|
||||
.with(gitaly_request_with_params(pagination_params: expected_pagination), kind_of(Hash))
|
||||
.and_return([])
|
||||
|
||||
client.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#branch_names_contains_sha' do
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
require "fast_spec_helper"
|
||||
require "support/graphql/fake_tracer"
|
||||
require "support/graphql/fake_query_type"
|
||||
|
||||
RSpec.describe Gitlab::Graphql::Tracers::ApplicationContextTracer do
|
||||
let(:tracer_spy) { spy('tracer_spy') }
|
||||
let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(['fooOperation']) }
|
||||
let(:dummy_schema) do
|
||||
schema = Class.new(GraphQL::Schema) do
|
||||
use Gitlab::Graphql::Tracers::ApplicationContextTracer
|
||||
|
||||
query Graphql::FakeQueryType
|
||||
end
|
||||
|
||||
fake_tracer = Graphql::FakeTracer.new(lambda do |key, *args|
|
||||
tracer_spy.trace(key, Gitlab::ApplicationContext.current)
|
||||
end)
|
||||
|
||||
schema.tracer(fake_tracer)
|
||||
|
||||
schema
|
||||
end
|
||||
|
||||
before do
|
||||
allow(::Gitlab::Graphql::KnownOperations).to receive(:default).and_return(default_known_operations)
|
||||
end
|
||||
|
||||
it "sets application context during execute_query and cleans up afterwards", :aggregate_failures do
|
||||
dummy_schema.execute("query fooOperation { helloWorld }")
|
||||
|
||||
# "parse" is just an arbitrary trace event that isn't setting caller_id
|
||||
expect(tracer_spy).to have_received(:trace).with("parse", hash_excluding("meta.caller_id"))
|
||||
expect(tracer_spy).to have_received(:trace).with("execute_query", hash_including("meta.caller_id" => "graphql:fooOperation")).once
|
||||
expect(Gitlab::ApplicationContext.current).not_to include("meta.caller_id")
|
||||
end
|
||||
|
||||
it "sets caller_id when operation is not known" do
|
||||
dummy_schema.execute("query fuzz { helloWorld }")
|
||||
|
||||
expect(tracer_spy).to have_received(:trace).with("execute_query", hash_including("meta.caller_id" => "graphql:unknown")).once
|
||||
end
|
||||
end
|
|
@ -123,6 +123,24 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
|
|||
|
||||
expect(milestone.persisted?).to be true
|
||||
end
|
||||
|
||||
context 'with clashing iid' do
|
||||
it 'creates milestone and claims iid for the new milestone' do
|
||||
clashing_iid = 1
|
||||
create(:milestone, iid: clashing_iid, project: project)
|
||||
|
||||
milestone = described_class.build(Milestone,
|
||||
'iid' => clashing_iid,
|
||||
'title' => 'milestone',
|
||||
'project' => project,
|
||||
'group' => nil,
|
||||
'group_id' => nil)
|
||||
|
||||
expect(milestone.persisted?).to be true
|
||||
expect(Milestone.count).to eq(2)
|
||||
expect(milestone.iid).to eq(clashing_iid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'merge_request' do
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe DataList do
|
||||
describe '#to_array' do
|
||||
let(:jira_integration) { create(:jira_integration) }
|
||||
let(:zentao_integration) { create(:zentao_integration) }
|
||||
let(:cases) do
|
||||
[
|
||||
[jira_integration, 'Integrations::JiraTrackerData', 'service_id'],
|
||||
[zentao_integration, 'Integrations::ZentaoTrackerData', 'integration_id']
|
||||
]
|
||||
end
|
||||
|
||||
def data_list(integration)
|
||||
DataList.new([integration], integration.to_data_fields_hash, integration.data_fields.class).to_array
|
||||
end
|
||||
|
||||
it 'returns current data' do
|
||||
cases.each do |integration, data_fields_class_name, foreign_key|
|
||||
data_fields_klass, columns, values_items = data_list(integration)
|
||||
|
||||
expect(data_fields_klass.to_s).to eq data_fields_class_name
|
||||
expect(columns.last).to eq foreign_key
|
||||
values = values_items.first
|
||||
expect(values.last).to eq integration.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -66,7 +66,7 @@ RSpec.describe Repository do
|
|||
it { is_expected.not_to include('v1.0.0') }
|
||||
end
|
||||
|
||||
describe 'tags_sorted_by' do
|
||||
describe '#tags_sorted_by' do
|
||||
let(:tags_to_compare) { %w[v1.0.0 v1.1.0] }
|
||||
let(:feature_flag) { true }
|
||||
|
||||
|
@ -87,7 +87,9 @@ RSpec.describe Repository do
|
|||
end
|
||||
|
||||
context 'name_asc' do
|
||||
subject { repository.tags_sorted_by('name_asc').map(&:name) & tags_to_compare }
|
||||
subject { repository.tags_sorted_by('name_asc', pagination_params).map(&:name) & tags_to_compare }
|
||||
|
||||
let(:pagination_params) { nil }
|
||||
|
||||
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
|
||||
|
||||
|
@ -96,6 +98,44 @@ RSpec.describe Repository do
|
|||
|
||||
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
context 'with limit' do
|
||||
let(:pagination_params) { { limit: 1 } }
|
||||
|
||||
it { is_expected.to eq(['v1.0.0']) }
|
||||
end
|
||||
|
||||
context 'with page token and limit' do
|
||||
let(:pagination_params) { { page_token: 'refs/tags/v1.0.0', limit: 1 } }
|
||||
|
||||
it { is_expected.to eq(['v1.1.0']) }
|
||||
end
|
||||
|
||||
context 'with page token only' do
|
||||
let(:pagination_params) { { page_token: 'refs/tags/v1.0.0' } }
|
||||
|
||||
it 'raises an ArgumentError' do
|
||||
expect { subject }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with negative limit' do
|
||||
let(:pagination_params) { { limit: -1 } }
|
||||
|
||||
it 'returns all tags' do
|
||||
is_expected.to eq(['v1.0.0', 'v1.1.0'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown token' do
|
||||
let(:pagination_params) { { page_token: 'unknown' } }
|
||||
|
||||
it 'raises an ArgumentError' do
|
||||
expect { subject }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'updated' do
|
||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe 'GraphQL' do
|
|||
let(:expected_execute_query_log) do
|
||||
{
|
||||
"correlation_id" => kind_of(String),
|
||||
"meta.caller_id" => "GraphqlController#execute",
|
||||
"meta.caller_id" => "graphql:anonymous",
|
||||
"meta.client_id" => kind_of(String),
|
||||
"meta.feature_category" => "not_owned",
|
||||
"meta.remote_ip" => kind_of(String),
|
||||
|
|
|
@ -13,6 +13,8 @@ RSpec.describe API::Todos do
|
|||
let_it_be(:john_doe) { create(:user, username: 'john_doe') }
|
||||
let_it_be(:issue) { create(:issue, project: project_1) }
|
||||
let_it_be(:merge_request) { create(:merge_request, source_project: project_1) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, project: project_1) }
|
||||
let_it_be(:alert_todo) { create(:todo, project: project_1, author: john_doe, user: john_doe, target: alert) }
|
||||
let_it_be(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) }
|
||||
let_it_be(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe, target: issue) }
|
||||
let_it_be(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe, target: issue) }
|
||||
|
@ -67,7 +69,7 @@ RSpec.describe API::Todos do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.length).to eq(4)
|
||||
expect(json_response.length).to eq(5)
|
||||
expect(json_response[0]['id']).to eq(pending_3.id)
|
||||
expect(json_response[0]['project']).to be_a Hash
|
||||
expect(json_response[0]['author']).to be_a Hash
|
||||
|
@ -95,6 +97,10 @@ RSpec.describe API::Todos do
|
|||
expect(json_response[3]['target']['merge_requests_count']).to be_nil
|
||||
expect(json_response[3]['target']['upvotes']).to eq(1)
|
||||
expect(json_response[3]['target']['downvotes']).to eq(0)
|
||||
|
||||
expect(json_response[4]['target_type']).to eq('AlertManagement::Alert')
|
||||
expect(json_response[4]['target']['iid']).to eq(alert.iid)
|
||||
expect(json_response[4]['target']['title']).to eq(alert.title)
|
||||
end
|
||||
|
||||
context "when current user does not have access to one of the TODO's target" do
|
||||
|
@ -105,7 +111,7 @@ RSpec.describe API::Todos do
|
|||
|
||||
get api('/todos', john_doe)
|
||||
|
||||
expect(json_response.count).to eq(4)
|
||||
expect(json_response.count).to eq(5)
|
||||
expect(json_response.map { |t| t['id'] }).not_to include(no_access_todo.id, pending_4.id)
|
||||
end
|
||||
end
|
||||
|
@ -163,7 +169,7 @@ RSpec.describe API::Todos do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.length).to eq(3)
|
||||
expect(json_response.length).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe BulkCreateIntegrationService do
|
|||
end
|
||||
|
||||
context 'integration with data fields' do
|
||||
let(:excluded_attributes) { %w[id service_id created_at updated_at] }
|
||||
let(:excluded_attributes) { %w[id service_id integration_id created_at updated_at] }
|
||||
|
||||
it 'updates the data fields from inherited integrations' do
|
||||
described_class.new(integration, batch, association).execute
|
||||
|
@ -82,6 +82,14 @@ RSpec.describe BulkCreateIntegrationService do
|
|||
|
||||
it_behaves_like 'creates integration from batch ids'
|
||||
it_behaves_like 'updates inherit_from_id'
|
||||
|
||||
context 'with different foreign key of data_fields' do
|
||||
let(:integration) { create(:zentao_integration, group: group, project: nil) }
|
||||
let(:created_integration) { project.zentao_integration }
|
||||
|
||||
it_behaves_like 'creates integration from batch ids'
|
||||
it_behaves_like 'updates inherit_from_id'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a group association' do
|
||||
|
@ -94,6 +102,13 @@ RSpec.describe BulkCreateIntegrationService do
|
|||
|
||||
it_behaves_like 'creates integration from batch ids'
|
||||
it_behaves_like 'updates inherit_from_id'
|
||||
|
||||
context 'with different foreign key of data_fields' do
|
||||
let(:integration) { create(:zentao_integration, group: group, project: nil, inherit_from_id: instance_integration.id) }
|
||||
|
||||
it_behaves_like 'creates integration from batch ids'
|
||||
it_behaves_like 'updates inherit_from_id'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -88,4 +88,22 @@ RSpec.describe BulkUpdateIntegrationService do
|
|||
described_class.new(group_integration, [integration]).execute
|
||||
end.to change { integration.reload.url }.to(group_integration.url)
|
||||
end
|
||||
|
||||
context 'with different foreign key of data_fields' do
|
||||
let(:integration) { create(:zentao_integration, project: create(:project, group: group)) }
|
||||
let(:group_integration) do
|
||||
Integrations::Zentao.create!(
|
||||
group: group,
|
||||
url: 'https://group.zentao.net',
|
||||
api_token: 'GROUP_TOKEN',
|
||||
zentao_product_xid: '1'
|
||||
)
|
||||
end
|
||||
|
||||
it 'works with batch as an array of ActiveRecord objects' do
|
||||
expect do
|
||||
described_class.new(group_integration, [integration]).execute
|
||||
end.to change { integration.reload.url }.to(group_integration.url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
RSpec.shared_examples 'issue description templates from current project only' do
|
||||
it 'loads issue description templates from the project only' do
|
||||
within('#service-desk-template-select') do
|
||||
expect(page).to have_content('project-issue-bar')
|
||||
expect(page).to have_content('project-issue-foo')
|
||||
expect(page).not_to have_content('group-issue-bar')
|
||||
expect(page).not_to have_content('group-issue-foo')
|
||||
expect(page).to have_content(:all, 'project-issue-bar')
|
||||
expect(page).to have_content(:all, 'project-issue-foo')
|
||||
expect(page).not_to have_content(:all, 'group-issue-bar')
|
||||
expect(page).not_to have_content(:all, 'group-issue-foo')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
root="$(cd "$(dirname "$0")/../.." || exit ; pwd -P)"
|
||||
|
||||
if [ $# -ne 0 ]; then
|
||||
shellcheck --exclude=SC1071 --external-sources "$@"
|
||||
else
|
||||
find \
|
||||
"${root}/bin" \
|
||||
"${root}/tooling" \
|
||||
-type f \
|
||||
-not -path "*.swp" \
|
||||
-not -path "*.rb" \
|
||||
-not -path "*.js" \
|
||||
-not -path "*.md" \
|
||||
-not -path "*.haml" \
|
||||
-not -path "*/Gemfile*" \
|
||||
-not -path '*/.bundle*' \
|
||||
-not -path '*/Makefile*' \
|
||||
-print0 \
|
||||
| xargs -0 shellcheck --exclude=SC1071 --external-sources --
|
||||
fi
|
Loading…
Reference in New Issue