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'),
+ },
+};
+
+
+
+
+
+
+
+
+ {{ item }}
+
+
+ templateClick(template)"
+ >
+ {{ template.name }}
+
+
+
+
+
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