diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 980da48a317..83440c58bb3 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -162,9 +162,7 @@ retrieve-frontend-fixtures:
echoinfo "INFO: Reusing frontend fixtures from 'retrieve-frontend-fixtures'."
exit 0
fi
- - run_timed_command "gem install knapsack --no-document"
- - section_start "gitaly-test-spawn" "Spawning Gitaly"; scripts/gitaly-test-spawn; section_end "gitaly-test-spawn"; # Do not use 'bundle exec' here
- - source ./scripts/rspec_helpers.sh
+ - !reference [.base-script, script]
- rspec_parallelized_job
artifacts:
name: frontend-fixtures
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 7c8ed3b0fc4..882c7736756 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -240,12 +240,14 @@
- ".gitlab/ci/review-apps/qa.gitlab-ci.yml"
- ".gitlab/ci/review-apps/rules.gitlab-ci.yml"
- ".gitlab/ci/test-on-gdk/*.yml"
+ - ".gitlab/ci/version.yml"
.gitaly-patterns: &gitaly-patterns
- "GITALY_SERVER_VERSION"
- "lib/gitlab/setup_helper.rb"
.workhorse-patterns: &workhorse-patterns
+ - ".gitlab/ci/version.yml"
- ".gitlab/ci/workhorse.gitlab-ci.yml"
- "GITLAB_WORKHORSE_VERSION"
- "workhorse/**/*"
@@ -713,7 +715,8 @@
- "ee/{lib/,spec/}tasks/gitlab/custom_roles/*"
.cng-orchestrator-patterns: &cng-orchestrator-patterns
- - qa/gems/gitlab-cng/**/*.rb
+ - "qa/gems/gitlab-cng/**/*.rb"
+ - "qa/gems/gitlab-cng/{Gemfile,Gemfile.lock}"
##################
# Conditions set #
diff --git a/Gemfile b/Gemfile
index bfc8e6e4406..dd2a598437e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -228,7 +228,7 @@ gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentati
gem 'elasticsearch-api', '7.17.11', feature_category: :global_search
gem 'aws-sdk-core', '~> 3.201.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'aws-sdk-s3', '~> 1.156.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'aws-sdk-s3', '~> 1.157.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'faraday-typhoeus', '~> 1.1', feature_category: :global_search
gem 'faraday_middleware-aws-sigv4', '~> 1.0.1', feature_category: :global_search
# Used with Elasticsearch to support http keep-alive connections
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 398738fb47e..57b66606be2 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -39,7 +39,7 @@
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
{"name":"aws-sdk-core","version":"3.201.3","platform":"ruby","checksum":"c045a7ff37b4a6f1de5742e64def0841bdf70d215cb17d3875b2c5bdd9e99d52"},
{"name":"aws-sdk-kms","version":"1.76.0","platform":"ruby","checksum":"e7f75013cba9ba357144f66bbc600631c192e2cda9dd572794be239654e2cf49"},
-{"name":"aws-sdk-s3","version":"1.156.0","platform":"ruby","checksum":"9302da1d1a70363308854d5065035f6c72cf8b8af895d8789487cd5c6b076a46"},
+{"name":"aws-sdk-s3","version":"1.157.0","platform":"ruby","checksum":"e1e0c7a268e710a7ccf4a0f9d2c33e3ca685b06968c3048d907e3a792580e990"},
{"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"},
{"name":"axe-core-api","version":"4.9.1","platform":"ruby","checksum":"9ea7ac16bfee1cb3545345d210878aa8cccfb41b493e00fe1faab79af4d9fed8"},
{"name":"axe-core-rspec","version":"4.9.1","platform":"ruby","checksum":"31ef067bee36d6efb3f156a83aa2fb6ac721270a53fb9473f0268e325a3e6efd"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 1dd453da2b6..2c454342847 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -330,7 +330,7 @@ GEM
aws-sdk-kms (1.76.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.156.0)
+ aws-sdk-s3 (1.157.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -1968,7 +1968,7 @@ DEPENDENCIES
awesome_print
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.201.0)
- aws-sdk-s3 (~> 1.156.0)
+ aws-sdk-s3 (~> 1.157.0)
axe-core-rspec (~> 4.9.0)
babosa (~> 2.0)
base32 (~> 0.3.0)
diff --git a/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue b/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue
index 9899e941fbf..24dfba58578 100644
--- a/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue
+++ b/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue
@@ -1,41 +1,38 @@
+
+
+
+
diff --git a/app/assets/javascripts/search/results/components/blob_chunks.vue b/app/assets/javascripts/search/results/components/blob_chunks.vue
new file mode 100644
index 00000000000..66939e14209
--- /dev/null
+++ b/app/assets/javascripts/search/results/components/blob_chunks.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ line.lineNumber }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/search/results/components/blob_footer.vue b/app/assets/javascripts/search/results/components/blob_footer.vue
new file mode 100644
index 00000000000..8ac8b6856a1
--- /dev/null
+++ b/app/assets/javascripts/search/results/components/blob_footer.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ {{ howMuchMore }}
+
+
+
+
+
+
+ {{ fileMatchCountTotal }}
+ {{ showingMatches }}
+ {{
+ content
+ }}
+ {{
+ content
+ }}
+
+
+
+
+
+ {{ $options.i18n.showLess }}
+
+
+
diff --git a/app/assets/javascripts/search/results/components/blob_header.vue b/app/assets/javascripts/search/results/components/blob_header.vue
new file mode 100644
index 00000000000..6721202a259
--- /dev/null
+++ b/app/assets/javascripts/search/results/components/blob_header.vue
@@ -0,0 +1,59 @@
+
+
+
+
diff --git a/app/assets/javascripts/search/results/components/result_empty.vue b/app/assets/javascripts/search/results/components/result_empty.vue
new file mode 100644
index 00000000000..7d02e15f087
--- /dev/null
+++ b/app/assets/javascripts/search/results/components/result_empty.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+ {{ $options.SCOPE_NAVIGATION_MAP[currentScope] }}
+
+
+ {{ query.search }}
+
+
+ {{ projectInitialJson.name }}
+
+
+
+
+ {{ $options.SCOPE_NAVIGATION_MAP[currentScope] }}
+
+
+ {{ query.search }}
+
+
+ {{ groupInitialJson.name }}
+
+
+
+
+ {{ $options.SCOPE_NAVIGATION_MAP[currentScope] }}
+
+
+ {{ query.search }}
+
+
+
+
+
diff --git a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue
index aa404e20663..a87f01d51c2 100644
--- a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue
+++ b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue
@@ -1,5 +1,5 @@
@@ -76,5 +106,41 @@ export default {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/search/results/constants.js b/app/assets/javascripts/search/results/constants.js
index ce8287a5f71..35e1f8bb826 100644
--- a/app/assets/javascripts/search/results/constants.js
+++ b/app/assets/javascripts/search/results/constants.js
@@ -2,3 +2,4 @@ export const DEFAULT_FETCH_CHUNKS = 50;
export const PROJECT_GRAPHQL_ID_TYPE = 'Project';
export const GROUP_GRAPHQL_ID_TYPE = 'Group';
export const SEARCH_RESULTS_DEBOUNCE = 500;
+export const DEFAULT_SHOW_CHUNKS = 3;
diff --git a/app/assets/javascripts/search/results/event_hub.js b/app/assets/javascripts/search/results/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/search/results/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index 6518c85643c..dd7e541e51d 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -4,6 +4,7 @@ import { languageFilterData } from '~/search/sidebar/components/language_filter/
import { LABEL_FILTER_PARAM } from '~/search/sidebar/components/label_filter/data';
import { archivedFilterData } from '~/search/sidebar/components/archived_filter/data';
import { INCLUDE_FORKED_FILTER_PARAM } from '~/search/sidebar/components/forks_filter/index.vue';
+import { s__ } from '~/locale';
export const MAX_FREQUENT_ITEMS = 5;
@@ -40,6 +41,20 @@ export const ICON_MAP = {
snippet_titles: 'snippet',
};
+export const SCOPE_NAVIGATION_MAP = {
+ blobs: s__(`GlobalSearch|Code`),
+ issues: s__(`GlobalSearch|Issues`),
+ epics: s__(`GlobalSearch|'Epics`),
+ merge_requests: s__(`GlobalSearch|Merge request`),
+ commits: s__(`GlobalSearch|Commits`),
+ notes: s__(`GlobalSearch|Comments`),
+ milestones: s__(`GlobalSearch|Milestones`),
+ users: s__(`GlobalSearch|Users`),
+ projects: s__(`GlobalSearch|Projects`),
+ wiki_blobs: s__(`GlobalSearch|Wiki`),
+ snippet_titles: s__(`GlobalSearch|Snippets`),
+};
+
export const ZOEKT_SEARCH_TYPE = 'zoekt';
export const ADVANCED_SEARCH_TYPE = 'advanced';
export const BASIC_SEARCH_TYPE = 'basic';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
index e1cffbcbef2..27a9c492d1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -25,4 +25,5 @@ export const FAILURE_REASONS = {
requested_changes: __('The change requests must be completed or resolved.'),
approvals_syncing: __('The merge request approvals are currently syncing.'),
locked_lfs_files: __('All LFS files must be unlocked.'),
+ security_policy_evaluation: __('All security policies must be evaluated.'),
};
diff --git a/app/assets/javascripts/work_items/components/work_item_abuse_modal.vue b/app/assets/javascripts/work_items/components/work_item_abuse_modal.vue
new file mode 100644
index 00000000000..190fe3cae10
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_abuse_modal.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 39e944744ab..67348d17bfc 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -37,6 +37,7 @@ import {
I18N_WORK_ITEM_ERROR_COPY_REFERENCE,
I18N_WORK_ITEM_ERROR_COPY_EMAIL,
TEST_ID_LOCK_ACTION,
+ TEST_ID_REPORT_ABUSE,
} from '../constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
@@ -55,6 +56,7 @@ export default {
referenceCopied: __('Reference copied'),
emailAddressCopied: __('Email address copied'),
moreActions: __('More actions'),
+ reportAbuse: __('Report abuse'),
},
components: {
GlDisclosureDropdown,
@@ -79,6 +81,7 @@ export default {
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
lockDiscussionTestId: TEST_ID_LOCK_ACTION,
stateToggleTestId: TEST_ID_TOGGLE_ACTION,
+ reportAbuseActionTestId: TEST_ID_REPORT_ABUSE,
props: {
fullPath: {
type: String,
@@ -164,6 +167,11 @@ export default {
required: false,
default: false,
},
+ workItemAuthorId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -228,6 +236,9 @@ export default {
showDropdownTooltip() {
return !this.isDropdownVisible ? this.$options.i18n.moreActions : '';
},
+ isAuthor() {
+ return this.workItemAuthorId === window.gon.current_user_id;
+ },
},
methods: {
copyToClipboard(text, message) {
@@ -356,6 +367,10 @@ export default {
emitStateToggleError(error) {
this.$emit('error', error);
},
+ handleToggleReportAbuseModal() {
+ this.$emit('toggleReportAbuseModal', true);
+ this.closeDropdown();
+ },
},
};
@@ -452,8 +467,16 @@ export default {
{{ i18n.copyCreateNoteEmail }}
+
+
+ {{ $options.i18n.reportAbuse }}
+
+
-
@@ -592,11 +597,13 @@ export default {
:is-modal="isModal"
:work-item-state="workItem.state"
:has-children="hasChildren"
+ :work-item-author-id="workItemAuthorId"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@promotedToObjective="$emit('promotedToObjective', workItemIid)"
@workItemStateUpdated="$emit('workItemStateUpdated')"
+ @toggleReportAbuseModal="toggleReportAbuseModal"
/>
@@ -728,14 +735,14 @@ export default {
:work-item-full-path="modalWorkItemNamespaceFullPath"
:show="true"
@close="updateUrl"
- @openReportAbuse="toggleReportAbuseDrawer(true, $event)"
+ @openReportAbuse="toggleReportAbuseModal(true, $event)"
/>
-
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index d4feb8619a3..7230e96a2c4 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -97,8 +97,7 @@ export default {
updateHasNotes() {
this.hasNotes = true;
},
- openReportAbuseDrawer(reply) {
- this.hide();
+ openReportAbuseModal(reply) {
this.$emit('openReportAbuse', reply);
},
},
@@ -132,7 +131,7 @@ export default {
@deleteWorkItem="deleteWorkItem"
@update-modal="updateModal"
@has-notes="updateHasNotes"
- @openReportAbuse="openReportAbuseDrawer"
+ @openReportAbuse="openReportAbuseModal"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 086e6517023..5f27743c27c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -14,7 +14,6 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import {
FORM_TYPES,
@@ -30,6 +29,7 @@ import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemChildrenLoadMore from '../shared/work_item_children_load_more.vue';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
+import WorkItemAbuseModal from '../work_item_abuse_modal.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
import WorkItemChildrenWrapper from './work_item_children_wrapper.vue';
@@ -42,7 +42,7 @@ export default {
WidgetWrapper,
WorkItemLinksForm,
WorkItemDetailModal,
- AbuseCategorySelector,
+ WorkItemAbuseModal,
WorkItemChildrenWrapper,
WorkItemChildrenLoadMore,
GlToggle,
@@ -109,7 +109,7 @@ export default {
parentIssue: null,
formType: null,
workItem: null,
- isReportDrawerOpen: false,
+ isReportModalOpen: false,
reportedUserId: 0,
reportedUrl: '',
widgetName: TASKS_ANCHOR,
@@ -206,13 +206,13 @@ export default {
updateWorkItemIdUrlQuery({ iid } = {}) {
updateHistory({ url: setUrlParams({ work_item_iid: iid }), replace: true });
},
- toggleReportAbuseDrawer(isOpen, reply = {}) {
- this.isReportDrawerOpen = isOpen;
+ toggleReportAbuseModal(isOpen, reply = {}) {
+ this.isReportModalOpen = isOpen;
this.reportedUrl = reply.url;
this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0;
},
- openReportAbuseDrawer(reply) {
- this.toggleReportAbuseDrawer(true, reply);
+ openReportAbuseModal(reply) {
+ this.toggleReportAbuseModal(true, reply);
},
async fetchNextPage() {
if (this.hasNextPage && !this.fetchNextPageInProgress) {
@@ -355,14 +355,14 @@ export default {
:work-item-full-path="activeChildNamespaceFullPath"
@close="closeModal"
@workItemDeleted="handleWorkItemDeleted(activeChild)"
- @openReportAbuse="openReportAbuseDrawer"
+ @openReportAbuse="openReportAbuseModal"
/>
-
diff --git a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue
index 97db3dea3f9..1f379433554 100644
--- a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue
+++ b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue
@@ -62,6 +62,11 @@ export default {
required: false,
default: () => [],
},
+ workItemAuthorId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
computed: {
canUpdate() {
@@ -153,6 +158,7 @@ export default {
:work-item-create-note-email="workItem.createNoteEmail"
:work-item-state="workItem.state"
:is-modal="isModal"
+ :work-item-author-id="workItemAuthorId"
@deleteWorkItem="$emit('deleteWorkItem')"
@toggleWorkItemConfidentiality="
$emit('toggleWorkItemConfidentiality', !workItem.confidential)
@@ -160,6 +166,7 @@ export default {
@error="$emit('error')"
@promotedToObjective="$emit('promotedToObjective')"
@workItemStateUpdated="$emit('workItemStateUpdated')"
+ @toggleReportAbuseModal="$emit('toggleReportAbuseModal', true)"
/>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index f4574076d3e..8be385f3636 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -277,6 +277,7 @@ export const TEST_ID_LOCK_ACTION = 'lock-action';
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
+export const TEST_ID_REPORT_ABUSE = 'report-abuse-action';
export const TODO_ADD_ICON = 'todo-add';
export const TODO_DONE_ICON = 'todo-done';
diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
index 190417bdaeb..1d17f38cc6f 100644
--- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
+++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
@@ -60,6 +60,9 @@ module Types
value 'LOCKED_LFS_FILES',
value: :locked_lfs_files,
description: 'Merge request includes locked LFS files.'
+ value 'SECURITY_POLICIES_EVALUATING',
+ value: :security_policy_evaluation,
+ description: 'All security policies must be evaluated.'
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c4959cd6ee1..dc970922168 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1274,7 +1274,8 @@ class MergeRequest < ApplicationRecord
skip_external_status_check: merge_when_checks_pass_strat,
skip_requested_changes_check: merge_when_checks_pass_strat,
skip_jira_check: merge_when_checks_pass_strat,
- skip_locked_lfs_files_check: merge_when_checks_pass_strat
+ skip_locked_lfs_files_check: merge_when_checks_pass_strat,
+ skip_security_policy_check: merge_when_checks_pass_strat
}
end
diff --git a/config/initializers_before_autoloader/000_inflections.rb b/config/initializers_before_autoloader/000_inflections.rb
index 7723cfbcf9c..d6e0fbcd6e7 100644
--- a/config/initializers_before_autoloader/000_inflections.rb
+++ b/config/initializers_before_autoloader/000_inflections.rb
@@ -20,6 +20,7 @@ ActiveSupport::Inflector.inflections do |inflect|
dependency_proxy_blob_registry
design_management_repository_registry
dependency_proxy_manifest_registry
+ duo_enterprise
duo_pro
event_log
file_registry
diff --git a/config/routes.rb b/config/routes.rb
index fb3427696f4..ab750a7fcb3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -145,6 +145,7 @@ InitializerConnections.raise_if_new_database_connection do
scope :ide, as: :ide, format: false do
get '/', to: 'ide#index'
get '/project', to: 'ide#index'
+ # note: This path has a hardcoded reference in the FE `app/assets/javascripts/ide/constants.js`
get '/oauth_redirect', to: 'ide#oauth_redirect'
scope path: 'project/:project_id', as: :project, constraints: { project_id: Gitlab::PathRegex.full_namespace_route_regex } do
diff --git a/danger/plugins/settings_sections.rb b/danger/plugins/settings_sections.rb
new file mode 100644
index 00000000000..c58e2832e4a
--- /dev/null
+++ b/danger/plugins/settings_sections.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative '../../tooling/danger/settings_sections'
+
+module Danger
+ class SettingsSections < ::Danger::Plugin
+ include Tooling::Danger::SettingsSections
+ end
+end
diff --git a/danger/settings_sections/Dangerfile b/danger/settings_sections/Dangerfile
new file mode 100644
index 00000000000..05b5f8e1d5d
--- /dev/null
+++ b/danger/settings_sections/Dangerfile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+settings_sections.check!
diff --git a/db/docs/ci_instance_variables.yml b/db/docs/ci_instance_variables.yml
index 328a8b8378b..f61e587566f 100644
--- a/db/docs/ci_instance_variables.yml
+++ b/db/docs/ci_instance_variables.yml
@@ -8,4 +8,4 @@ description: CI/CD variables available to all projects and groups in an instance
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30156
milestone: '13.0'
gitlab_schema: gitlab_ci
-sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/459039
+exempt_from_sharding: true # table not used in .com nor Cells.
\ No newline at end of file
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 601261a86de..12be1235faa 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -35344,6 +35344,7 @@ Detailed representation of whether a GitLab merge request can be merged.
| `NOT_OPEN` | Merge request must be open before merging. |
| `PREPARING` | Merge request diff is being created. |
| `REQUESTED_CHANGES` | Indicates a reviewer has requested changes. |
+| `SECURITY_POLICIES_EVALUATING` | All security policies must be evaluated. |
| `UNCHECKED` | Merge status has not been checked. |
### `DiffPositionType`
@@ -36119,6 +36120,7 @@ Representation of mergeability check identifier.
| `NOT_APPROVED` | Checks whether the merge request is approved. |
| `NOT_OPEN` | Checks whether the merge request is open. |
| `REQUESTED_CHANGES` | Checks whether the merge request has changes requested. |
+| `SECURITY_POLICY_EVALUATION` | Checks whether the security policies are evaluated. |
| `STATUS_CHECKS_MUST_PASS` | Checks whether the external status checks pass. |
### `MergeabilityCheckStatus`
diff --git a/doc/development/documentation/styleguide/deprecations_and_removals.md b/doc/development/documentation/styleguide/deprecations_and_removals.md
index 4977824b549..f91c96553fc 100644
--- a/doc/development/documentation/styleguide/deprecations_and_removals.md
+++ b/doc/development/documentation/styleguide/deprecations_and_removals.md
@@ -5,7 +5,7 @@ group: unassigned
description: 'Guidelines for deprecations and page removals'
---
-## Deprecations and removals
+# Deprecations and removals
When GitLab deprecates or removes a feature, use the following process to update the documentation.
This process requires temporarily changing content to be "deprecated" or "removed" before it's deleted.
@@ -16,7 +16,7 @@ NOTE:
A separate process exists for [GraphQL docs](../../api_graphql_styleguide.md#deprecating-schema-items)
and [REST API docs](../restful_api_styleguide.md#deprecations).
-### Deprecate a page or topic
+## Deprecate a page or topic
To deprecate a page or topic:
@@ -67,7 +67,7 @@ To deprecate a page or topic:
1. Open a merge request to add the word `(deprecated)` to the left nav, after the page title.
-### Remove a page
+## Remove a page
Mark content as removed during the release the feature was removed.
The title and a removed indicator remains until three months after the removal.
@@ -107,7 +107,7 @@ To remove a page:
This content is removed from the documentation as part of the Technical Writing team's
[regularly scheduled tasks](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks).
-### Remove a topic
+## Remove a topic
To remove a topic:
@@ -136,7 +136,7 @@ To remove a topic:
This content is removed from the documentation as part of the Technical Writing team's
[regularly scheduled tasks](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks).
-### Removing version-specific upgrade pages
+## Removing version-specific upgrade pages
Version-specific upgrade pages are in the `doc/update/versions/` directory.
diff --git a/doc/development/work_items.md b/doc/development/work_items.md
index ba21402b56a..86f951dd993 100644
--- a/doc/development/work_items.md
+++ b/doc/development/work_items.md
@@ -4,7 +4,7 @@ group: Project Management
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
-## Work items development
+# Work items development
- Work item lists are only available at group level `http://gdk.test:3000/groups/flightjs/-/work_items`,
they are enabled with feature flags: `namespace_level_work_items` and `work_item_epics_rollout`.
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 47810c59fa2..5e3c5ae109c 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -95,12 +95,23 @@ Prerequisites:
- You must be an administrator.
-In GitLab 15.7 and later, you can [use the application settings API to disable personal access tokens](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
+Depending on your GitLab version, you can use either the application settings API
+or the Admin UI to disable personal access tokens.
+
+### Use the application settings API
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384201) in GitLab 15.7.
+
+In GitLab 15.7 and later, you can use the [`disable_personal_access_tokens` attribute in the application settings API](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls) to disable personal access tokens.
NOTE:
After you have used the API to disable personal access tokens, those tokens cannot be used in subsequent API calls to manage this setting. To re-enable personal access tokens, you must use the [GitLab Rails console](../../administration/operations/rails_console.md). You can also upgrade to GitLab 17.3 or later so you can use the Admin UI instead.
-In GitLab 17.3 and later, you can disable personal access tokens in the Admin UI:
+### Use the Admin UI
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436991) in GitLab 17.3.
+
+In GitLab 17.3 and later, you can use the Admin UI to disable personal access tokens:
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > General**.
diff --git a/doc/user/project/repository/code_suggestions/supported_extensions.md b/doc/user/project/repository/code_suggestions/supported_extensions.md
index 3b5fd9b2d00..9097143b7de 100644
--- a/doc/user/project/repository/code_suggestions/supported_extensions.md
+++ b/doc/user/project/repository/code_suggestions/supported_extensions.md
@@ -121,7 +121,7 @@ When you're ready to start coding:
1. Open relevant files, including configuration files, to provide better context.
1. Close any files you don't want to be used as context.
-## View Multiple Code Suggestions
+## View multiple code suggestions
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/1325) in GitLab 17.1.
@@ -130,10 +130,14 @@ might be available. To view all available suggestions:
1. Hover over the code completion suggestion.
1. Scroll through the alternatives. Either:
- - Use keyboard shortcuts. Press Option + `]` to view the
- next suggestion, and Option + `[` to view the previous
- suggestions.
- - Select the right or left arrow to see next or previous options.
+ - Use keyboard shortcuts:
+ - On a Mac, press Option + ] to view the
+ next suggestion, and Option + [ to view the previous
+ suggestions.
+ - On Windows, press Alt + ] to view the
+ next suggestion, and Alt + [ to view the previous
+ suggestions.
+ - On the dialog that's displayed, select the right or left arrow to see next or previous options.
1. Press Tab to apply the suggestion you prefer.
## Add additional languages for Code Suggestions
diff --git a/lib/search/group_settings.rb b/lib/search/group_settings.rb
index 32903860e7d..96c3fb8878b 100644
--- a/lib/search/group_settings.rb
+++ b/lib/search/group_settings.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Search
+ # Generates a list of all available setting sections of a group.
+ # This list is used by the command palette's search functionality.
class GroupSettings
include Rails.application.routes.url_helpers
diff --git a/lib/search/project_settings.rb b/lib/search/project_settings.rb
index 413b69a0ac8..645783c83b9 100644
--- a/lib/search/project_settings.rb
+++ b/lib/search/project_settings.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Search
+ # Generates a list of all available setting sections of a project.
+ # This list is used by the command palette's search functionality.
class ProjectSettings
include Rails.application.routes.url_helpers
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index aff58159ae8..55d5b2460af 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1716,9 +1716,6 @@ msgstr ""
msgid "- Push code to the repository."
msgstr ""
-msgid "- Select -"
-msgstr ""
-
msgid "- User"
msgid_plural "- Users"
msgstr[0] ""
@@ -5449,6 +5446,9 @@ msgstr ""
msgid "All required approvals must be given."
msgstr ""
+msgid "All security policies must be evaluated."
+msgstr ""
+
msgid "All threads resolved!"
msgstr ""
@@ -13375,7 +13375,7 @@ msgstr ""
msgid "Company"
msgstr ""
-msgid "Company Name"
+msgid "Company name"
msgstr ""
msgid "Compare"
@@ -15525,7 +15525,7 @@ msgstr ""
msgid "Couldn't reorder child due to an internal error."
msgstr ""
-msgid "Country / Region"
+msgid "Country or region"
msgstr ""
msgid "Counts"
@@ -19665,6 +19665,18 @@ msgstr ""
msgid "DuoCodeReview|I have encountered some issues while I was reviewing. Please try again later."
msgstr ""
+msgid "DuoEnterpriseTrial|Start your free Duo Enterprise trial"
+msgstr ""
+
+msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial"
+msgstr ""
+
+msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial on %{group_name}"
+msgstr ""
+
+msgid "DuoEnterpriseTrial|We just need some additional information to activate your trial."
+msgstr ""
+
msgid "DuoProDiscover|Accelerate your path to market"
msgstr ""
@@ -22826,9 +22838,6 @@ msgstr ""
msgid "Finished"
msgstr ""
-msgid "First Name"
-msgstr ""
-
msgid "First Seen"
msgstr ""
@@ -24392,12 +24401,18 @@ msgstr ""
msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list."
msgstr ""
+msgid "GlobalSearch|%{lessButtonStart}Show less%{lessButtonEnd} - Too many matches found. Showing %{showingMatches} chunks out of %{fileMatches} results. %{fileLinkStart}Open the file to view all.%{fileLinkEnd}"
+msgstr ""
+
msgid "GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is disabled since %{ref_elem} is not the default branch. %{docs_link}"
msgstr ""
msgid "GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is enabled."
msgstr ""
+msgid "GlobalSearch|'Epics"
+msgstr ""
+
msgid "GlobalSearch|Aggregations load error."
msgstr ""
@@ -24410,12 +24425,21 @@ msgstr ""
msgid "GlobalSearch|Change context %{kbdStart}↵%{kbdEnd}"
msgstr ""
+msgid "GlobalSearch|Code"
+msgstr ""
+
msgid "GlobalSearch|Command palette"
msgstr ""
msgid "GlobalSearch|Commands %{superKey} %{link2Start}k%{link2End}"
msgstr ""
+msgid "GlobalSearch|Comments"
+msgstr ""
+
+msgid "GlobalSearch|Commits"
+msgstr ""
+
msgid "GlobalSearch|Could not load search results. Please refresh the page to try again."
msgstr ""
@@ -24500,6 +24524,9 @@ msgstr ""
msgid "GlobalSearch|Merge Requests"
msgstr ""
+msgid "GlobalSearch|Merge request"
+msgstr ""
+
msgid "GlobalSearch|Merge requests I've created"
msgstr ""
@@ -24509,9 +24536,15 @@ msgstr ""
msgid "GlobalSearch|Merge requests that I'm a reviewer"
msgstr ""
+msgid "GlobalSearch|Milestones"
+msgstr ""
+
msgid "GlobalSearch|No labels found"
msgstr ""
+msgid "GlobalSearch|No results found"
+msgstr ""
+
msgid "GlobalSearch|No results found. Edit your search and try again."
msgstr ""
@@ -24521,6 +24554,9 @@ msgstr ""
msgid "GlobalSearch|Only first %{max_shown} of not indexed projects is shown"
msgstr ""
+msgid "GlobalSearch|Open file in repository"
+msgstr ""
+
msgid "GlobalSearch|Pages or actions"
msgstr ""
@@ -24587,12 +24623,21 @@ msgstr ""
msgid "GlobalSearch|Settings"
msgstr ""
+msgid "GlobalSearch|Show %{matches} more matches"
+msgstr ""
+
+msgid "GlobalSearch|Show less"
+msgstr ""
+
msgid "GlobalSearch|Show more"
msgstr ""
msgid "GlobalSearch|Showing top %{maxItems}"
msgstr ""
+msgid "GlobalSearch|Snippets"
+msgstr ""
+
msgid "GlobalSearch|The search term must be at least 3 characters long."
msgstr ""
@@ -24629,9 +24674,21 @@ msgstr ""
msgid "GlobalSearch|View syntax options."
msgstr ""
+msgid "GlobalSearch|We couldn't find any %{scope} matching %{term}"
+msgstr ""
+
+msgid "GlobalSearch|We couldn't find any %{scope} matching %{term} in group %{group}"
+msgstr ""
+
+msgid "GlobalSearch|We couldn't find any %{scope} matching %{term} in project %{project}"
+msgstr ""
+
msgid "GlobalSearch|What are you searching for?"
msgstr ""
+msgid "GlobalSearch|Wiki"
+msgstr ""
+
msgid "GlobalSearch|Your work"
msgstr ""
@@ -26750,15 +26807,15 @@ msgstr ""
msgid "IDE|Contact your administrator or try to open the Web IDE again with another domain."
msgstr ""
+msgid "IDE|Could not find a callback URL entry for %{expectedCallbackUrl}."
+msgstr ""
+
msgid "IDE|Edit"
msgstr ""
msgid "IDE|Editing this application might affect the functionality of the Web IDE. Ensure the configuration meets the following conditions:"
msgstr ""
-msgid "IDE|Error reloading page"
-msgstr ""
-
msgid "IDE|GitLab logo"
msgstr ""
@@ -30876,9 +30933,6 @@ msgstr ""
msgid "Last GitLab activity"
msgstr ""
-msgid "Last Name"
-msgstr ""
-
msgid "Last Seen"
msgstr ""
@@ -39888,9 +39942,6 @@ msgstr ""
msgid "Please select a Jira project"
msgstr ""
-msgid "Please select a country / region"
-msgstr ""
-
msgid "Please select a group"
msgstr ""
@@ -49240,6 +49291,9 @@ msgstr ""
msgid "Select a comment template"
msgstr ""
+msgid "Select a country or region"
+msgstr ""
+
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -56302,12 +56356,15 @@ msgstr ""
msgid "Trials|Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you’re enjoying the features of GitLab %{planName}. To keep those features after your trial ends, you’ll need to buy a subscription. (You can also choose GitLab Premium if it meets your needs.)"
msgstr ""
-msgid "Trial| By selecting Continue or registering through a third party, you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}"
+msgid "Trial|Activate my trial"
msgstr ""
msgid "Trial|Allowed characters: +, 0-9, -, and spaces."
msgstr ""
+msgid "Trial|By clicking \"%{buttonText}\" you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}"
+msgstr ""
+
msgid "Trial|Continue"
msgstr ""
@@ -56317,16 +56374,19 @@ msgstr ""
msgid "Trial|GitLab Subscription Agreement"
msgstr ""
-msgid "Trial|Please select"
+msgid "Trial|Privacy Statement"
msgstr ""
-msgid "Trial|Privacy Statement"
+msgid "Trial|Select number of employees"
+msgstr ""
+
+msgid "Trial|Select state or province"
msgstr ""
msgid "Trial|Start free GitLab Ultimate trial"
msgstr ""
-msgid "Trial|State/Province"
+msgid "Trial|State or province"
msgstr ""
msgid "Trial|To activate your trial, we need additional details from you."
@@ -58668,6 +58728,9 @@ msgstr ""
msgid "View File Metadata"
msgstr ""
+msgid "View Line in repository"
+msgstr ""
+
msgid "View Stage: %{title}"
msgstr ""
@@ -59527,6 +59590,9 @@ msgstr ""
msgid "We're experiencing difficulties and this tab content is currently unavailable."
msgstr ""
+msgid "We're sorry, your GitLab Duo Enterprise trial could not be created because our system did not respond successfully."
+msgstr ""
+
msgid "We're sorry, your GitLab Duo Pro trial could not be created because our system did not respond successfully."
msgstr ""
diff --git a/package.json b/package.json
index 4bb4b06cfae..ef1d1db066e 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.109.0",
- "@gitlab/ui": "87.8.0",
+ "@gitlab/ui": "88.0.0",
"@gitlab/web-ide": "^0.0.1-dev-20240613133550",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-4",
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb
index 9d7085067cd..0378aa6e718 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb
@@ -71,7 +71,7 @@ module QA
runner.remove_via_api!
end
- it 'runs in project pipeline with correct inputs', :aggregate_failures,
+ it 'runs in project pipeline with correct inputs', :blocking, :aggregate_failures,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/451582' do
Flow::Pipeline.visit_latest_pipeline
diff --git a/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js b/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js
index 1ce0201f5e9..2de27b26aae 100644
--- a/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js
+++ b/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js
@@ -1,87 +1,111 @@
-import { GlButton, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import OAuthDomainMismatchError from '~/ide/components/oauth_domain_mismatch_error.vue';
-const MOCK_CALLBACK_URL_ORIGIN = 'https://example1.com';
-const MOCK_PATH_NAME = '/path/to/ide';
+const MOCK_CALLBACK_URLS = [
+ {
+ base: 'https://example1.com/',
+ },
+ {
+ base: 'https://example2.com/',
+ },
+ {
+ base: 'https://example3.com/relative-path/',
+ },
+];
+const MOCK_CALLBACK_URL = 'https://example.com';
+const MOCK_PATH_NAME = 'path/to/ide';
+
+const EXPECTED_DROPDOWN_ITEMS = MOCK_CALLBACK_URLS.map(({ base }) => ({
+ text: base,
+ href: `${base}${MOCK_PATH_NAME}`,
+}));
describe('OAuthDomainMismatchError', () => {
- useMockLocationHelper();
-
let wrapper;
- let originalLocation;
const findButton = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
- const findDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const createWrapper = (props = {}) => {
wrapper = mount(OAuthDomainMismatchError, {
propsData: {
- callbackUrlOrigins: [MOCK_CALLBACK_URL_ORIGIN],
+ expectedCallbackUrl: MOCK_CALLBACK_URL,
+ callbackUrls: MOCK_CALLBACK_URLS,
...props,
},
});
};
beforeEach(() => {
- originalLocation = window.location;
- window.location.pathname = MOCK_PATH_NAME;
- });
-
- afterEach(() => {
- window.location = originalLocation;
+ setWindowLocation(`/${MOCK_PATH_NAME}`);
});
describe('single callback URL domain passed', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper({
+ callbackUrls: MOCK_CALLBACK_URLS.slice(0, 1),
+ });
+ });
+
+ it('renders expected callback URL message', () => {
+ expect(wrapper.text()).toContain(
+ `Could not find a callback URL entry for ${MOCK_CALLBACK_URL}.`,
+ );
});
it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
- it('reloads page with correct url on button click', async () => {
- findButton().vm.$emit('click');
- await nextTick();
-
- expect(window.location.replace).toHaveBeenCalledTimes(1);
- expect(window.location.replace).toHaveBeenCalledWith(
- new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(),
- );
+ it('renders button with correct attributes', () => {
+ const button = findButton();
+ expect(button.exists()).toBe(true);
+ const baseUrl = MOCK_CALLBACK_URLS[0].base;
+ expect(button.text()).toContain(baseUrl);
+ expect(button.attributes('href')).toBe(`${baseUrl}${MOCK_PATH_NAME}`);
});
});
describe('multiple callback URL domains passed', () => {
- const MOCK_CALLBACK_URL_ORIGINS = [MOCK_CALLBACK_URL_ORIGIN, 'https://example2.com'];
-
beforeEach(() => {
- createWrapper({ callbackUrlOrigins: MOCK_CALLBACK_URL_ORIGINS });
+ createWrapper();
});
- it('renders dropdown', () => {
- expect(findDropdown().exists()).toBe(true);
+ it('renders dropdown with correct items', () => {
+ const dropdown = findDropdown();
+
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS);
+ });
+ });
+
+ describe('with erroneous callback from current origin', () => {
+ beforeEach(() => {
+ createWrapper({
+ callbackUrls: MOCK_CALLBACK_URLS.concat({
+ base: `${TEST_HOST}/foo`,
+ }),
+ });
});
- it('renders dropdown items', () => {
- const dropdownItems = findDropdownItems();
- expect(dropdownItems.length).toBe(MOCK_CALLBACK_URL_ORIGINS.length);
- expect(dropdownItems.at(0).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[0]);
- expect(dropdownItems.at(1).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[1]);
+ it('filters out item with current origin', () => {
+ expect(findDropdown().props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS);
+ });
+ });
+
+ describe('when no callback URL passed', () => {
+ beforeEach(() => {
+ createWrapper({
+ callbackUrls: [],
+ });
});
- it('reloads page with correct url on dropdown item click', async () => {
- const dropdownItem = findDropdownItems().at(0);
- dropdownItem.vm.$emit('select', MOCK_CALLBACK_URL_ORIGIN);
- await nextTick();
-
- expect(window.location.replace).toHaveBeenCalledTimes(1);
- expect(window.location.replace).toHaveBeenCalledWith(
- new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(),
- );
+ it('does not render dropdown or button', () => {
+ expect(findDropdown().exists()).toBe(false);
+ expect(findButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js
index 3c390bdcf41..08c49bb4f3c 100644
--- a/spec/frontend/ide/helpers.js
+++ b/spec/frontend/ide/helpers.js
@@ -1,7 +1,6 @@
import * as pathUtils from 'path';
-import { commitActionTypes } from '~/ide/constants';
+import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, commitActionTypes } from '~/ide/constants';
import { decorateData } from '~/ide/stores/utils';
-import { WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/lib/gitlab_web_ide/get_oauth_config';
export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
diff --git a/spec/frontend/ide/index_spec.js b/spec/frontend/ide/index_spec.js
index 414c963aeb7..8f79af84a63 100644
--- a/spec/frontend/ide/index_spec.js
+++ b/spec/frontend/ide/index_spec.js
@@ -2,12 +2,14 @@ import { startIde } from '~/ide/index';
import { IDE_ELEMENT_ID } from '~/ide/constants';
import { OAuthCallbackDomainMismatchErrorApp } from '~/ide/oauth_callback_domain_mismatch_error';
import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/ide/init_gitlab_web_ide');
-const MOCK_CALLBACK_URL = `${window.location.origin}/ide/redirect`;
+const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect';
const MOCK_DATA_SET = {
- callbackUrls: JSON.stringify([MOCK_CALLBACK_URL]),
+ callbackUrls: JSON.stringify([`${TEST_HOST}/-/ide/oauth_redirect`]),
useNewWebIde: true,
};
/**
@@ -27,12 +29,20 @@ const setupMockIdeElement = (customData = MOCK_DATA_SET) => {
};
describe('startIde', () => {
+ let renderErrorSpy;
+
+ beforeEach(() => {
+ setWindowLocation(`${TEST_HOST}/-/ide/edit/gitlab-org/gitlab`);
+ renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError');
+ });
+
afterEach(() => {
- document.getElementById(IDE_ELEMENT_ID).remove();
+ document.getElementById(IDE_ELEMENT_ID)?.remove();
});
describe('when useNewWebIde feature flag is true', () => {
let ideElement;
+
beforeEach(async () => {
ideElement = setupMockIdeElement();
@@ -43,34 +53,14 @@ describe('startIde', () => {
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
expect(initGitlabWebIDE).toHaveBeenCalledWith(ideElement);
});
+
+ it('does not render error page', () => {
+ expect(renderErrorSpy).not.toHaveBeenCalled();
+ });
});
- describe('OAuth callback origin mismatch check', () => {
- let renderErrorSpy;
-
- beforeEach(() => {
- renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError');
- });
-
- it('does not render error page if no callbackUrl provided', async () => {
- setupMockIdeElement({ useNewWebIde: true });
- await startIde();
-
- expect(renderErrorSpy).not.toHaveBeenCalled();
- expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
- });
-
- it('does not call renderOAuthDomainMismatchError if no mismatch detected', async () => {
- setupMockIdeElement();
- await startIde();
-
- expect(renderErrorSpy).not.toHaveBeenCalled();
- expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
- });
-
- it('renders error page if OAuth callback origin does not match window.location.origin', async () => {
- const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect';
- renderErrorSpy.mockImplementation(() => {});
+ describe('with mismatch callback url', () => {
+ it('renders error page', async () => {
setupMockIdeElement({
callbackUrls: JSON.stringify([MOCK_MISMATCH_CALLBACK_URL]),
useNewWebIde: true,
@@ -82,4 +72,17 @@ describe('startIde', () => {
expect(initGitlabWebIDE).not.toHaveBeenCalled();
});
});
+
+ describe('with relative URL location and mismatch callback url', () => {
+ it('renders error page', async () => {
+ setWindowLocation(`${TEST_HOST}/relative-path/-/ide/edit/project`);
+
+ setupMockIdeElement();
+
+ await startIde();
+
+ expect(renderErrorSpy).toHaveBeenCalledTimes(1);
+ expect(initGitlabWebIDE).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 5f7e4caed19..3f905574d2d 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -135,7 +135,7 @@ describe('ide/init_gitlab_web_ide', () => {
mrTargetProject: '',
forkInfo: null,
username: gon.current_username,
- gitlabUrl: TEST_HOST,
+ gitlabUrl: `${TEST_HOST}/`,
nonce: TEST_NONCE,
httpHeaders: {
'mock-csrf-header': 'mock-csrf-token',
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
index 3c42f54a1f7..e50868d69ad 100644
--- a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
+++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
@@ -16,7 +16,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
expect(actual).toEqual({
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- gitlabUrl: TEST_HOST,
+ gitlabUrl: `${TEST_HOST}/`,
});
});
@@ -27,7 +27,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
expect(actual).toEqual({
baseUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}`,
+ gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/`,
});
});
});
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/oauth_callback_urls_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/oauth_callback_urls_spec.js
new file mode 100644
index 00000000000..61a8e3288d8
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/oauth_callback_urls_spec.js
@@ -0,0 +1,89 @@
+import {
+ parseCallbackUrls,
+ getOAuthCallbackUrl,
+} from '~/ide/lib/gitlab_web_ide/oauth_callback_urls';
+import { logError } from '~/lib/logger';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { IDE_PATH, WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/constants';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+jest.mock('~/lib/logger');
+
+const MOCK_IDE_PATH = joinPaths(IDE_PATH, 'some/path');
+
+describe('ide/lib/oauth_callback_urls', () => {
+ describe('getOAuthCallbackUrl', () => {
+ const mockPath = MOCK_IDE_PATH;
+ const MOCK_RELATIVE_PATH = 'relative-path';
+ const mockPathWithRelative = joinPaths(MOCK_RELATIVE_PATH, MOCK_IDE_PATH);
+
+ const originalHref = window.location.href;
+
+ afterEach(() => {
+ setWindowLocation(originalHref);
+ });
+
+ const expectedBaseUrlWithRelative = joinPaths(window.location.origin, MOCK_RELATIVE_PATH);
+
+ it.each`
+ path | expectedCallbackBaseUrl
+ ${mockPath} | ${window.location.origin}
+ ${mockPathWithRelative} | ${expectedBaseUrlWithRelative}
+ `(
+ 'retrieves expected callback URL based on window url',
+ ({ path, expectedCallbackBaseUrl }) => {
+ setWindowLocation(path);
+
+ const actual = getOAuthCallbackUrl();
+ const expected = joinPaths(expectedCallbackBaseUrl, WEB_IDE_OAUTH_CALLBACK_URL_PATH);
+ expect(actual).toEqual(expected);
+ },
+ );
+ });
+ describe('parseCallbackUrls', () => {
+ it('parses the given JSON URL array and returns some metadata for them', () => {
+ const actual = parseCallbackUrls(
+ JSON.stringify([
+ 'https://gitlab.com/-/ide/oauth_redirect',
+ 'not a url',
+ 'https://gdk.test:3443/-/ide/oauth_redirect/',
+ 'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo',
+ 'https://example.com/not-a-real-one-/ide/oauth_redirectz',
+ ]),
+ );
+
+ expect(actual).toEqual([
+ {
+ base: 'https://gitlab.com/',
+ url: 'https://gitlab.com/-/ide/oauth_redirect',
+ },
+ {
+ base: 'https://gdk.test:3443/',
+ url: 'https://gdk.test:3443/-/ide/oauth_redirect/',
+ },
+ {
+ base: 'https://gdk.test:3443/gitlab/',
+ url: 'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo',
+ },
+ {
+ base: 'https://example.com/',
+ url: 'https://example.com/not-a-real-one-/ide/oauth_redirectz',
+ },
+ ]);
+ });
+
+ it('returns empty when given empty', () => {
+ expect(parseCallbackUrls('')).toEqual([]);
+ expect(logError).not.toHaveBeenCalled();
+ });
+
+ it('returns empty when not valid JSON', () => {
+ expect(parseCallbackUrls('babar')).toEqual([]);
+ expect(logError).toHaveBeenCalledWith('Failed to parse callback URLs JSON');
+ });
+
+ it('returns empty when not array JSON', () => {
+ expect(parseCallbackUrls('{}')).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/ide/mount_oauth_callback_spec.js b/spec/frontend/ide/mount_oauth_callback_spec.js
index 9187fffca81..e63ef9c0496 100644
--- a/spec/frontend/ide/mount_oauth_callback_spec.js
+++ b/spec/frontend/ide/mount_oauth_callback_spec.js
@@ -46,7 +46,7 @@ describe('~/ide/mount_oauth_callback', () => {
clientId: TEST_OAUTH_CLIENT_ID,
protectRefreshToken: true,
},
- gitlabUrl: TEST_HOST,
+ gitlabUrl: `${TEST_HOST}/`,
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
username: TEST_USERNAME,
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 75f8a1f342d..f29fa1f76cf 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1304,4 +1304,21 @@ describe('URL utility', () => {
expect(urlUtils.buildURLwithRefType({ base, path, refType })).toBe(output);
});
});
+
+ describe('stripRelativeUrlRootFromPath', () => {
+ it.each`
+ relativeUrlRoot | path | expectation
+ ${''} | ${'/foo/bar'} | ${'/foo/bar'}
+ ${'/'} | ${'/foo/bar'} | ${'/foo/bar'}
+ ${'/foo'} | ${'/foo/bar'} | ${'/bar'}
+ ${'/gitlab/'} | ${'/gitlab/-/ide/foo'} | ${'/-/ide/foo'}
+ `(
+ 'with relative_url_root="$relativeUrlRoot", "$path" should return "$expectation"',
+ ({ relativeUrlRoot, path, expectation }) => {
+ window.gon.relative_url_root = relativeUrlRoot;
+
+ expect(urlUtils.stripRelativeUrlRootFromPath(path)).toBe(expectation);
+ },
+ );
+ });
});
diff --git a/spec/frontend/search/results/components/__snapshots__/result_empty_spec.js.snap b/spec/frontend/search/results/components/__snapshots__/result_empty_spec.js.snap
new file mode 100644
index 00000000000..b2590297b8f
--- /dev/null
+++ b/spec/frontend/search/results/components/__snapshots__/result_empty_spec.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GlobalSearchResultsEmpty component basics renders all parts of header 1`] = `
+
+
+
+`;
diff --git a/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap b/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap
index e3f226345bd..be8f34ad824 100644
--- a/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap
+++ b/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap
@@ -3,5 +3,89 @@
exports[`ZoektBlobResults when component loads normally renders component properly 1`] = `
+>
+
+
+
`;
diff --git a/spec/frontend/search/results/components/blob_body_spec.js b/spec/frontend/search/results/components/blob_body_spec.js
new file mode 100644
index 00000000000..f821c497277
--- /dev/null
+++ b/spec/frontend/search/results/components/blob_body_spec.js
@@ -0,0 +1,45 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BlobChunks from '~/search/results/components/blob_chunks.vue';
+import ZoektBlobResultsChunks from '~/search/results/components/blob_body.vue';
+import eventHub from '~/search/results/event_hub';
+import { mockDataForBlobBody } from '../../mock_data';
+
+describe('BlobChunks', () => {
+ let wrapper;
+
+ const createComponent = (file = {}) => {
+ wrapper = shallowMountExtended(ZoektBlobResultsChunks, {
+ propsData: {
+ file,
+ },
+ });
+ };
+
+ const findBlobChunks = () => wrapper.findAllComponents(BlobChunks);
+
+ describe('component basics', () => {
+ beforeEach(() => {
+ createComponent(mockDataForBlobBody);
+ });
+
+ it(`renders default amount of chunks`, () => {
+ expect(findBlobChunks()).toHaveLength(3);
+ expect(findBlobChunks().at(0).props()).toMatchObject({
+ chunk: {
+ lines: expect.any(Array),
+ matchCountInChunk: expect.any(Number),
+ __typename: expect.any(String),
+ },
+ blameLink: 'blame/test.js',
+ fileUrl: 'https://gitlab.com/file/test.js',
+ });
+ });
+
+ it(`renders all chunks`, async () => {
+ eventHub.$emit('showMore', { id: 'Testjs/Test:file/test.js', state: true });
+ await nextTick();
+ expect(findBlobChunks()).toHaveLength(4);
+ });
+ });
+});
diff --git a/spec/frontend/search/results/components/blob_chunks_spec.js b/spec/frontend/search/results/components/blob_chunks_spec.js
new file mode 100644
index 00000000000..0bd5f63d767
--- /dev/null
+++ b/spec/frontend/search/results/components/blob_chunks_spec.js
@@ -0,0 +1,77 @@
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BlobChunks from '~/search/results/components/blob_chunks.vue';
+
+describe('BlobChunks', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(BlobChunks, {
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ GlLink,
+ },
+ });
+ };
+
+ const findGlIcon = () => wrapper.findAllComponents(GlIcon);
+ const findGlLink = () => wrapper.findAllComponents(GlLink);
+ const findLine = () => wrapper.findAllByTestId('search-blob-line');
+ const findLineNumbers = () => wrapper.findAllByTestId('search-blob-line-numbers');
+ const findLineCode = () => wrapper.findAllByTestId('search-blob-line-code');
+ const findRootElement = () => wrapper.find('#search-blob-content');
+
+ describe('component basics', () => {
+ beforeEach(() => {
+ createComponent({
+ chunk: {
+ lines: [
+ {
+ lineNumber: 1,
+ richText: '',
+ text: '',
+ __typename: 'SearchBlobLine',
+ },
+ {
+ lineNumber: 2,
+ richText: 'test1',
+ text: 'test1',
+ __typename: 'SearchBlobLine',
+ },
+ { lineNumber: 3, richText: '', text: '', __typename: 'SearchBlobLine' },
+ ],
+ matchCountInChunk: 1,
+ __typename: 'SearchBlobChunk',
+ },
+ blameLink: 'https://gitlab.com/blame/test.js',
+ fileUrl: 'https://gitlab.com/file/test.js',
+ });
+ });
+
+ it(`renders default state`, () => {
+ expect(findLine()).toHaveLength(3);
+ expect(findLineNumbers()).toHaveLength(3);
+ expect(findLineCode()).toHaveLength(3);
+ expect(findGlLink()).toHaveLength(6);
+ expect(findGlIcon()).toHaveLength(3);
+ });
+
+ it(`renders proper colors`, () => {
+ expect(findRootElement().classes('white')).toBe(true);
+ expect(findLineCode().at(1).find('b').classes('hll')).toBe(true);
+ });
+
+ it(`renders links correctly`, () => {
+ expect(findGlLink().at(0).attributes('href')).toBe('https://gitlab.com/blame/test.js#L1');
+ expect(findGlLink().at(0).attributes('title')).toBe('View blame');
+ expect(findGlLink().at(0).findComponent(GlIcon).exists()).toBe(true);
+ expect(findGlLink().at(0).findComponent(GlIcon).props('name')).toBe('git');
+
+ expect(findGlLink().at(1).attributes('href')).toBe('https://gitlab.com/file/test.js#L1');
+ expect(findGlLink().at(1).attributes('title')).toBe('View Line in repository');
+ expect(findGlLink().at(1).text()).toBe('1');
+ });
+ });
+});
diff --git a/spec/frontend/search/results/components/blob_footer_spec.js b/spec/frontend/search/results/components/blob_footer_spec.js
new file mode 100644
index 00000000000..d494b434e29
--- /dev/null
+++ b/spec/frontend/search/results/components/blob_footer_spec.js
@@ -0,0 +1,107 @@
+import { nextTick } from 'vue';
+import { GlSprintf, GlButton, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BlobFooter from '~/search/results/components/blob_footer.vue';
+import eventHub from '~/search/results/event_hub';
+import { mockDataForBlobBody } from '../../mock_data';
+
+describe('BlobFooter', () => {
+ let wrapper;
+ let spy;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(BlobFooter, {
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ GlLink,
+ },
+ });
+ };
+
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ describe('component basics', () => {
+ beforeEach(() => {
+ createComponent({
+ file: mockDataForBlobBody,
+ });
+ spy = jest.spyOn(eventHub, '$emit');
+ });
+
+ it(`renders default closed state`, () => {
+ expect(findGlButton().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Show 1 more matches');
+ });
+
+ it(`renders default open state`, async () => {
+ findGlButton().vm.$emit('click');
+ await nextTick();
+ expect(spy).toHaveBeenCalledWith('showMore', {
+ id: 'Testjs/Test:file/test.js',
+ state: true,
+ });
+ expect(wrapper.text()).toContain('Show less');
+ });
+ });
+
+ describe('component with too many results', () => {
+ beforeEach(() => {
+ createComponent({
+ // matchCountTotal: 100,
+ // matchCount: 100,
+ // filePath: 'test/file.js',
+ // projectPath: 'Testjs/Test',
+ // fileLink: 'https://gitlab.com/test/file.js',
+ file: {
+ ...mockDataForBlobBody,
+ chunks: [
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ...mockDataForBlobBody.chunks,
+ ],
+ matchCountTotal: 200,
+ },
+ });
+ });
+
+ it(`renders closed state`, () => {
+ expect(findGlButton().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Show 97 more matches');
+ });
+
+ it(`renders open state`, async () => {
+ findGlButton().vm.$emit('click');
+ await nextTick();
+ expect(findGlLink().exists()).toBe(true);
+ expect(wrapper.text()).toContain(
+ 'Show less - Too many matches found. Showing 50 chunks out of 200 results. Open the file to view all.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/search/results/components/blob_header_spec.js b/spec/frontend/search/results/components/blob_header_spec.js
new file mode 100644
index 00000000000..e94119228b0
--- /dev/null
+++ b/spec/frontend/search/results/components/blob_header_spec.js
@@ -0,0 +1,54 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import BlobHeader from '~/search/results/components/blob_header.vue';
+
+describe('BlobHeader', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(BlobHeader, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
+ const findFileIcon = () => wrapper.findComponent(FileIcon);
+ const findProjectPath = () => wrapper.findByTestId('project-path-content');
+ const findProjectName = () => wrapper.findByTestId('file-name-content');
+
+ describe('component basics', () => {
+ beforeEach(() => {
+ createComponent({
+ filePath: 'test/file.js',
+ projectPath: 'Testjs/Test',
+ fileUrl: 'https://gitlab.com/test/file.js',
+ });
+ });
+
+ it(`renders all parts of header`, () => {
+ expect(findClipboardButton().exists()).toBe(true);
+ expect(findFileIcon().exists()).toBe(true);
+ expect(findProjectPath().exists()).toBe(true);
+ expect(findProjectName().exists()).toBe(true);
+ });
+ });
+
+ describe('limited component', () => {
+ beforeEach(() => {
+ createComponent({
+ filePath: 'test/file.js',
+ fileUrl: 'https://gitlab.com/test/file.js',
+ });
+ });
+
+ it(`renders withough projectPath`, () => {
+ expect(findClipboardButton().exists()).toBe(true);
+ expect(findFileIcon().exists()).toBe(true);
+ expect(findProjectPath().exists()).toBe(false);
+ expect(findProjectName().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/search/results/components/result_empty_spec.js b/spec/frontend/search/results/components/result_empty_spec.js
new file mode 100644
index 00000000000..067ba0447d4
--- /dev/null
+++ b/spec/frontend/search/results/components/result_empty_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import GlobalSearchResultsEmpty from '~/search/results/components/result_empty.vue';
+import { MOCK_QUERY } from '../../mock_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchResultsEmpty', () => {
+ let wrapper;
+
+ const getterSpies = {
+ currentScope: jest.fn(() => 'blobs'),
+ };
+
+ const createComponent = (
+ props,
+ initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' },
+ ) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ ...initialState,
+ },
+ getters: getterSpies,
+ });
+
+ wrapper = shallowMountExtended(GlobalSearchResultsEmpty, {
+ store,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('component basics', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it(`renders all parts of header`, () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/search/results/components/zoekt_blob_results_spec.js b/spec/frontend/search/results/components/zoekt_blob_results_spec.js
index de8144037ae..9ad36971c40 100644
--- a/spec/frontend/search/results/components/zoekt_blob_results_spec.js
+++ b/spec/frontend/search/results/components/zoekt_blob_results_spec.js
@@ -10,6 +10,7 @@ import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
+import EmptyResult from '~/search/results/components/result_empty.vue';
import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data';
jest.mock('~/alert');
@@ -26,6 +27,7 @@ describe('ZoektBlobResults', () => {
const blobSearchHandler = jest.fn().mockResolvedValue(mockGetBlobSearchQuery);
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
+ const mockQueryEmpty = jest.fn().mockReturnValue({});
const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error'));
const createComponent = ({
@@ -42,7 +44,7 @@ describe('ZoektBlobResults', () => {
},
getters: getterSpies,
});
-
+ // apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]);
wrapper = shallowMountExtended(ZoektBlobResults, {
apolloProvider,
store,
@@ -53,6 +55,7 @@ describe('ZoektBlobResults', () => {
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyResult = () => wrapper.findComponent(EmptyResult);
describe('when loading results', () => {
beforeEach(async () => {
@@ -81,6 +84,21 @@ describe('ZoektBlobResults', () => {
});
});
+ describe('when component has no results', () => {
+ beforeEach(async () => {
+ createComponent({
+ queryHandler: mockQueryEmpty,
+ });
+ jest.advanceTimersByTime(500);
+ await waitForPromises();
+ });
+
+ it(`renders component properly`, async () => {
+ await nextTick();
+ expect(findEmptyResult().exists()).toBe(true);
+ });
+ });
+
describe('when component has load error', () => {
beforeEach(async () => {
createComponent({ queryHandler: mockQueryError });
diff --git a/spec/frontend/work_items/components/work_item_abuse_modal_spec.js b/spec/frontend/work_items/components/work_item_abuse_modal_spec.js
new file mode 100644
index 00000000000..59d36a279fa
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_abuse_modal_spec.js
@@ -0,0 +1,101 @@
+import { GlModal, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
+import { CATEGORY_OPTIONS } from '~/abuse_reports/components/constants';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+describe('WorkItemAbuseModal', () => {
+ let wrapper;
+
+ const ACTION_PATH = '/abuse_reports/add_category';
+ const USER_ID = 1;
+ const REPORTED_FROM_URL = 'http://example.com';
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(WorkItemAbuseModal, {
+ propsData: {
+ reportedUserId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
+ ...props,
+ },
+ provide: {
+ reportAbusePath: ACTION_PATH,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent({ showModal: true });
+ });
+
+ const findAbuseModal = () => wrapper.findComponent(GlModal);
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+
+ const findCSRFToken = () => findForm().find('input[name="authenticity_token"]');
+ const findUserId = () => wrapper.findByTestId('input-user-id');
+ const findReferer = () => wrapper.findByTestId('input-referer');
+
+ describe('Modal', () => {
+ it('renders report abuse modal with the form', () => {
+ expect(findAbuseModal().exists()).toBe(true);
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('should set the modal title when the `title` prop is set', () => {
+ const title = 'Report abuse to administrator';
+ createComponent({ title, showModal: true });
+
+ expect(findAbuseModal().props().title).toBe(title);
+ });
+
+ it('should set modal size to `sm` by default', () => {
+ expect(findAbuseModal().props('size')).toBe('sm');
+ });
+
+ it('renders radio form group with the first option selected by default', () => {
+ const firstOption = CATEGORY_OPTIONS[0].value;
+ expect(findRadioGroup().attributes('checked')).toBe(firstOption);
+ });
+ });
+
+ describe('Select category form', () => {
+ it('renders POST form with path', () => {
+ expect(findForm().attributes()).toMatchObject({
+ method: 'post',
+ action: ACTION_PATH,
+ });
+ });
+
+ it('renders csrf token', () => {
+ expect(findCSRFToken().attributes('value')).toBe('mock-csrf-token');
+ });
+
+ it('renders label', () => {
+ expect(findFormGroup().attributes('label')).toBe('Why are you reporting this user?');
+ });
+
+ it('renders radio group', () => {
+ expect(findRadioGroup().props('options')).toEqual(CATEGORY_OPTIONS);
+ expect(findRadioGroup().attributes('name')).toBe('abuse_report[category]');
+ });
+
+ it('renders userId as a hidden fields', () => {
+ expect(findUserId().attributes()).toMatchObject({
+ type: 'hidden',
+ name: 'user_id',
+ value: USER_ID.toString(),
+ });
+ });
+
+ it('renders referer as a hidden fields', () => {
+ expect(findReferer().attributes()).toMatchObject({
+ type: 'hidden',
+ name: 'abuse_report[reported_from_url]',
+ value: REPORTED_FROM_URL,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index bda391a71fa..bb276b0f6da 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -5,7 +5,7 @@ import {
GlToggle,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
@@ -19,6 +19,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { isLoggedIn } from '~/lib/utils/common_utils';
import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
STATE_OPEN,
@@ -30,6 +31,7 @@ import {
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_PROMOTE_ACTION,
TEST_ID_TOGGLE_ACTION,
+ TEST_ID_REPORT_ABUSE,
} from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
@@ -64,6 +66,8 @@ describe('WorkItemActions component', () => {
const findWorkItemToggleOption = () => wrapper.findComponent(WorkItemStateToggle);
const findCopyCreateNoteEmailButton = () =>
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
+ const findReportAbuseButton = () => wrapper.findByTestId(TEST_ID_REPORT_ABUSE);
+ const findReportAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
const findMoreDropdown = () => wrapper.findByTestId('work-item-actions-dropdown');
const findMoreDropdownTooltip = () => getBinding(findMoreDropdown().element, 'gl-tooltip');
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
@@ -217,6 +221,10 @@ describe('WorkItemActions component', () => {
{
divider: true,
},
+ {
+ testId: TEST_ID_REPORT_ABUSE,
+ text: 'Report abuse',
+ },
{
testId: TEST_ID_DELETE_ACTION,
text: 'Delete task',
@@ -502,4 +510,22 @@ describe('WorkItemActions component', () => {
expect(findMoreDropdownTooltip().value).toBe('More actions');
});
});
+
+ describe('report abuse action', () => {
+ it('renders the report abuse button', () => {
+ createComponent();
+
+ expect(findReportAbuseButton().exists()).toBe(true);
+ expect(findReportAbuseModal().exists()).toBe(false);
+ });
+
+ it('opens the report abuse modal', async () => {
+ createComponent();
+
+ findReportAbuseButton().vm.$emit('action');
+ await nextTick();
+
+ expect(wrapper.emitted('toggleReportAbuseModal')).toEqual([[true]]);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 97f1e8753dc..285c03899b0 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -20,7 +20,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue';
import { i18n } from '~/work_items/constants';
@@ -83,7 +83,7 @@ describe('WorkItemDetail component', () => {
const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships);
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
@@ -696,7 +696,7 @@ describe('WorkItemDetail component', () => {
});
it('should not be visible by default', () => {
- expect(findAbuseCategorySelector().exists()).toBe(false);
+ expect(findWorkItemAbuseModal().exists()).toBe(false);
});
it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
@@ -704,13 +704,25 @@ describe('WorkItemDetail component', () => {
await nextTick();
- expect(findAbuseCategorySelector().exists()).toBe(true);
+ expect(findWorkItemAbuseModal().exists()).toBe(true);
- findAbuseCategorySelector().vm.$emit('close-drawer');
+ findWorkItemAbuseModal().vm.$emit('close-modal');
await nextTick();
- expect(findAbuseCategorySelector().exists()).toBe(false);
+ expect(findWorkItemAbuseModal().exists()).toBe(false);
+ });
+
+ it('should be visible when the work item actions button emits `toggleReportAbuseModal` event', async () => {
+ findWorkItemActions().vm.$emit('toggleReportAbuseModal', true);
+ await nextTick();
+
+ expect(findWorkItemAbuseModal().exists()).toBe(true);
+
+ findWorkItemAbuseModal().vm.$emit('close-modal');
+ await nextTick();
+
+ expect(findWorkItemAbuseModal().exists()).toBe(false);
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 0fc21a9d095..128ba9218bc 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -16,7 +16,7 @@ import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
import { FORM_TYPES } from '~/work_items/constants';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
@@ -94,7 +94,7 @@ describe('WorkItemLinks', () => {
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findAbuseCategoryModal = () => wrapper.findComponent(WorkItemAbuseModal);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
@@ -237,7 +237,7 @@ describe('WorkItemLinks', () => {
});
it('should not be visible by default', () => {
- expect(findAbuseCategorySelector().exists()).toBe(false);
+ expect(findAbuseCategoryModal().exists()).toBe(false);
});
it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
@@ -245,13 +245,13 @@ describe('WorkItemLinks', () => {
await nextTick();
- expect(findAbuseCategorySelector().exists()).toBe(true);
+ expect(findAbuseCategoryModal().exists()).toBe(true);
- findAbuseCategorySelector().vm.$emit('close-drawer');
+ findAbuseCategoryModal().vm.$emit('close-modal');
await nextTick();
- expect(findAbuseCategorySelector().exists()).toBe(false);
+ expect(findAbuseCategoryModal().exists()).toBe(false);
});
});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a9b5b4fcead..23295cdf69e 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -270,6 +270,9 @@ merge_requests:
- scan_result_policy_violations
- applicable_post_merge_approval_rules
- requested_changes
+- scan_result_policy_reads_through_violations
+- scan_result_policy_reads_through_approval_rules
+- running_scan_result_policy_violations
external_pull_requests:
- project
merge_request_diff:
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f97a2c20b1e..75f48ddf5e0 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -3946,19 +3946,18 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'when auto_merge_requested is true' do
let(:options) { { auto_merge_requested: true, auto_merge_strategy: auto_merge_strategy } }
- where(:auto_merge_strategy, :skip_approved_check, :skip_draft_check, :skip_blocked_check,
- :skip_discussions_check, :skip_external_status_check, :skip_requested_changes_check, :skip_jira_check, :skip_locked_lfs_files_check) do
- '' | false | false | false | false | false | false | false | false
- AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false | false | false | false | false | false | false | false
- AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true | true | true | true | true | true | true | true
+ where(:auto_merge_strategy, :skip_checks) do
+ '' | false
+ AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false
+ AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true
end
with_them do
it do
- is_expected.to include(skip_approved_check: skip_approved_check, skip_draft_check: skip_draft_check,
- skip_blocked_check: skip_blocked_check, skip_discussions_check: skip_discussions_check,
- skip_external_status_check: skip_external_status_check, skip_requested_changes_check: skip_requested_changes_check,
- skip_jira_check: skip_jira_check)
+ is_expected.to include(skip_approved_check: skip_checks, skip_draft_check: skip_checks,
+ skip_blocked_check: skip_checks, skip_discussions_check: skip_checks,
+ skip_external_status_check: skip_checks, skip_requested_changes_check: skip_checks,
+ skip_jira_check: skip_checks, skip_security_policy_check: skip_checks)
end
end
end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 29e2f7c7693..244b6320dfd 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
# We cannot disable SQL query limiting here, since the transaction does not
# begin until we enter the controller.
headers = {
- 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => '205,https://gitlab.com/gitlab-org/gitlab/-/issues/469250'
+ 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => '206,https://gitlab.com/gitlab-org/gitlab/-/issues/469250'
}
post_graphql(query, current_user: current_user, headers: headers)
diff --git a/spec/tooling/danger/settings_sections_spec.rb b/spec/tooling/danger/settings_sections_spec.rb
new file mode 100644
index 00000000000..1fbea0e1f3a
--- /dev/null
+++ b/spec/tooling/danger/settings_sections_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'gitlab/dangerfiles/spec_helper'
+require 'fast_spec_helper'
+
+require_relative '../../../tooling/danger/settings_sections'
+
+RSpec.describe Tooling::Danger::SettingsSections, feature_category: :tooling do
+ include_context 'with dangerfile'
+
+ subject(:settings_section_check) { fake_danger.new(helper: fake_helper) }
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:matching_changed_files) { ['app/views/foo/bar.html.haml', 'app/assets/js/foo/bar.vue'] }
+ let(:changed_lines) { ['-render SettingsBlockComponent.new(id: "foo") do', ''] }
+ let(:stable_branch?) { false }
+
+ before do
+ allow(fake_helper).to receive(:changed_files).and_return(matching_changed_files)
+ allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
+ allow(fake_helper).to receive(:stable_branch?).and_return(stable_branch?)
+ end
+
+ context 'when on stable branch' do
+ let(:stable_branch?) { true }
+
+ it 'does not write any markdown' do
+ expect(settings_section_check).not_to receive(:markdown)
+ settings_section_check.check!
+ end
+ end
+
+ context 'when none of the changed files are Haml or Vue files' do
+ let(:matching_changed_files) { [] }
+
+ it 'does not write any markdown' do
+ expect(settings_section_check).not_to receive(:markdown)
+ settings_section_check.check!
+ end
+ end
+
+ context 'when none of the changed lines match the pattern' do
+ let(:changed_lines) { ['-foo', '+bar'] }
+
+ it 'does not write any markdown' do
+ expect(settings_section_check).not_to receive(:markdown)
+ settings_section_check.check!
+ end
+ end
+
+ it 'adds a new markdown section listing every matching line' do
+ expect(settings_section_check).to receive(:markdown).with(/Searchable setting sections/)
+ expect(settings_section_check).to receive(:markdown).with(/SettingsBlock/)
+ expect(settings_section_check).to receive(:markdown).with(/settings-block/)
+ settings_section_check.check!
+ end
+end
diff --git a/tooling/danger/settings_sections.rb b/tooling/danger/settings_sections.rb
new file mode 100644
index 00000000000..b05fb721e94
--- /dev/null
+++ b/tooling/danger/settings_sections.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Tooling
+ module Danger
+ module SettingsSections
+ def check!
+ return if helper.stable_branch?
+
+ changed_code_files = helper.changed_files(/\.(haml|vue)$/)
+ return if changed_code_files.empty?
+
+ vc_regexp = /(SettingsBlockComponent|settings-block)/
+ lines_with_matches = filter_changed_lines(changed_code_files, vc_regexp)
+ return if lines_with_matches.empty?
+
+ markdown(<<~MARKDOWN)
+ ## Searchable setting sections
+
+ Looks like you have edited the template of some settings section. Please check that all changed sections are still searchable:
+
+ - If you created a new section, make sure to add it to either `lib/search/project_settings.rb` or `lib/search/group_settings.rb`, or in their counterparts in `ee/` if this section is only available behind a licensed feature.
+ - If you removed a section, make sure to also remove it from the files above.
+ - If you changed a section's id, please update it also in the files above.
+ - If you just moved code around within the same page, there is nothing to do.
+
+ MARKDOWN
+
+ lines_with_matches.each do |file, lines|
+ markdown(<<~MARKDOWN)
+ #### `#{file}`
+
+ ```shell
+ #{lines.join("\n")}
+ ```
+
+ MARKDOWN
+ end
+ end
+
+ def filter_changed_lines(files, pattern)
+ files_with_lines = {}
+ files.each do |file|
+ next if file.start_with?('spec/', 'ee/spec/', 'qa/')
+
+ matching_changed_lines = helper.changed_lines(file).select { |line| line =~ pattern }
+ next unless matching_changed_lines.any?
+
+ files_with_lines[file] = matching_changed_lines
+ end
+
+ files_with_lines
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 48c7c0b3f6d..a220c87885d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1359,10 +1359,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.109.0.tgz#af953d8114768343034f1f02bc8e2d93eb613c65"
integrity sha512-MmBTsco2LIh/l16iJQy6R98YDOlE3C++AE0Z1+KCpAX/3+fLAmULx2sWp+JnmM0ws8J0LaeLN6+vWiPaEWA16Q==
-"@gitlab/ui@87.8.0":
- version "87.8.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-87.8.0.tgz#94b9e301330b22d466fffddaa4f9838385b68ae0"
- integrity sha512-oGWyFmI87IbTYb6uYGt79MwV/hkl/vVKqLtMCgx2JLnzYSXWxYAdCKPhmQiO8Fib5RpfYwLzsxZ5qfaazTq4ig==
+"@gitlab/ui@88.0.0":
+ version "88.0.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-88.0.0.tgz#aab7b5ef169d02d65e80901680052c1f755eeec3"
+ integrity sha512-O9K5UalOBLboPnskMPezt2nttKL/YXiSlADcDZ/MKTfw9usxRVMm+COuS+zrmqXfOEZfmNMd2lbQnXW9uJlyRQ==
dependencies:
"@floating-ui/dom" "1.4.3"
echarts "^5.3.2"