Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-09 12:12:15 +00:00
parent 1aa9cd3080
commit b808458daa
69 changed files with 979 additions and 336 deletions

View File

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

View File

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

View File

@ -1 +1 @@
460a880c6993ab5f76cac951fccc02efd5cbd444
06ec7a17f320497d13efdc06f7798b919f45fa9d

View File

@ -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;
}

View File

@ -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" />

View File

@ -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';

View File

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

View File

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

View File

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

View File

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

View File

@ -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),

View File

@ -122,3 +122,5 @@ class HelpController < ApplicationController
end
end
end
::HelpController.prepend_mod

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
20f10ae28d439de1d07357ab7e977dae88feaaedb16770820350a9bf8242817f

View File

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

View File

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

View File

@ -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 projects 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**.

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module BulkImports
module Groups
module Common
module Pipelines
class MilestonesPipeline
include NdjsonPipeline

View File

@ -28,7 +28,7 @@ module BulkImports
stage: 1
},
milestones: {
pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline,
pipeline: BulkImports::Common::Pipelines::MilestonesPipeline,
stage: 1
},
badges: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => {

View File

@ -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',

View File

@ -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 },
],
];

View File

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

View File

@ -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',
};

View File

@ -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);
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),

View File

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

View File

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

View File

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

View File

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

22
tooling/bin/shellcheck Executable file
View File

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