Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/flash.js12
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/editor.vue94
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue68
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue27
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb9
-rw-r--r--app/events/projects/project_deleted_event.rb16
-rw-r--r--app/helpers/tree_helper.rb15
-rw-r--r--app/services/projects/destroy_service.rb8
-rw-r--r--app/views/layouts/_flash.html.haml3
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/shared/_confirm_fork_modal.html.haml12
-rw-r--r--app/views/shared/_web_ide_button.html.haml9
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/namespaces/update_root_statistics_worker.rb17
-rw-r--r--config/feature_flags/development/new_route_storage_purchase.yml2
-rw-r--r--config/feature_flags/development/publish_project_deleted_event.yml8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/development/snowplow/implementation.md2
-rw-r--r--doc/integration/elasticsearch.md92
-rw-r--r--lib/api/lint.rb5
-rw-r--r--lib/gitlab/changelog/release.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml10
-rw-r--r--lib/gitlab/event_store.rb1
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/project/web_ide/edit.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb34
-rw-r--r--spec/events/projects/project_deleted_event_spec.rb34
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb2
-rw-r--r--spec/features/groups/members/leave_group_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb2
-rw-r--r--spec/features/profiles/password_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb2
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb2
-rw-r--r--spec/features/projects/network_graph_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb8
-rw-r--r--spec/features/projects_spec.rb4
-rw-r--r--spec/features/triggers_spec.rb6
-rw-r--r--spec/features/users/logout_spec.rb2
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js32
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js10
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js20
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js9
-rw-r--r--spec/frontend/flash_spec.js8
-rw-r--r--spec/frontend/gl_form_spec.js15
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js19
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/confirm_fork_modal_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js47
-rw-r--r--spec/lib/gitlab/changelog/release_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb5
-rw-r--r--spec/requests/api/lint_spec.rb17
-rw-r--r--spec/requests/git_http_spec.rb26
-rw-r--r--spec/services/projects/destroy_service_spec.rb26
-rw-r--r--spec/support/matchers/event_store.rb12
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb2
-rw-r--r--spec/tooling/quality/test_level_spec.rb4
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
-rw-r--r--spec/workers/namespaces/update_root_statistics_worker_spec.rb23
-rw-r--r--tooling/quality/test_level.rb1
64 files changed, 851 insertions, 170 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 2bc8dd4dfaf..e27cac66be3 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-c066a74badc13503380df33e9ec322040b290088
+3074a5673df93a6a765c2bdde84bd4f6f670afcb
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index d9c2e55cffe..fa605f8c056 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -18,6 +18,13 @@ const VARIANT_DANGER = 'danger';
const VARIANT_INFO = 'info';
const VARIANT_TIP = 'tip';
+const TYPE_TO_VARIANT = {
+ [FLASH_TYPES.ALERT]: VARIANT_DANGER,
+ [FLASH_TYPES.NOTICE]: VARIANT_INFO,
+ [FLASH_TYPES.SUCCESS]: VARIANT_SUCCESS,
+ [FLASH_TYPES.WARNING]: VARIANT_WARNING,
+};
+
const FLASH_CLOSED_EVENT = 'flashClosed';
const getCloseEl = (flashEl) => {
@@ -61,7 +68,7 @@ const createAction = (config) => `
`;
const createFlashEl = (message, type) => `
- <div class="flash-${type}">
+ <div class="flash-${type}" data-testid="alert-${TYPE_TO_VARIANT[type]}">
<div class="flash-text">
${escape(message)}
<div class="close-icon-wrapper js-close-icon">
@@ -189,6 +196,9 @@ const createAlert = function createAlert({
secondaryButtonLink: secondaryButton?.link,
secondaryButtonText: secondaryButton?.text,
},
+ attrs: {
+ 'data-testid': `alert-${variant}`,
+ },
on,
},
message,
diff --git a/app/assets/javascripts/pipeline_wizard/components/editor.vue b/app/assets/javascripts/pipeline_wizard/components/editor.vue
new file mode 100644
index 00000000000..41611233f71
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/components/editor.vue
@@ -0,0 +1,94 @@
+<script>
+import { debounce } from 'lodash';
+import { isDocument } from 'yaml';
+import { CONTENT_UPDATE_DEBOUNCE } from '~/editor/constants';
+import SourceEditor from '~/editor/source_editor';
+import { YamlEditorExtension } from '~/editor/extensions/source_editor_yaml_ext';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
+
+export default {
+ name: 'YamlEditor',
+ props: {
+ doc: {
+ type: Object,
+ required: true,
+ validator: (d) => isDocument(d),
+ },
+ highlight: {
+ type: [String, Array],
+ required: false,
+ default: null,
+ },
+ filename: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ editor: null,
+ isUpdating: false,
+ yamlEditorExtension: null,
+ };
+ },
+ watch: {
+ doc: {
+ handler() {
+ this.updateEditorContent();
+ },
+ deep: true,
+ },
+ highlight(v) {
+ this.requestHighlight(v);
+ },
+ },
+ mounted() {
+ this.editor = new SourceEditor().createInstance({
+ el: this.$el,
+ blobPath: this.filename,
+ language: 'yaml',
+ });
+ [, this.yamlEditorExtension] = this.editor.use([
+ { definition: SourceEditorExtension },
+ {
+ definition: YamlEditorExtension,
+ setupOptions: {
+ highlightPath: this.highlight,
+ },
+ },
+ ]);
+ this.editor.onDidChangeModelContent(
+ debounce(() => this.handleChange(), CONTENT_UPDATE_DEBOUNCE),
+ );
+ this.updateEditorContent();
+ this.emitValue();
+ },
+ methods: {
+ async updateEditorContent() {
+ this.isUpdating = true;
+ this.editor.setDoc(this.doc);
+ this.isUpdating = false;
+ this.requestHighlight(this.highlight);
+ },
+ handleChange() {
+ this.emitValue();
+ if (!this.isUpdating) {
+ this.handleTouch();
+ }
+ },
+ emitValue() {
+ this.$emit('update:yaml', this.editor.getValue());
+ },
+ handleTouch() {
+ this.$emit('touch');
+ },
+ requestHighlight(path) {
+ this.editor.highlight(path, true);
+ },
+ },
+};
+</script>
+
+<template>
+ <div id="source-editor-yaml-editor"></div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 4b222608e5f..3aaa7d915ea 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,6 +1,7 @@
<script>
import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import LineHighlighter from '~/blob/line_highlighter';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
@@ -20,13 +21,22 @@ export default {
};
},
computed: {
+ refactorBlobViewerEnabled() {
+ return this.glFeatures.refactorBlobViewer;
+ },
+
lineNumbers() {
return this.content.split('\n').length;
},
},
mounted() {
- const { hash } = window.location;
- if (hash) this.scrollToLine(hash, true);
+ if (this.refactorBlobViewerEnabled) {
+ // This line will be removed once we start using highlight.js on the frontend (https://gitlab.com/groups/gitlab-org/-/epics/7146)
+ new LineHighlighter(); // eslint-disable-line no-new
+ } else {
+ const { hash } = window.location;
+ if (hash) this.scrollToLine(hash, true);
+ }
},
methods: {
scrollToLine(hash, scroll = false) {
diff --git a/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue
new file mode 100644
index 00000000000..64e3b5d0bae
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export const i18n = {
+ btnText: __('Fork project'),
+ title: __('Fork project?'),
+ message: __(
+ 'You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.',
+ ),
+};
+
+export default {
+ name: 'ConfirmForkModal',
+ components: {
+ GlModal,
+ },
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
+ props: {
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ modalId: {
+ type: String,
+ required: true,
+ },
+ forkPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ btnActions() {
+ return {
+ cancel: { text: __('Cancel') },
+ primary: {
+ text: this.$options.i18n.btnText,
+ attributes: {
+ href: this.forkPath,
+ variant: 'confirm',
+ 'data-qa-selector': 'fork_project_button',
+ 'data-method': 'post',
+ },
+ },
+ };
+ },
+ },
+ i18n,
+};
+</script>
+<template>
+ <gl-modal
+ :visible="visible"
+ data-qa-selector="confirm_fork_modal"
+ :modal-id="modalId"
+ :title="$options.i18n.title"
+ :action-primary="btnActions.primary"
+ :action-cancel="btnActions.cancel"
+ @change="$emit('change', $event)"
+ >
+ <p>{{ $options.i18n.message }}</p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index f02cd5c4e2e..82022d1f4d6 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,9 +1,9 @@
<script>
-import $ from 'jquery';
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
const KEY_EDIT = 'edit';
const KEY_WEB_IDE = 'webide';
@@ -16,6 +16,7 @@ export default {
GlModal,
GlSprintf,
GlLink,
+ ConfirmForkModal,
},
i18n: {
modal: {
@@ -103,11 +104,22 @@ export default {
required: false,
default: false,
},
+ forkPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ forkModalId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
selection: KEY_WEB_IDE,
showEnableGitpodModal: false,
+ showForkModal: false,
};
},
computed: {
@@ -128,7 +140,7 @@ export default {
return;
}
- this.showJQueryModal('#modal-confirm-fork-edit');
+ this.showModal('showForkModal');
},
}
: { href: this.editUrl };
@@ -171,7 +183,7 @@ export default {
return;
}
- this.showJQueryModal('#modal-confirm-fork-webide');
+ this.showModal('showForkModal');
},
}
: { href: this.webIdeUrl };
@@ -247,9 +259,6 @@ export default {
select(key) {
this.selection = key;
},
- showJQueryModal(id) {
- $(id).modal('show');
- },
showModal(dataKey) {
this[dataKey] = true;
},
@@ -282,5 +291,11 @@ export default {
</template>
</gl-sprintf>
</gl-modal>
+ <confirm-fork-modal
+ v-if="showWebIdeButton || showEditButton"
+ v-model="showForkModal"
+ :modal-id="forkModalId"
+ :fork-path="forkPath"
+ />
</div>
</template>
diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb
index c002c9b83f9..24520a187e3 100644
--- a/app/controllers/repositories/git_http_client_controller.rb
+++ b/app/controllers/repositories/git_http_client_controller.rb
@@ -17,6 +17,9 @@ module Repositories
prepend_before_action :authenticate_user, :parse_repo_path
+ skip_around_action :sessionless_bypass_admin_mode!
+ around_action :bypass_admin_mode!, if: :authenticated_user
+
feature_category :source_code_management
def authenticated_user
@@ -136,6 +139,12 @@ module Repositories
container &&
Guest.can?(repo_type.guest_read_ability, container)
end
+
+ def bypass_admin_mode!(&block)
+ return yield unless Gitlab::CurrentSettings.admin_mode
+
+ Gitlab::Auth::CurrentUserMode.bypass_session!(authenticated_user.id, &block)
+ end
end
end
diff --git a/app/events/projects/project_deleted_event.rb b/app/events/projects/project_deleted_event.rb
new file mode 100644
index 00000000000..ac58c5c6755
--- /dev/null
+++ b/app/events/projects/project_deleted_event.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Projects
+ class ProjectDeletedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' }
+ },
+ 'required' => %w[project_id namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 4437f309a9c..23a9601aed7 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -175,6 +175,21 @@ module TreeHelper
}
end
+ def fork_modal_options(project, ref, path, blob)
+ if show_edit_button?({ blob: blob })
+ fork_path = fork_and_edit_path(project, ref, path)
+ fork_modal_id = "modal-confirm-fork-edit"
+ elsif show_web_ide_button?
+ fork_path = ide_fork_and_edit_path(project, ref, path)
+ fork_modal_id = "modal-confirm-fork-webide"
+ end
+
+ {
+ fork_path: fork_path,
+ fork_modal_id: fork_modal_id
+ }
+ end
+
def web_ide_button_data(options = {})
{
project_path: project_to_use.full_path,
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 733a4b45cb2..95af5a6863f 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -37,6 +37,8 @@ module Projects
system_hook_service.execute_hooks_for(project, :destroy)
log_info("Project \"#{project.full_path}\" was deleted")
+ publish_project_deleted_event_for(project) if Feature.enabled?(:publish_project_deleted_event, default_enabled: :yaml)
+
current_user.invalidate_personal_projects_count
true
@@ -260,6 +262,12 @@ module Projects
def flush_caches(project)
Projects::ForksCountService.new(project).delete_cache
end
+
+ def publish_project_deleted_event_for(project)
+ data = { project_id: project.id, namespace_id: project.namespace_id }
+ event = Projects::ProjectDeletedEvent.new(data: data)
+ Gitlab::EventStore.publish(event)
+ end
end
end
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index dded5ba76b0..21cccb86398 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,5 +1,6 @@
-# We currently only support `alert`, `notice`, `success`, 'toast', and 'raw'
- icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'}
+- type_to_variant = {'alert' => 'danger', 'notice' => 'info', 'success' => 'success'}
.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } }
- flash.each do |key, value|
- if key == 'toast' && value
@@ -9,7 +10,7 @@
- elsif value == I18n.t('devise.failure.unconfirmed')
= render 'shared/confirm_your_email_alert'
- elsif value
- %div{ class: "flash-#{key} mb-2" }
+ %div{ class: "flash-#{key} mb-2", data: { testid: "alert-#{type_to_variant[key]}" } }
= sprite_icon(icons[key], css_class: 'align-middle mr-1') unless icons[key].nil?
%span= value
- if %w(alert notice success).include?(key)
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index 7d696a988d4..4df109dbb61 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -14,7 +14,7 @@
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3'
= render 'shared/web_hooks/test_button', hook: @hook
- = link_to _('Delete'), project_hook_path(@project, @hook), method: :delete, class: 'btn gl-button btn-danger float-right', data: { confirm: _('Are you sure?') }
+ = link_to _('Delete'), project_hook_path(@project, @hook), method: :delete, class: 'btn gl-button btn-danger float-right', aria: { label: s_('Webhooks|Delete webhook') }, data: { confirm: s_('Webhooks|Are you sure you want to delete this project hook?'), confirm_btn_variant: 'danger' }
%hr
diff --git a/app/views/shared/_confirm_fork_modal.html.haml b/app/views/shared/_confirm_fork_modal.html.haml
deleted file mode 100644
index 96b128eb2ec..00000000000
--- a/app/views/shared/_confirm_fork_modal.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-.modal{ data: { qa_selector: 'confirm_fork_modal'}, id: "modal-confirm-fork-#{type}" }
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= _('Fork project?')
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": "true" } &times;
- .modal-body.p-3
- %p= _("You can’t %{tag_start}edit%{tag_end} files directly in this project. Fork this project and submit a merge request with your changes.") % { tag_start: '', tag_end: ''}
- .modal-footer
- = link_to _('Cancel'), '#', class: "btn gl-button btn-default", "data-dismiss" => "modal"
- = link_to _('Fork project'), fork_path, class: 'btn gl-button btn-confirm', data: { qa_selector: 'fork_project_button' }, method: :post
diff --git a/app/views/shared/_web_ide_button.html.haml b/app/views/shared/_web_ide_button.html.haml
index 82af52cdd59..83646a3c92e 100644
--- a/app/views/shared/_web_ide_button.html.haml
+++ b/app/views/shared/_web_ide_button.html.haml
@@ -1,8 +1,5 @@
- type = blob ? 'blob' : 'tree'
+- button_data = web_ide_button_data({ blob: blob })
+- fork_options = fork_modal_options(@project, @ref, @path, blob)
-.d-inline-block{ data: { options: web_ide_button_data({ blob: blob }).to_json }, id: "js-#{type}-web-ide-link" }
-
-- if show_edit_button?({ blob: blob })
- = render 'shared/confirm_fork_modal', fork_path: fork_and_edit_path(@project, @ref, @path), type: 'edit'
-- if show_web_ide_button?
- = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path), type: 'webide'
+.gl-display-inline-block{ data: { options: button_data.merge(fork_options).to_json }, id: "js-#{type}-web-ide-link" }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 18646e5320b..f39aee14a69 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2569,6 +2569,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: namespaces_update_root_statistics
+ :worker_name: Namespaces::UpdateRootStatisticsWorker
+ :feature_category: :source_code_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: new_issue
:worker_name: NewIssueWorker
:feature_category: :team_planning
diff --git a/app/workers/namespaces/update_root_statistics_worker.rb b/app/workers/namespaces/update_root_statistics_worker.rb
new file mode 100644
index 00000000000..9fdf8e2506b
--- /dev/null
+++ b/app/workers/namespaces/update_root_statistics_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class UpdateRootStatisticsWorker
+ include Gitlab::EventStore::Subscriber
+
+ data_consistency :always
+
+ idempotent!
+
+ feature_category :source_code_management
+
+ def handle_event(event)
+ ScheduleAggregationWorker.perform_async(event.data[:namespace_id])
+ end
+ end
+end
diff --git a/config/feature_flags/development/new_route_storage_purchase.yml b/config/feature_flags/development/new_route_storage_purchase.yml
index b248a5f7d09..8a81af55bd5 100644
--- a/config/feature_flags/development/new_route_storage_purchase.yml
+++ b/config/feature_flags/development/new_route_storage_purchase.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327896
milestone: '14.3'
type: development
group: group::purchase
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/publish_project_deleted_event.yml b/config/feature_flags/development/publish_project_deleted_event.yml
new file mode 100644
index 00000000000..1287ebe9f66
--- /dev/null
+++ b/config/feature_flags/development/publish_project_deleted_event.yml
@@ -0,0 +1,8 @@
+---
+name: publish_project_deleted_event
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78862
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351073
+milestone: '14.8'
+type: development
+group: group::pipeline insights
+default_enabled: false
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d52659d80e7..9c5294b6b2b 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -295,6 +295,8 @@
- 1
- - namespaces_sync_namespace_name
- 1
+- - namespaces_update_root_statistics
+ - 1
- - new_epic
- 2
- - new_issue
diff --git a/doc/development/snowplow/implementation.md b/doc/development/snowplow/implementation.md
index 439485c9e73..3f038dc74ca 100644
--- a/doc/development/snowplow/implementation.md
+++ b/doc/development/snowplow/implementation.md
@@ -41,7 +41,7 @@ To implement tracking for HAML or Vue templates, add a [`data-track` attribute](
The following example shows `data-track-*` attributes assigned to a button:
```haml
-%button.btn{ data: { track: { action: "click_button", label: "template_preview", property: "my-template" } } }
+%button.btn{ data: { track_action: "click_button", track_label: "template_preview", track_property: "my-template" } }
```
```html
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index 1229134f946..527078f72db 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -164,8 +164,7 @@ may need to set the `production -> elasticsearch -> indexer_path` setting in you
## Enable Advanced Search
-For GitLab instances with more than 50GB repository data you can follow the instructions for [Indexing large
-instances](#indexing-large-instances) below.
+For GitLab instances with more than 50GB repository data you can follow the instructions for [how to index large instances efficiently](#how-to-index-large-instances-efficiently) below.
To enable Advanced Search, you must have administrator access to GitLab:
@@ -552,7 +551,7 @@ For basic guidance on choosing a cluster configuration you may refer to [Elastic
- The `Number of Elasticsearch shards` setting usually corresponds with the number of CPUs available in your cluster. For example, if you have a 3-node cluster with 4 cores each, this means you benefit from having at least 3*4=12 shards in the cluster. It's only possible to change the shards number by using [Split index API](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-split-index.html) or by reindexing to a different index with a changed number of shards.
- The `Number of Elasticsearch replicas` setting should most of the time be equal to `1` (each shard has 1 replica). Using `0` is not recommended, because losing one node corrupts the index.
-### Indexing large instances
+### How to index large instances efficiently
This section may be helpful in the event that the other
[basic instructions](#enable-advanced-search) cause problems
@@ -750,6 +749,86 @@ However, some larger installations may wish to tune the merge policy settings:
- Do not do a [force merge](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html "Force Merge") to remove deleted documents. A warning in the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html "Force Merge") states that this can lead to very large segments that may never get reclaimed, and can also cause significant performance or availability issues.
+## Index large instances with dedicated Sidekiq nodes or processes
+
+Indexing a large instance can be a lengthy and resource-intensive process that has the potential
+of overwhelming Sidekiq nodes and processes. This negatively affects the GitLab performance and
+availability.
+
+As GitLab allows you to start multiple Sidekiq processes, you can create an
+additional process dedicated to indexing a set of queues (or queue group). This way, you can
+ensure that indexing queues always have a dedicated worker, while the rest of the queues have
+another dedicated worker to avoid contention.
+
+For this purpose, use the [queue selector](../administration/operations/extra_sidekiq_processes.md#queue-selector)
+option that allows a more general selection of queue groups using a [worker matching query](../administration/operations/extra_sidekiq_routing.md#worker-matching-query).
+
+To handle these two queue groups, we generally recommend one of the following two options. You can either:
+
+- [Use two queue groups on one single node](#single-node-two-processes).
+- [Use two queue groups, one on each node](#two-nodes-one-process-for-each).
+
+For the steps below, consider:
+
+- `feature_category=global_search` as an indexing queue group with its own Sidekiq process.
+- `feature_category!=global_search` as a non-indexing queue group that has its own Sidekiq process.
+
+### Single node, two processes
+
+To create both an indexing and a non-indexing Sidekiq process in one node:
+
+1. On your Sidekiq node, change the `/etc/gitlab/gitlab.rb` file to:
+
+ ```ruby
+ sidekiq['enable'] = true
+ sidekiq['queue_selector'] = true
+ sidekiq['queue_groups'] = [
+ "feature_category=global_search",
+ "feature_category!=global_search"
+ ]
+ ```
+
+1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
+for the changes to take effect.
+
+WARNING:
+When starting multiple processes, the number of processes cannot exceed the number of CPU
+cores you want to dedicate to Sidekiq. Each Sidekiq process can use only one CPU core, subject
+to the available workload and concurrency settings. For more details, see how to
+[run multiple Sidekiq processes](../administration/operations/extra_sidekiq_processes.md).
+
+### Two nodes, one process for each
+
+To handle these queue groups on two nodes:
+
+1. To set up the indexing Sidekiq process, on your indexing Sidekiq node, change the `/etc/gitlab/gitlab.rb` file to:
+
+ ```ruby
+ sidekiq['enable'] = true
+ sidekiq['queue_selector'] = true
+ sidekiq['queue_groups'] = [
+ "feature_category=global_search"
+ ]
+ ```
+
+1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
+for the changes to take effect.
+
+1. To set up the non-indexing Sidekiq process, on your non-indexing Sidekiq node, change the `/etc/gitlab/gitlab.rb` file to:
+
+ ```ruby
+ sidekiq['enable'] = true
+ sidekiq['queue_selector'] = true
+ sidekiq['queue_groups'] = [
+ "feature_category!=global_search"
+ ]
+ ```
+
+ to set up a non-indexing Sidekiq process.
+
+1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
+for the changes to take effect.
+
## Reverting to Basic Search
Sometimes there may be issues with your Elasticsearch index data and as such
@@ -992,6 +1071,13 @@ however searches will only surface results that can be viewed by the user.
Advanced Search will honor all permission checks in the application by
filtering out projects that a user does not have access to at search time.
+### Indexing fails with `error: elastic: Error 429 (Too Many Requests)`
+
+If `ElasticCommitIndexerWorker` Sidekiq workers are failing with this error during indexing, it usually means that Elasticsearch is unable to keep up with the concurrency of indexing request. To address change the following settings:
+
+- To decrease the indexing throughput you can decrease `Bulk request concurrency` (see [Advanced Search settings](#advanced-search-configuration)). This is set to `10` by default, but you change it to as low as 1 to reduce the number of concurrent indexing operations.
+- If changing `Bulk request concurrency` didn't help, you can use the [queue selector](../administration/operations/extra_sidekiq_processes.md#queue-selector) option to [limit indexing jobs only to specific Sidekiq nodes](#index-large-instances-with-dedicated-sidekiq-nodes-or-processes), which should reduce the number of indexing requests.
+
### Access requirements for the self-managed AWS OpenSearch Service
To use the self-managed AWS OpenSearch Service with GitLab, configure your instance's domain access policies
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index bfd457a3092..886fca07b96 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -46,7 +46,10 @@ module API
get ':id/ci/lint', urgency: :low do
authorize! :download_code, user_project
- content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default)
+ if user_project.commit.present?
+ content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default)
+ end
+
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user)
.validate(content, dry_run: params[:dry_run])
diff --git a/lib/gitlab/changelog/release.rb b/lib/gitlab/changelog/release.rb
index a0d598c7464..f782197cc8e 100644
--- a/lib/gitlab/changelog/release.rb
+++ b/lib/gitlab/changelog/release.rb
@@ -67,7 +67,7 @@ module Gitlab
markdown =
begin
@config.template.evaluate(state, data).strip
- rescue TemplateParser::ParseError => e
+ rescue TemplateParser::Error => e
raise Error, e.message
end
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 46756348e9d..075e13e87f0 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -103,7 +103,7 @@ canary:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -135,7 +135,7 @@ canary:
production:
<<: *production_template
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -153,7 +153,7 @@ production_manual:
<<: *production_template
allow_failure: false
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -192,7 +192,7 @@ production_manual:
resource_group: production
allow_failure: true
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -207,7 +207,7 @@ production_manual:
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index 950c3a6545e..e9c5d970c21 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -103,7 +103,7 @@ canary:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -135,7 +135,7 @@ canary:
production:
<<: *production_template
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -153,7 +153,7 @@ production_manual:
<<: *production_template
allow_failure: false
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -192,7 +192,7 @@ production_manual:
resource_group: production
allow_failure: true
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
@@ -207,7 +207,7 @@ production_manual:
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
rules:
- - if: '$CI_DEPLOY_FREEZE != null'
+ - if: '$CI_DEPLOY_FREEZE'
when: never
- if: '($CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == "") && ($KUBECONFIG == null || $KUBECONFIG == "")'
when: never
diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb
index 7dbbcdbb1a7..e20ea1c7365 100644
--- a/lib/gitlab/event_store.rb
+++ b/lib/gitlab/event_store.rb
@@ -34,6 +34,7 @@ module Gitlab
# Add subscriptions here:
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
+ store.subscribe ::Namespaces::UpdateRootStatisticsWorker, to: ::Projects::ProjectDeletedEvent
end
private_class_method :configure!
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5325715b5a2..63fee5760d3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -40584,6 +40584,9 @@ msgstr ""
msgid "Webhooks|An issue is created, updated, closed, or reopened."
msgstr ""
+msgid "Webhooks|Are you sure you want to delete this project hook?"
+msgstr ""
+
msgid "Webhooks|Are you sure you want to delete this webhook?"
msgstr ""
@@ -41456,9 +41459,6 @@ msgstr ""
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
-msgid "You can’t %{tag_start}edit%{tag_end} files directly in this project. Fork this project and submit a merge request with your changes."
-msgstr ""
-
msgid "You can’t edit files directly in this project. Fork this project and submit a merge request with your changes."
msgstr ""
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 9c0a3ab691c..403c919c6e5 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -68,7 +68,7 @@ module QA
element :delete_button
end
- view 'app/views/shared/_confirm_fork_modal.html.haml' do
+ view 'app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue' do
element :fork_project_button
element :confirm_fork_modal
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb
index 178615f23f1..d9b119c2626 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb
@@ -31,6 +31,26 @@ module QA
it 'merges after pipeline succeeds' do
transient_test = repeat > 1
+ # Push a new pipeline config file
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files(
+ [
+ {
+ file_path: '.gitlab-ci.yml',
+ content: <<~EOF
+ test:
+ tags: ["runner-for-#{project.name}"]
+ script: sleep 20
+ only:
+ - merge_requests
+ EOF
+ }
+ ]
+ )
+ end
+
repeat.times do |i|
QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if transient_test
@@ -55,22 +75,16 @@ module QA
# start it.
merge_request.visit!
- # Push a new pipeline config file
+ # Push a new file to trigger a new pipeline
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.commit_message = 'Add new file'
commit.branch = branch_name
commit.add_files(
[
{
- file_path: '.gitlab-ci.yml',
- content: <<~EOF
- test:
- tags: ["runner-for-#{project.name}"]
- script: sleep 20
- only:
- - merge_requests
- EOF
+ file_path: "#{branch_name}-file.md",
+ content: "file content"
}
]
)
diff --git a/spec/events/projects/project_deleted_event_spec.rb b/spec/events/projects/project_deleted_event_spec.rb
new file mode 100644
index 00000000000..fd8cec7271b
--- /dev/null
+++ b/spec/events/projects/project_deleted_event_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ProjectDeletedEvent do
+ where(:data, :valid) do
+ [
+ [{ project_id: 1, namespace_id: 2 }, true],
+ [{ project_id: 1 }, false],
+ [{ namespace_id: 1 }, false],
+ [{ project_id: 'foo', namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: 'foo' }, false],
+ [{ project_id: [], namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: [] }, false],
+ [{ project_id: {}, namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: {} }, false],
+ ['foo', false],
+ [123, false],
+ [[], false]
+ ]
+ end
+
+ with_them do
+ it 'validates data' do
+ constructor = -> { described_class.new(data: data) }
+
+ if valid
+ expect { constructor.call }.not_to raise_error
+ else
+ expect { constructor.call }.to raise_error(Gitlab::EventStore::InvalidEvent)
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index 58bea5c4b5f..f2f6e26fbee 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Admin Mode Logout', :js do
it 'disable shows flash notice' do
gitlab_disable_admin_mode
- expect(page).to have_selector('.flash-notice')
+ expect(page).to have_selector('[data-testid="alert-info"]')
end
context 'on a read-only instance' do
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index e6bf1ffc2f7..9612c6625f6 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe 'Groups > Members > Leave group' do
visit group_path(group, leave: 1)
- expect(find('.flash-alert')).to have_content 'You do not have permission to leave this group'
+ expect(find('[data-testid="alert-danger"]')).to have_content 'You do not have permission to leave this group'
end
def left_group_message(group)
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index 3ddcbf1bd01..3929d3694ff 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe 'Recent searches', :js do
set_recent_searches(project_1_local_storage_key, 'fail')
visit project_issues_path(project_1)
- expect(find('.flash-alert')).to have_text('An error occurred while parsing recent searches')
+ expect(find('[data-testid="alert-danger"]')).to have_text('An error occurred while parsing recent searches')
end
context 'on tablet/mobile screen' do
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 25fe43617fd..898e2c2aa59 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Profile > Password' do
it 'shows a success message' do
fill_passwords(Gitlab::Password.test_default, Gitlab::Password.test_default)
- page.within('.flash-notice') do
+ page.within('[data-testid="alert-info"]') do
expect(page).to have_content('Password was successfully updated. Please sign in again.')
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index c8cbf396098..77194fd6ca1 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -984,7 +984,7 @@ RSpec.describe 'File blob', :js do
visit_blob('README.md')
expect(page).to have_selector('.file-content')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
it 'displays a GPG badge' do
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index c8a9f959188..c9fee9bee7a 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -21,6 +21,6 @@ RSpec.describe 'Projects > Members > Group member cannot leave group project' do
it 'renders a flash message if attempting to leave by url', :js do
visit project_path(project, leave: 1)
- expect(find('.flash-alert')).to have_content 'You do not have permission to leave this project'
+ expect(find('[data-testid="alert-danger"]')).to have_content 'You do not have permission to leave this project'
end
end
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index 4ae809399b6..1ee0ea51e53 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'Project Network Graph', :js do
find('button').click
end
- expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist."
+ expect(page).to have_selector '[data-testid="alert-danger"]', text: "Git revision ';' does not exist."
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index e941ad7643e..dde8bd48790 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -857,7 +857,7 @@ RSpec.describe 'Pipelines', :js do
it 'increments jobs_cache_index' do
click_button 'Clear runner caches'
wait_for_requests
- expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Project cache successfully reset.'
end
end
@@ -865,7 +865,7 @@ RSpec.describe 'Pipelines', :js do
it 'sets jobs_cache_index to 1' do
click_button 'Clear runner caches'
wait_for_requests
- expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Project cache successfully reset.'
end
end
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index f8bbaa9535b..cd94e6da018 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('add tests for .gitattributes custom highlighting')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
expect(page).not_to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
end
@@ -36,7 +36,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('add spaces in whitespace file')
expect(page).not_to have_selector('[data-qa-selector="label-lfs"]', text: 'LFS') # rubocop:disable QA/SelectorUsage
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
it 'renders tree table with non-ASCII filenames without errors' do
@@ -46,7 +46,7 @@ RSpec.describe 'Projects tree', :js do
expect(page).to have_selector('.tree-item')
expect(page).to have_content('Files, encoding and much more')
expect(page).to have_content('テスト.txt')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
context "with a tree that contains pathspec characters" do
@@ -139,7 +139,7 @@ RSpec.describe 'Projects tree', :js do
wait_for_requests
expect(page).to have_selector('.tree-item')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
context 'for signed commit' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 31f30075327..1049f8bc18f 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -236,7 +236,7 @@ RSpec.describe 'Project' do
it 'does not show an error' do
wait_for_requests
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
end
@@ -316,7 +316,7 @@ RSpec.describe 'Project' do
wait_for_requests
expect(page).to have_selector('.tree-item')
- expect(page).not_to have_selector('.flash-alert')
+ expect(page).not_to have_selector('[data-testid="alert-danger"]')
end
context 'for signed commit' do
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 2ddd86dd807..1f1824c897e 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Triggers', :js do
click_button 'Add trigger'
aggregate_failures 'display creation notice and trigger is created' do
- expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Trigger was created successfully.'
expect(page.find('.triggers-list')).to have_content 'trigger desc'
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
@@ -63,7 +63,7 @@ RSpec.describe 'Triggers', :js do
click_button 'Save trigger'
aggregate_failures 'display update notice and trigger is updated' do
- expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Trigger was successfully updated.'
expect(page.find('.triggers-list')).to have_content new_trigger_title
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
@@ -89,7 +89,7 @@ RSpec.describe 'Triggers', :js do
end
aggregate_failures 'trigger is removed' do
- expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+ expect(page.find('[data-testid="alert-info"]')).to have_content 'Trigger removed'
expect(page).to have_css('[data-testid="no_triggers_content"]')
end
end
diff --git a/spec/features/users/logout_spec.rb b/spec/features/users/logout_spec.rb
index ffb8785b277..3129eb5e6f3 100644
--- a/spec/features/users/logout_spec.rb
+++ b/spec/features/users/logout_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Logout/Sign out', :js do
it 'sign out does not show signed out flash notice' do
gitlab_sign_out
- expect(page).not_to have_selector('.flash-notice')
+ expect(page).not_to have_selector('[data-testid="alert-info"]')
end
context 'on a read-only instance' do
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 493a96c207b..03ae437a89e 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -583,33 +583,25 @@ describe('ErrorDetails', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track IGNORE status update', () => {
+ it('should track IGNORE status update', async () => {
Tracking.event.mockClear();
- findUpdateIgnoreStatusButton().vm.$emit('click');
- setImmediate(() => {
- const { category, action } = trackErrorStatusUpdateOptions('ignored');
- expect(Tracking.event).toHaveBeenCalledWith(category, action);
- });
+ await findUpdateIgnoreStatusButton().trigger('click');
+ const { category, action } = trackErrorStatusUpdateOptions('ignored');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track RESOLVE status update', () => {
+ it('should track RESOLVE status update', async () => {
Tracking.event.mockClear();
- findUpdateResolveStatusButton().vm.$emit('click');
- setImmediate(() => {
- const { category, action } = trackErrorStatusUpdateOptions('resolved');
- expect(Tracking.event).toHaveBeenCalledWith(category, action);
- });
+ await findUpdateResolveStatusButton().trigger('click');
+ const { category, action } = trackErrorStatusUpdateOptions('resolved');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track external Sentry link views', () => {
+ it('should track external Sentry link views', async () => {
Tracking.event.mockClear();
- findExternalUrl().trigger('click');
- setImmediate(() => {
- const { category, action, label, property } = trackClickErrorLinkToSentryOptions(
- externalUrl,
- );
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
- });
+ await findExternalUrl().trigger('click');
+ const { category, action, label, property } = trackClickErrorLinkToSentryOptions(externalUrl);
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
});
});
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index d4d145b5840..59671c175e7 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -447,7 +447,7 @@ describe('ErrorTrackingList', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track status updates', () => {
+ it('should track status updates', async () => {
Tracking.event.mockClear();
const status = 'ignored';
findErrorActions().vm.$emit('update-issue-status', {
@@ -455,10 +455,10 @@ describe('ErrorTrackingList', () => {
status,
});
- setImmediate(() => {
- const { category, action } = trackErrorStatusUpdateOptions(status);
- expect(Tracking.event).toHaveBeenCalledWith(category, action);
- });
+ await nextTick();
+
+ const { category, action } = trackErrorStatusUpdateOptions(status);
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index 728c6abb23a..d27b23c5cd1 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -71,12 +71,12 @@ describe('Feature flags', () => {
describe('when limit exceeded', () => {
const provideData = { ...mockData, featureFlagsLimitExceeded: true };
- beforeEach((done) => {
+ beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory(provideData);
- setImmediate(done);
+ return waitForPromises();
});
it('makes the new feature flag button do nothing if clicked', () => {
@@ -116,12 +116,12 @@ describe('Feature flags', () => {
userListPath: null,
};
- beforeEach((done) => {
+ beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory(provideData);
- setImmediate(done);
+ return waitForPromises();
});
it('does not render configure button', () => {
@@ -202,7 +202,7 @@ describe('Feature flags', () => {
});
describe('with paginated feature flags', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(200, getRequestData, {
'x-next-page': '2',
'x-page': '1',
@@ -214,7 +214,7 @@ describe('Feature flags', () => {
factory();
jest.spyOn(store, 'dispatch');
- setImmediate(done);
+ return waitForPromises();
});
it('should render a table with feature flags', () => {
@@ -270,11 +270,11 @@ describe('Feature flags', () => {
});
describe('unsuccessful request', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(500, {});
factory();
- setImmediate(done);
+ return waitForPromises();
});
it('should render error state', () => {
@@ -300,12 +300,12 @@ describe('Feature flags', () => {
});
describe('rotate instance id', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory();
- setImmediate(done);
+ return waitForPromises();
});
it('should fire the rotate action when a `token` event is received', () => {
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index 44f67f269a2..c4e125e96da 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
describe('Filtered Search Visual Tokens', () => {
@@ -715,18 +716,16 @@ describe('Filtered Search Visual Tokens', () => {
`);
});
- it('renders a author token value element', () => {
+ it('renders a author token value element', async () => {
const { tokenNameElement, tokenValueElement } = findElements(authorToken);
const tokenName = tokenNameElement.textContent;
const tokenValue = 'new value';
subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
- jest.runOnlyPendingTimers();
+ await waitForPromises();
- setImmediate(() => {
- expect(tokenValueElement.textContent).toBe(tokenValue);
- });
+ expect(tokenValueElement.textContent).toBe(tokenValue);
});
});
});
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index d5451ec2064..942e2c330fa 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -517,16 +517,12 @@ describe('Flash', () => {
`;
});
- it('removes global flash on click', (done) => {
+ it('removes global flash on click', () => {
addDismissFlashClickListener(el, false);
el.querySelector('.js-close-icon').click();
- setImmediate(() => {
- expect(document.querySelector('.flash')).toBeNull();
-
- done();
- });
+ expect(document.querySelector('.flash')).toBeNull();
});
});
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index 07487fbb60e..ab5627ce216 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -8,7 +8,7 @@ describe('GLForm', () => {
const testContext = {};
describe('when instantiated', () => {
- beforeEach((done) => {
+ beforeEach(() => {
window.gl = window.gl || {};
testContext.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
@@ -18,22 +18,11 @@ describe('GLForm', () => {
jest.spyOn($.prototype, 'css').mockImplementation(() => {});
testContext.glForm = new GLForm(testContext.form, false);
-
- setImmediate(() => {
- $.prototype.off.mockClear();
- $.prototype.on.mockClear();
- $.prototype.css.mockClear();
- done();
- });
});
describe('setupAutosize', () => {
- beforeEach((done) => {
+ beforeEach(() => {
testContext.glForm.setupAutosize();
-
- setImmediate(() => {
- done();
- });
});
it('should register an autosize event handler on the textarea', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index 78810da7f2c..dea920ecb5e 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -1,5 +1,6 @@
import Vue, { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import { createRouter } from '~/ide/ide_router';
@@ -55,32 +56,28 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(findPathText()).toEqual(f.name);
});
- it('opens a closed file in the editor when clicking the file path', (done) => {
+ it('opens a closed file in the editor when clicking the file path', async () => {
jest.spyOn(vm, 'openPendingTab');
jest.spyOn(router, 'push').mockImplementation(() => {});
findPathEl.click();
- setImmediate(() => {
- expect(vm.openPendingTab).toHaveBeenCalled();
- expect(router.push).toHaveBeenCalled();
+ await nextTick();
- done();
- });
+ expect(vm.openPendingTab).toHaveBeenCalled();
+ expect(router.push).toHaveBeenCalled();
});
- it('calls updateViewer with diff when clicking file', (done) => {
+ it('calls updateViewer with diff when clicking file', async () => {
jest.spyOn(vm, 'openFileInEditor');
jest.spyOn(vm, 'updateViewer');
jest.spyOn(router, 'push').mockImplementation(() => {});
findPathEl.click();
- setImmediate(() => {
- expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ await waitForPromises();
- done();
- });
+ expect(vm.updateViewer).toHaveBeenCalledWith('diff');
});
describe('computed', () => {
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
new file mode 100644
index 00000000000..446412a4f02
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -0,0 +1,69 @@
+import { mount } from '@vue/test-utils';
+import { Document } from 'yaml';
+import YamlEditor from '~/pipeline_wizard/components/editor.vue';
+
+describe('Pages Yaml Editor wrapper', () => {
+ const defaultOptions = {
+ propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
+ };
+
+ describe('mount hook', () => {
+ const wrapper = mount(YamlEditor, defaultOptions);
+
+ it('editor is mounted', () => {
+ expect(wrapper.vm.editor).not.toBeFalsy();
+ expect(wrapper.find('.gl-source-editor').exists()).toBe(true);
+ });
+ });
+
+ describe('watchers', () => {
+ describe('doc', () => {
+ const doc = new Document({ baz: ['bar'] });
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(YamlEditor, defaultOptions);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it("causes the editor's value to be set to the stringified document", async () => {
+ await wrapper.setProps({ doc });
+ expect(wrapper.vm.editor.getValue()).toEqual(doc.toString());
+ });
+
+ it('emits an update:yaml event with the yaml representation of doc', async () => {
+ await wrapper.setProps({ doc });
+ const changeEvents = wrapper.emitted('update:yaml');
+ expect(changeEvents[2]).toEqual([doc.toString()]);
+ });
+
+ it('does not cause the touch event to be emitted', () => {
+ wrapper.setProps({ doc });
+ expect(wrapper.emitted('touch')).not.toBeTruthy();
+ });
+ });
+
+ describe('highlight', () => {
+ const highlight = 'foo';
+ const wrapper = mount(YamlEditor, defaultOptions);
+
+ it('calls editor.highlight(path, keep=true)', async () => {
+ const highlightSpy = jest.spyOn(wrapper.vm.yamlEditorExtension.obj, 'highlight');
+ await wrapper.setProps({ highlight });
+ expect(highlightSpy).toHaveBeenCalledWith(expect.anything(), highlight, true);
+ });
+ });
+ });
+
+ describe('events', () => {
+ const wrapper = mount(YamlEditor, defaultOptions);
+
+ it('emits touch if content is changed in editor', async () => {
+ await wrapper.vm.editor.setValue('foo: boo');
+ expect(wrapper.emitted('touch')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 1ff98d4aec5..663ebd3e12f 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -2,16 +2,20 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
+import LineHighlighter from '~/blob/line_highlighter';
+
+jest.mock('~/blob/line_highlighter');
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
- function createComponent(content = contentMock, isRawContent = false) {
+ function createComponent(content = contentMock, isRawContent = false, glFeatures = {}) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
+ glFeatures,
},
propsData: {
content,
@@ -26,6 +30,20 @@ describe('Blob Simple Viewer component', () => {
wrapper.destroy();
});
+ describe('refactorBlobViewer feature flag', () => {
+ it('loads the LineHighlighter if refactorBlobViewer is enabled', () => {
+ createComponent('', false, { refactorBlobViewer: true });
+
+ expect(LineHighlighter).toHaveBeenCalled();
+ });
+
+ it('does not load the LineHighlighter if refactorBlobViewer is disabled', () => {
+ createComponent('', false, { refactorBlobViewer: false });
+
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+ });
+
it('does not fail if content is empty', () => {
const spy = jest.spyOn(window.console, 'error');
createComponent('');
diff --git a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
new file mode 100644
index 00000000000..1cde92cf522
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
@@ -0,0 +1,80 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ConfirmForkModal, { i18n } from '~/vue_shared/components/confirm_fork_modal.vue';
+
+describe('vue_shared/components/confirm_fork_modal', () => {
+ let wrapper = null;
+
+ const forkPath = '/fake/fork/path';
+ const modalId = 'confirm-fork-modal';
+ const defaultProps = { modalId, forkPath };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findModalProp = (prop) => findModal().props(prop);
+ const findModalActionProps = () => findModalProp('actionPrimary');
+
+ const createComponent = (props = {}) =>
+ shallowMountExtended(ConfirmForkModal, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('visible = false', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('sets the visible prop to `false`', () => {
+ expect(findModalProp('visible')).toBe(false);
+ });
+
+ it('sets the modal title', () => {
+ const title = findModalProp('title');
+ expect(title).toBe(i18n.title);
+ });
+
+ it('sets the modal id', () => {
+ const fakeModalId = findModalProp('modalId');
+ expect(fakeModalId).toBe(modalId);
+ });
+
+ it('has the fork path button', () => {
+ const modalProps = findModalActionProps();
+ expect(modalProps.text).toBe(i18n.btnText);
+ expect(modalProps.attributes.variant).toBe('confirm');
+ });
+
+ it('sets the correct fork path', () => {
+ const modalProps = findModalActionProps();
+ expect(modalProps.attributes.href).toBe(forkPath);
+ });
+
+ it('has the fork message', () => {
+ expect(findModal().text()).toContain(i18n.message);
+ });
+ });
+
+ describe('visible = true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ visible: true });
+ });
+
+ it('sets the visible prop to `true`', () => {
+ expect(findModalProp('visible')).toBe(true);
+ });
+
+ it('emits the `change` event if the modal is hidden', () => {
+ expect(wrapper.emitted('change')).toBeUndefined();
+
+ findModal().vm.$emit('change', false);
+
+ expect(wrapper.emitted('change')).toEqual([[false]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 9b7c594b910..5589cbfd08f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
@@ -13,6 +14,7 @@ const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled';
const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true';
+const forkPath = '/some/fork/path';
const ACTION_EDIT = {
href: TEST_EDIT_URL,
@@ -74,6 +76,7 @@ describe('Web IDE link component', () => {
editUrl: TEST_EDIT_URL,
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
+ forkPath,
...props,
},
stubs: {
@@ -96,6 +99,7 @@ describe('Web IDE link component', () => {
const findActionsButton = () => wrapper.find(ActionsButton);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
+ const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
it.each([
{
@@ -231,16 +235,28 @@ describe('Web IDE link component', () => {
});
describe('edit actions', () => {
- it.each([
+ const testActions = [
{
- props: { showWebIdeButton: true, showEditButton: false },
+ props: {
+ showWebIdeButton: true,
+ showEditButton: false,
+ forkPath,
+ forkModalId: 'edit-modal',
+ },
expectedEventPayload: 'ide',
},
{
- props: { showWebIdeButton: false, showEditButton: true },
+ props: {
+ showWebIdeButton: false,
+ showEditButton: true,
+ forkPath,
+ forkModalId: 'webide-modal',
+ },
expectedEventPayload: 'simple',
},
- ])(
+ ];
+
+ it.each(testActions)(
'emits the correct event when an action handler is called',
async ({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true, disableForkModal: true });
@@ -250,6 +266,29 @@ describe('Web IDE link component', () => {
expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
},
);
+
+ it.each(testActions)('renders the fork confirmation modal', async ({ props }) => {
+ createComponent({ ...props, needsToFork: true });
+
+ expect(findForkConfirmModal().exists()).toBe(true);
+ expect(findForkConfirmModal().props()).toEqual({
+ visible: false,
+ forkPath,
+ modalId: props.forkModalId,
+ });
+ });
+
+ it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => {
+ createComponent({ ...props, needsToFork: true }, mountExtended);
+
+ await findActionsButton().trigger('click');
+
+ expect(findForkConfirmModal().props()).toEqual({
+ visible: true,
+ forkPath,
+ modalId: props.forkModalId,
+ });
+ });
});
describe('when Gitpod is not enabled', () => {
diff --git a/spec/lib/gitlab/changelog/release_spec.rb b/spec/lib/gitlab/changelog/release_spec.rb
index d8434821640..defcec5aa65 100644
--- a/spec/lib/gitlab/changelog/release_spec.rb
+++ b/spec/lib/gitlab/changelog/release_spec.rb
@@ -139,6 +139,16 @@ RSpec.describe Gitlab::Changelog::Release do
OUT
end
end
+
+ context 'when template parser raises an error' do
+ before do
+ allow(config).to receive(:template).and_raise(Gitlab::TemplateParser::Error)
+ end
+
+ it 'raises a Changelog error' do
+ expect { release.to_markdown }.to raise_error(Gitlab::Changelog::Error)
+ end
+ end
end
describe '#header_start_position' do
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index bcc99b78805..b657f73fa77 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -66,6 +66,11 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
expect(build_names).not_to include('review')
end
+ it 'when CI_DEPLOY_FREEZE is present' do
+ create(:ci_variable, project: project, key: 'CI_DEPLOY_FREEZE', value: 'true')
+ expect(build_names).to eq %w(placeholder)
+ end
+
it 'when CANARY_ENABLED' do
create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: 'true')
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index abb8948f13a..73bc4a5d1f3 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -110,7 +110,7 @@ RSpec.describe API::Lint do
context 'when authenticated' do
let_it_be(:api_user) { create(:user) }
- context 'with valid .gitlab-ci.yaml content' do
+ context 'with valid .gitlab-ci.yml content' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
@@ -140,7 +140,7 @@ RSpec.describe API::Lint do
end
end
- context 'with valid .gitlab-ci.yaml with warnings' do
+ context 'with valid .gitlab-ci.yml with warnings' do
let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
it 'passes validation but returns warnings' do
@@ -153,7 +153,7 @@ RSpec.describe API::Lint do
end
end
- context 'with valid .gitlab-ci.yaml using deprecated keywords' do
+ context 'with valid .gitlab-ci.yml using deprecated keywords' do
let(:yaml_content) { { job: { script: 'ls', type: 'test' }, types: ['test'] }.to_yaml }
it 'passes validation but returns warnings' do
@@ -166,7 +166,7 @@ RSpec.describe API::Lint do
end
end
- context 'with an invalid .gitlab_ci.yml' do
+ context 'with an invalid .gitlab-ci.yml' do
context 'with invalid syntax' do
let(:yaml_content) { 'invalid content' }
@@ -384,6 +384,15 @@ RSpec.describe API::Lint do
project.add_developer(api_user)
end
+ context 'with no commit' do
+ it 'returns error about providing content' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['errors']).to match_array(['Please provide content of .gitlab-ci.yml'])
+ end
+ end
+
context 'with valid .gitlab-ci.yml content' do
let(:yaml_content) do
{ include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 623cf24b9cb..340ed7bde53 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -836,6 +836,24 @@ RSpec.describe 'Git HTTP requests' do
end
end
end
+
+ context "when the user is admin" do
+ let(:admin) { create(:admin) }
+ let(:env) { { user: admin.username, password: admin.password } }
+
+ # Currently, the admin mode is bypassed for git operations.
+ # Once the admin mode is considered for git operations, this test will fail.
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/296509
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+ end
end
end
@@ -929,10 +947,10 @@ RSpec.describe 'Git HTTP requests' do
context 'when admin mode is disabled' do
it_behaves_like 'can download code only'
- it 'downloads from other project get status 404' do
+ it 'downloads from other project get status 403' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -1534,10 +1552,10 @@ RSpec.describe 'Git HTTP requests' do
context 'when admin mode is disabled' do
it_behaves_like 'can download code only'
- it 'downloads from other project get status 404' do
+ it 'downloads from other project get status 403' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index a6aa76c7e6c..0beb5157ed6 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -18,17 +18,35 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
shared_examples 'deleting the project' do
- before do
- # Run sidekiq immediately to check that renamed repository will be removed
+ it 'deletes the project', :sidekiq_inline do
destroy_project(project, user, {})
- end
- it 'deletes the project', :sidekiq_inline do
expect(Project.unscoped.all).not_to include(project)
expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey
end
+
+ it 'publishes a ProjectDeleted event with project id and namespace id' do
+ expected_data = { project_id: project.id, namespace_id: project.namespace_id }
+ expect(Gitlab::EventStore)
+ .to receive(:publish)
+ .with(event_type(Projects::ProjectDeletedEvent).containing(expected_data))
+
+ destroy_project(project, user, {})
+ end
+
+ context 'when feature flag publish_project_deleted_event is disabled' do
+ before do
+ stub_feature_flags(publish_project_deleted_event: false)
+ end
+
+ it 'does not publish an event' do
+ expect(Gitlab::EventStore).not_to receive(:publish)
+
+ destroy_project(project, user, {})
+ end
+ end
end
shared_examples 'deleting the project with pipeline and build' do
diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb
new file mode 100644
index 00000000000..96a71ae3c22
--- /dev/null
+++ b/spec/support/matchers/event_store.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :event_type do |event_class|
+ match do |actual|
+ actual.instance_of?(event_class) &&
+ actual.data == @expected_data
+ end
+
+ chain :containing do |expected_data|
+ @expected_data = expected_data
+ end
+end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 52451839281..c63faace6b2 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -166,7 +166,7 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
expect(find('.flash-container')).to be_present
- expect(find('.flash-text').text).to have_content('Variables key (key) has already been taken')
+ expect(find('[data-testid="alert-danger"]').text).to have_content('Variables key (key) has already been taken')
end
it 'prevents a variable to be added if no values are provided when a variable is set to masked' do
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 8a944a473d7..33d3a5b49b3 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,events,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@@ -110,7 +110,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)})
+ .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|events|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)})
end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index a21cf3ed5f6..1cd5d23d8fc 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -349,6 +349,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Namespaces::OnboardingPipelineCreatedWorker' => 3,
'Namespaces::OnboardingProgressWorker' => 3,
'Namespaces::OnboardingUserAddedWorker' => 3,
+ 'Namespaces::RefreshRootStatisticsWorker' => 3,
'Namespaces::RootStatisticsWorker' => 3,
'Namespaces::ScheduleAggregationWorker' => 3,
'NetworkPolicyMetricsWorker' => 3,
diff --git a/spec/workers/namespaces/update_root_statistics_worker_spec.rb b/spec/workers/namespaces/update_root_statistics_worker_spec.rb
new file mode 100644
index 00000000000..a525904b757
--- /dev/null
+++ b/spec/workers/namespaces/update_root_statistics_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::UpdateRootStatisticsWorker do
+ let(:namespace_id) { 123 }
+
+ let(:event) do
+ Projects::ProjectDeletedEvent.new(data: { project_id: 1, namespace_id: namespace_id })
+ end
+
+ subject { consume_event(event) }
+
+ def consume_event(event)
+ described_class.new.perform(event.class.name, event.data)
+ end
+
+ it 'enqueues ScheduleAggregationWorker' do
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(namespace_id)
+
+ subject
+ end
+end
diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb
index 50cbc69beb2..624564ecd05 100644
--- a/tooling/quality/test_level.rb
+++ b/tooling/quality/test_level.rb
@@ -24,6 +24,7 @@ module Quality
elastic
elastic_integration
experiments
+ events
factories
finders
frontend