diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 91320b8988f..17e17712ff8 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -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 # ####################### diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 8824d3d753f..82c11cb8009 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -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 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index c104cc574c6..c44cd17461b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -460a880c6993ab5f76cac951fccc02efd5cbd444 +06ec7a17f320497d13efdc06f7798b919f45fa9d diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue index 8bffd893473..611b78b3c5e 100644 --- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue +++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue @@ -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; } diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue index 07af2b848c2..b86c4df253e 100644 --- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue +++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue @@ -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)" > - + {{ $options.errorTexts.loadMergedYaml }} diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index e15fce794af..a2eaeeef286 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -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'; diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index f820428b25f..0a573cb3d58 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -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; diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue index 4c083ed5496..14c8c53dd19 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -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" diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index fe2d376f1da..d964a701b08 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -1,15 +1,8 @@ @@ -193,12 +195,13 @@ export default { - + @@ -210,6 +213,7 @@ export default { +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'), + }, +}; + + diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js index f842ffaaa2b..e14cdee17ce 100644 --- a/app/assets/javascripts/projects/settings_service_desk/index.js +++ b/app/assets/javascripts/projects/settings_service_desk/index.js @@ -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), diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 0ad7478584f..e0020c22145 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -122,3 +122,5 @@ class HelpController < ApplicationController end end end + +::HelpController.prepend_mod diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 98628590ffc..271d7634cb5 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -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 diff --git a/app/helpers/issuables_description_templates_helper.rb b/app/helpers/issuables_description_templates_helper.rb index a5b9a6eee80..6b546d5c6fc 100644 --- a/app/helpers/issuables_description_templates_helper.rb +++ b/app/helpers/issuables_description_templates_helper.rb @@ -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) diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb index 429ad868018..dee202d3785 100644 --- a/app/helpers/routing/pseudonymization_helper.rb +++ b/app/helpers/routing/pseudonymization_helper.rb @@ -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 diff --git a/app/models/data_list.rb b/app/models/data_list.rb index adad8e3013e..e99364b2709 100644 --- a/app/models/data_list.rb +++ b/app/models/data_list.rb @@ -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 diff --git a/app/models/integration.rb b/app/models/integration.rb index 4dd3e1a1785..3f66dd7ae81 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -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 diff --git a/app/models/repository.rb b/app/models/repository.rb index 119d874a6e1..7b24ae35125 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -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 diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb index 45465ba3946..29c4d0cc220 100644 --- a/app/services/bulk_update_integration_service.rb +++ b/app/services/bulk_update_integration_service.rb @@ -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 diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml index 7b345941cf7..63cf4dfe0ab 100644 --- a/app/views/projects/_service_desk_settings.html.haml +++ b/app/views/projects/_service_desk_settings.html.haml @@ -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' diff --git a/bin/background_jobs b/bin/background_jobs index d8929881f12..f301bb46ca9 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -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() diff --git a/bin/mail_room b/bin/mail_room index cf9d422909e..3717e49e37f 100755 --- a/bin/mail_room +++ b/bin/mail_room @@ -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() diff --git a/bin/parallel-rsync-repos b/bin/parallel-rsync-repos index 21921148fa0..bd849371766 100755 --- a/bin/parallel-rsync-repos +++ b/bin/parallel-rsync-repos @@ -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 diff --git a/bin/web b/bin/web index c1ab4718f0d..4d2a16f6665 100755 --- a/bin/web +++ b/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 diff --git a/bin/with_env b/bin/with_env index e678fa2f0cc..b0647a50e27 100755 --- a/bin/with_env +++ b/bin/with_env @@ -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 diff --git a/config/feature_flags/development/jira_issue_details_edit_status.yml b/config/feature_flags/development/jira_issue_details_edit_status.yml deleted file mode 100644 index 311e243c570..00000000000 --- a/config/feature_flags/development/jira_issue_details_edit_status.yml +++ /dev/null @@ -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 diff --git a/config/routes/project.rb b/config/routes/project.rb index 446ecc4159b..ec399573f57 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -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] diff --git a/db/post_migrate/20211105135157_drop_ci_build_trace_sections.rb b/db/post_migrate/20211105135157_drop_ci_build_trace_sections.rb new file mode 100644 index 00000000000..1595068952d --- /dev/null +++ b/db/post_migrate/20211105135157_drop_ci_build_trace_sections.rb @@ -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 diff --git a/db/schema_migrations/20211105135157 b/db/schema_migrations/20211105135157 new file mode 100644 index 00000000000..694509bfafd --- /dev/null +++ b/db/schema_migrations/20211105135157 @@ -0,0 +1 @@ +20f10ae28d439de1d07357ab7e977dae88feaaedb16770820350a9bf8242817f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 790b9066eac..4279fbfd448 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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; diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index 7c3c5989171..4f6140197ec 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -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 x 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. diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index 61ef68e69af..5fe3e5be4c0 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -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**. diff --git a/lib/api/entities/alert_management/alert.rb b/lib/api/entities/alert_management/alert.rb new file mode 100644 index 00000000000..664cd53293e --- /dev/null +++ b/lib/api/entities/alert_management/alert.rb @@ -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 diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb index 8d222db488a..5bbbb59f565 100644 --- a/lib/api/entities/todo.rb +++ b/lib/api/entities/todo.rb @@ -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 diff --git a/lib/bulk_imports/groups/pipelines/milestones_pipeline.rb b/lib/bulk_imports/common/pipelines/milestones_pipeline.rb similarity index 94% rename from lib/bulk_imports/groups/pipelines/milestones_pipeline.rb rename to lib/bulk_imports/common/pipelines/milestones_pipeline.rb index b2bd14952e7..aea2a04c1c7 100644 --- a/lib/bulk_imports/groups/pipelines/milestones_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/milestones_pipeline.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module BulkImports - module Groups + module Common module Pipelines class MilestonesPipeline include NdjsonPipeline diff --git a/lib/bulk_imports/groups/stage.rb b/lib/bulk_imports/groups/stage.rb index a1869b4cb0e..241dd428dd5 100644 --- a/lib/bulk_imports/groups/stage.rb +++ b/lib/bulk_imports/groups/stage.rb @@ -28,7 +28,7 @@ module BulkImports stage: 1 }, milestones: { - pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline, + pipeline: BulkImports::Common::Pipelines::MilestonesPipeline, stage: 1 }, badges: { diff --git a/lib/bulk_imports/projects/stage.rb b/lib/bulk_imports/projects/stage.rb index 5d563b9b728..d41beceffd7 100644 --- a/lib/bulk_imports/projects/stage.rb +++ b/lib/bulk_imports/projects/stage.rb @@ -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 diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index 544b6ce4ed0..dca5326f270 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -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 diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index c5f3f224a3d..da1a3efd562 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -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 diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index c28abda3843..c064811b1e7 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -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) diff --git a/lib/gitlab/graphql/tracers/application_context_tracer.rb b/lib/gitlab/graphql/tracers/application_context_tracer.rb new file mode 100644 index 00000000000..4193c46e321 --- /dev/null +++ b/lib/gitlab/graphql/tracers/application_context_tracer.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index db2d6cdf3dc..01ebba2c07e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index bb35c5eb17c..47f7e701ae8 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -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 diff --git a/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz b/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz deleted file mode 100644 index f959cd7a0bd..00000000000 Binary files a/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz and /dev/null differ diff --git a/spec/fixtures/bulk_imports/milestones.ndjson b/spec/fixtures/bulk_imports/milestones.ndjson deleted file mode 100644 index 40523f276e7..00000000000 --- a/spec/fixtures/bulk_imports/milestones.ndjson +++ /dev/null @@ -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} diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 87301f6b7cb..c22aace856c 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -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', () => { diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index b67fe632a35..ddc802457e9 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -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', diff --git a/spec/frontend/projects/settings_service_desk/components/mock_data.js b/spec/frontend/projects/settings_service_desk/components/mock_data.js new file mode 100644 index 00000000000..934778ff601 --- /dev/null +++ b/spec/frontend/projects/settings_service_desk/components/mock_data.js @@ -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 }, + ], +]; diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js index 8acf2376860..62224612387 100644 --- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js @@ -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, diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js index eacf858f22c..a5807ac588c 100644 --- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js @@ -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', }; diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js new file mode 100644 index 00000000000..cdb355f5a9b --- /dev/null +++ b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js @@ -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); + }); + }); +}); diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb index 55649e9087a..6b05bab7432 100644 --- a/spec/helpers/issuables_description_templates_helper_spec.rb +++ b/spec/helpers/issuables_description_templates_helper_spec.rb @@ -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 diff --git a/spec/helpers/routing/pseudonymization_helper_spec.rb b/spec/helpers/routing/pseudonymization_helper_spec.rb index eb7b03959b2..e41e62a4fe2 100644 --- a/spec/helpers/routing/pseudonymization_helper_spec.rb +++ b/spec/helpers/routing/pseudonymization_helper_spec.rb @@ -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 diff --git a/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb new file mode 100644 index 00000000000..9f71175f46f --- /dev/null +++ b/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb @@ -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 diff --git a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb deleted file mode 100644 index a8354e62459..00000000000 --- a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb +++ /dev/null @@ -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 diff --git a/spec/lib/bulk_imports/groups/stage_spec.rb b/spec/lib/bulk_imports/groups/stage_spec.rb index b322b7b0edf..5719acac4d7 100644 --- a/spec/lib/bulk_imports/groups/stage_spec.rb +++ b/spec/lib/bulk_imports/groups/stage_spec.rb @@ -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] ] diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb index eec2fe55177..685bf223f9c 100644 --- a/spec/lib/bulk_imports/projects/stage_spec.rb +++ b/spec/lib/bulk_imports/projects/stage_spec.rb @@ -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], diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 1004947e368..f1b6a59abf9 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -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 diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index d013bb2bd55..2e37c98a591 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -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 diff --git a/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb new file mode 100644 index 00000000000..6eff816b95a --- /dev/null +++ b/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb @@ -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 diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb index 42598047500..189b798c2e8 100644 --- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb @@ -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 diff --git a/spec/models/data_list_spec.rb b/spec/models/data_list_spec.rb new file mode 100644 index 00000000000..d2f15386808 --- /dev/null +++ b/spec/models/data_list_spec.rb @@ -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 diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7bad907cf90..94acb9a6430 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -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 diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index d03441b0d4c..b8f7af29a9f 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -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), diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index d31f571e636..c9deb84ff98 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -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 diff --git a/spec/services/bulk_create_integration_service_spec.rb b/spec/services/bulk_create_integration_service_spec.rb index 517222c0e69..a536fd415f2 100644 --- a/spec/services/bulk_create_integration_service_spec.rb +++ b/spec/services/bulk_create_integration_service_spec.rb @@ -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 diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb index c10a9b75648..e3a7e4201f7 100644 --- a/spec/services/bulk_update_integration_service_spec.rb +++ b/spec/services/bulk_update_integration_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/service_desk_issue_templates_examples.rb b/spec/support/shared_examples/service_desk_issue_templates_examples.rb index fd9645df7a3..ed6c5199936 100644 --- a/spec/support/shared_examples/service_desk_issue_templates_examples.rb +++ b/spec/support/shared_examples/service_desk_issue_templates_examples.rb @@ -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 diff --git a/tooling/bin/shellcheck b/tooling/bin/shellcheck new file mode 100755 index 00000000000..b499bfa3e5e --- /dev/null +++ b/tooling/bin/shellcheck @@ -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