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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-04 15:17:18 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-04 15:17:18 +0300
commitfb5d3cceb8d43f8c2dc22a5d8c74327e9397f2e8 (patch)
treedbd3a17217fa46cf279ed692b605e03222fca360
parent6cd4578a23ffe0fb94632f83a07a25d01f8d6821 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/lint/ambiguous_operator_precedence.yml2
-rw-r--r--.rubocop_todo/lint/triple_quotes.yml7
-rw-r--r--.rubocop_todo/rails/pluck.yml1
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue38
-rw-r--r--app/assets/javascripts/group_settings/constants.js11
-rw-r--r--app/assets/javascripts/group_settings/mount_shared_runners.js5
-rw-r--r--app/assets/javascripts/issues/constants.js5
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue165
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue82
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js2
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue7
-rw-r--r--app/assets/javascripts/right_sidebar.js6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue141
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue96
-rw-r--r--app/assets/stylesheets/components/detail_page.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss10
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/graphql_controller.rb7
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/finders/packages/conan/package_finder.rb28
-rw-r--r--app/helpers/ci/runners_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/models/concerns/protected_branch_access.rb6
-rw-r--r--app/models/concerns/protected_ref_access.rb20
-rw-r--r--app/models/packages/package.rb1
-rw-r--r--app/models/protected_tag/create_access_level.rb28
-rw-r--r--app/services/packages/conan/search_service.rb37
-rw-r--r--app/services/packages/conan/single_package_search_service.rb50
-rw-r--r--app/views/groups/runners/register.html.haml2
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml31
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--config/feature_flags/development/filter_vulnerability_findings_dismissed_on_default.yml2
-rw-r--r--config/feature_flags/development/use_pipeline_over_multikey.yml (renamed from config/feature_flags/development/use_merge_base_for_security_widget.yml)10
-rw-r--r--data/deprecations/16-0-graphql-CiRunnerUpgradeStatusType-renamed.yml24
-rw-r--r--doc/administration/geo/setup/database.md2
-rw-r--r--doc/administration/reference_architectures/10k_users.md2
-rw-r--r--doc/administration/reference_architectures/25k_users.md2
-rw-r--r--doc/administration/reference_architectures/3k_users.md2
-rw-r--r--doc/administration/reference_architectures/50k_users.md2
-rw-r--r--doc/administration/reference_architectures/5k_users.md2
-rw-r--r--doc/administration/reference_architectures/index.md2
-rw-r--r--doc/api/packages/conan.md2
-rw-r--r--doc/ci/caching/index.md1
-rw-r--r--doc/integration/kerberos.md10
-rw-r--r--doc/operations/incident_management/manage_incidents.md20
-rw-r--r--doc/subscriptions/gitlab_com/index.md2
-rw-r--r--doc/update/deprecations.md14
-rw-r--r--doc/user/compliance/compliance_report/index.md1
-rw-r--r--doc/user/group/epics/img/button_close_epic.pngbin13850 -> 0 bytes
-rw-r--r--doc/user/group/epics/manage_epics.md11
-rw-r--r--doc/user/packages/conan_repository/index.md13
-rw-r--r--doc/user/profile/notifications.md2
-rw-r--r--doc/user/project/issues/create_issues.md2
-rw-r--r--doc/user/project/issues/managing_issues.md12
-rw-r--r--doc/user/project/merge_requests/index.md5
-rw-r--r--doc/user/report_abuse.md4
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/conan_instance_packages.rb6
-rw-r--r--lib/api/conan_project_packages.rb6
-rw-r--r--lib/api/concerns/packages/conan_endpoints.rb3
-rw-r--r--lib/gitlab/avatar_cache.rb8
-rw-r--r--lib/gitlab/discussions_diff/highlight_cache.rb16
-rw-r--r--lib/gitlab/git.rb15
-rw-r--r--lib/gitlab/git/wraps_gitaly_errors.rb40
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb16
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb4
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb6
-rw-r--r--lib/gitlab/set_cache.rb8
-rw-r--r--lib/gitlab/utils/email.rb88
-rw-r--r--locale/gitlab.pot68
-rw-r--r--spec/controllers/application_controller_spec.rb24
-rw-r--r--spec/controllers/graphql_controller_spec.rb16
-rw-r--r--spec/features/abuse_report_spec.rb1
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb4
-rw-r--r--spec/features/incidents/incident_details_spec.rb2
-rw-r--r--spec/features/issues/discussion_lock_spec.rb1
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb3
-rw-r--r--spec/features/issues/issue_detail_spec.rb5
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb4
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb3
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb4
-rw-r--r--spec/features/merge_request/user_manages_subscription_spec.rb4
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb3
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/profiles/user_uses_comment_template_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb2
-rw-r--r--spec/features/reportable_note/issue_spec.rb4
-rw-r--r--spec/finders/packages/conan/package_finder_spec.rb25
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js66
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js317
-rw-r--r--spec/frontend/issues/show/components/new_header_actions_popover_spec.js77
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js2
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js14
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js4
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb18
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/helpers/sessions_helper_spec.rb28
-rw-r--r--spec/lib/gitlab/avatar_cache_spec.rb54
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb4
-rw-r--r--spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb88
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb8
-rw-r--r--spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb81
-rw-r--r--spec/lib/gitlab/reactive_cache_set_cache_spec.rb28
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb64
-rw-r--r--spec/lib/gitlab/utils/email_spec.rb32
-rw-r--r--spec/models/packages/package_spec.rb8
-rw-r--r--spec/requests/api/api_spec.rb22
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb23
-rw-r--r--spec/services/packages/conan/search_service_spec.rb28
-rw-r--r--spec/services/packages/conan/single_package_search_service_spec.rb45
123 files changed, 1952 insertions, 530 deletions
diff --git a/.rubocop_todo/lint/ambiguous_operator_precedence.yml b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
index 6b4ba8ff8d7..a890c6af6d0 100644
--- a/.rubocop_todo/lint/ambiguous_operator_precedence.yml
+++ b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
@@ -38,7 +38,6 @@ Lint/AmbiguousOperatorPrecedence:
- 'ee/app/models/geo/upload_registry.rb'
- 'ee/app/models/iterations/cadence.rb'
- 'ee/app/models/license.rb'
- - 'ee/app/models/namespaces/storage/root_size.rb'
- 'ee/app/policies/ee/issuable_policy.rb'
- 'ee/app/services/boards/epics/position_create_service.rb'
- 'ee/app/services/geo/registry_consistency_service.rb'
@@ -81,7 +80,6 @@ Lint/AmbiguousOperatorPrecedence:
- 'lib/gitlab/template_parser/parser.rb'
- 'lib/gitlab/tree_summary.rb'
- 'lib/gitlab/usage/metrics/name_suggestion.rb'
- - 'lib/gitlab/utils/email.rb'
- 'lib/omni_auth/strategies/bitbucket.rb'
- 'qa/qa/ee/page/project/secure/pipeline_security.rb'
- 'qa/qa/resource/api_fabricator.rb'
diff --git a/.rubocop_todo/lint/triple_quotes.yml b/.rubocop_todo/lint/triple_quotes.yml
deleted file mode 100644
index 7695fa46b03..00000000000
--- a/.rubocop_todo/lint/triple_quotes.yml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-# Cop supports --autocorrect.
-Lint/TripleQuotes:
- Details: grace period
- Exclude:
- - 'spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
- - 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
diff --git a/.rubocop_todo/rails/pluck.yml b/.rubocop_todo/rails/pluck.yml
index 791e15c64f8..38a8fa7487a 100644
--- a/.rubocop_todo/rails/pluck.yml
+++ b/.rubocop_todo/rails/pluck.yml
@@ -73,7 +73,6 @@ Rails/Pluck:
- 'ee/spec/requests/api/groups_spec.rb'
- 'ee/spec/requests/api/iterations_spec.rb'
- 'ee/spec/requests/api/members_spec.rb'
- - 'ee/spec/requests/api/merge_request_approval_rules_spec.rb'
- 'ee/spec/requests/api/project_approval_rules_spec.rb'
- 'ee/spec/requests/api/project_approval_settings_spec.rb'
- 'ee/spec/requests/api/projects_spec.rb'
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index 8011090f1cb..a4ec48ffd2f 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -1,7 +1,16 @@
<script>
import { GlToggle, GlAlert } from '@gitlab/ui';
+import { sprintf } from '~/locale';
import { updateGroup } from '~/api/groups_api';
-import { I18N_UPDATE_ERROR_MESSAGE, I18N_REFRESH_MESSAGE } from '../constants';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import {
+ I18N_CONFIRM_MESSAGE,
+ I18N_CONFIRM_OK,
+ I18N_CONFIRM_CANCEL,
+ I18N_CONFIRM_TITLE,
+ I18N_UPDATE_ERROR_MESSAGE,
+ I18N_REFRESH_MESSAGE,
+} from '../constants';
export default {
components: {
@@ -10,6 +19,8 @@ export default {
},
inject: [
'groupId',
+ 'groupName',
+ 'groupIsEmpty',
'sharedRunnersSetting',
'parentSharedRunnersSetting',
'runnerEnabledValue',
@@ -39,9 +50,28 @@ export default {
},
},
methods: {
- onSharedRunnersToggle(value) {
- const newSetting = value ? this.runnerEnabledValue : this.runnerDisabledValue;
- this.updateSetting(newSetting);
+ async onSharedRunnersToggle(enabled) {
+ if (enabled) {
+ this.updateSetting(this.runnerEnabledValue);
+ return;
+ }
+ if (this.groupIsEmpty) {
+ this.updateSetting(this.runnerDisabledValue);
+ return;
+ }
+
+ // Confirm when disabling for a group with subgroups or projects
+ const confirmDisabled = await confirmAction(I18N_CONFIRM_MESSAGE, {
+ title: sprintf(I18N_CONFIRM_TITLE, { groupName: this.groupName }),
+ cancelBtnText: I18N_CONFIRM_CANCEL,
+ primaryBtnText: I18N_CONFIRM_OK,
+ primaryBtnVariant: 'danger',
+ size: 'md',
+ });
+
+ if (confirmDisabled) {
+ this.updateSetting(this.runnerDisabledValue);
+ }
},
onOverrideToggle(value) {
const newSetting = value ? this.runnerAllowOverrideValue : this.runnerDisabledValue;
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index 1b44161903d..d4ac7d94bf4 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -1,4 +1,13 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+
+export const I18N_CONFIRM_MESSAGE = s__(
+ 'Runners|Shared runners will be disabled for all projects and subgroups in this group. If you proceed, you must manually re-enable shared runners in the settings of each project and subgroup.',
+);
+export const I18N_CONFIRM_OK = s__('Runners|Yes, disable shared runners');
+export const I18N_CONFIRM_CANCEL = s__('Runners|No, keep shared runners enabled');
+export const I18N_CONFIRM_TITLE = s__(
+ 'Runners|Are you sure you want to disable shared runners for %{groupName}?',
+);
export const I18N_UPDATE_ERROR_MESSAGE = __('An error occurred while updating configuration.');
export const I18N_REFRESH_MESSAGE = __('Refresh the page and try again.');
diff --git a/app/assets/javascripts/group_settings/mount_shared_runners.js b/app/assets/javascripts/group_settings/mount_shared_runners.js
index e7e104d61b3..0767330cd54 100644
--- a/app/assets/javascripts/group_settings/mount_shared_runners.js
+++ b/app/assets/javascripts/group_settings/mount_shared_runners.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import UpdateSharedRunnersForm from './components/shared_runners_form.vue';
export default (containerId = 'update-shared-runners-form') => {
@@ -6,6 +7,8 @@ export default (containerId = 'update-shared-runners-form') => {
const {
groupId,
+ groupName,
+ groupIsEmpty,
sharedRunnersSetting,
parentSharedRunnersSetting,
runnerEnabledValue,
@@ -17,6 +20,8 @@ export default (containerId = 'update-shared-runners-form') => {
el: containerEl,
provide: {
groupId,
+ groupName,
+ groupIsEmpty: parseBoolean(groupIsEmpty),
sharedRunnersSetting,
parentSharedRunnersSetting,
runnerEnabledValue,
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 0c563a1e952..c79612ad5d0 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -26,3 +26,8 @@ export const issuableStatusText = {
[STATUS_MERGED]: __('Merged'),
[STATUS_LOCKED]: __('Open'),
};
+
+export const IssuableTypeText = {
+ [TYPE_ISSUE]: __('issue'),
+ [TYPE_MERGE_REQUEST]: __('merge request'),
+};
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 84def374d13..b929c4dbae0 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -2,23 +2,36 @@
import {
GlButton,
GlDropdown,
+ GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
-import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
+import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants';
+import {
+ ISSUE_STATE_EVENT_CLOSE,
+ ISSUE_STATE_EVENT_REOPEN,
+ NEW_ACTIONS_POPOVER_KEY,
+} from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
+import toast from '~/vue_shared/plugins/global_toast';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
+import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
+import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issuesEventHub from '../event_hub';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
@@ -44,21 +57,27 @@ export default {
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
reportAbuse: __('Report abuse to administrator'),
+ referenceFetchError: __('An error occurred while fetching reference'),
+ copyReferenceText: __('Copy reference'),
},
components: {
DeleteIssueModal,
GlButton,
GlDropdown,
+ GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
+ NewHeaderActionsPopover,
+ SidebarSubscriptionsWidget,
+ IssuableLockForm,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
- mixins: [trackingMixin],
+ mixins: [trackingMixin, glFeatureFlagMixin()],
inject: {
canCreateIssue: {
default: false,
@@ -105,15 +124,46 @@ export default {
reportedFromUrl: {
default: '',
},
+ issuableEmailAddress: {
+ default: '',
+ },
+ fullPath: {
+ default: '',
+ },
},
data() {
return {
isReportAbuseDrawerOpen: false,
};
},
+ apollo: {
+ issuableReference: {
+ query: issueReferenceQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.reference || '';
+ },
+ skip() {
+ return !this.isMrSidebarMoved;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.referenceFetchError });
+ Sentry.captureException(error);
+ },
+ },
+ },
computed: {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
+ ...mapGetters(['getNoteableData']),
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
isClosed() {
return this.openState === STATUS_CLOSED;
},
@@ -157,6 +207,17 @@ export default {
hasMobileDropdown() {
return this.hasDesktopDropdown || this.showToggleIssueStateButton;
},
+ copyMailAddressText() {
+ return sprintf(__('Copy %{issueType} email address'), {
+ issueType: IssuableTypeText[this.issueType],
+ });
+ },
+ isMrSidebarMoved() {
+ return this.glFeatures.movedMrSidebar;
+ },
+ showLockIssueOption() {
+ return this.isMrSidebarMoved && this.issueType === TYPE_ISSUE;
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -166,6 +227,7 @@ export default {
},
methods: {
...mapActions(['toggleStateButtonLoading']),
+ ...mapActions(['updateLockedAttribute']),
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
@@ -244,7 +306,19 @@ export default {
edit() {
issuesEventHub.$emit('open.form');
},
+ dismissPopover() {
+ if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) {
+ setCookie(NEW_ACTIONS_POPOVER_KEY, true);
+ }
+ },
+ copyReference() {
+ toast(__('Reference copied'));
+ },
+ copyEmailAddress() {
+ toast(__('Email address copied'));
+ },
},
+ TYPE_ISSUE,
};
</script>
@@ -259,6 +333,21 @@ export default {
data-testid="mobile-dropdown"
:loading="isToggleStateButtonLoading"
>
+ <template v-if="isMrSidebarMoved">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
+
+ <gl-dropdown-divider />
+ </template>
+
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
+
<gl-dropdown-item v-if="canUpdateIssue" @click="edit">
{{ $options.i18n.edit }}
</gl-dropdown-item>
@@ -275,9 +364,21 @@ export default {
<gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template v-if="isMrSidebarMoved">
+ <gl-dropdown-item
+ :data-clipboard-text="issuableReference"
+ data-testid="copy-reference"
+ @click="copyReference"
+ >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ >
+ <gl-dropdown-item
+ v-if="issuableEmailAddress"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @click="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-dropdown-item
+ >
+ </template>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -287,6 +388,7 @@ export default {
{{ __('Submit as spam') }}
</gl-dropdown-item>
<template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -295,6 +397,13 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
+ <gl-dropdown-item
+ v-if="!isIssueAuthor"
+ data-testid="report-abuse-item"
+ @click="toggleReportAbuseDrawer(true)"
+ >
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
</gl-dropdown>
<gl-button
@@ -322,6 +431,7 @@ export default {
<gl-dropdown
v-if="hasDesktopDropdown"
+ id="new-actions-header-dropdown"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex! gl-sm-ml-3"
icon="ellipsis_v"
@@ -334,7 +444,19 @@ export default {
data-testid="desktop-dropdown"
no-caret
right
+ @shown="dismissPopover"
>
+ <template v-if="isMrSidebarMoved">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
+
+ <gl-dropdown-divider />
+ </template>
+
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
@@ -346,9 +468,24 @@ export default {
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
+ <template v-if="isMrSidebarMoved">
+ <gl-dropdown-item
+ :data-clipboard-text="issuableReference"
+ data-testid="copy-reference"
+ @click="copyReference"
+ >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ >
+ <gl-dropdown-item
+ v-if="issuableEmailAddress"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @click="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-dropdown-item
+ >
+ </template>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -357,8 +494,8 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
-
<template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -368,8 +505,16 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
+ <gl-dropdown-item
+ v-if="!isIssueAuthor"
+ data-testid="report-abuse-item"
+ @click="toggleReportAbuseDrawer(true)"
+ >
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
</gl-dropdown>
+ <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" />
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
new file mode 100644
index 00000000000..8262b3ac0ff
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlPopover, GlButton } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
+import { IssuableTypeText } from '~/issues/constants';
+
+export default {
+ name: 'NewHeaderActionsPopover',
+ i18n: {
+ popoverText: s__(
+ 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.',
+ ),
+ confirmButtonText: s__('HeaderAction|Okay!'),
+ },
+ components: {
+ GlPopover,
+ GlButton,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ issueType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dismissKey: NEW_ACTIONS_POPOVER_KEY,
+ popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)),
+ };
+ },
+ computed: {
+ popoverText() {
+ return sprintf(this.$options.i18n.popoverText, {
+ issueType: IssuableTypeText[this.issueType],
+ });
+ },
+ showPopover() {
+ return !this.popoverDismissed && this.isMrSidebarMoved;
+ },
+ isMrSidebarMoved() {
+ return this.glFeatures.movedMrSidebar;
+ },
+ },
+ methods: {
+ dismissPopover() {
+ this.popoverDismissed = true;
+ setCookie(this.dismissKey, this.popoverDismissed);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-popover
+ v-if="showPopover"
+ target="new-actions-header-dropdown"
+ container="viewport"
+ placement="left"
+ :show="showPopover"
+ triggers="manual"
+ content="text"
+ :css-classes="['gl-p-2 new-header-popover']"
+ >
+ <template #title>
+ <div class="gl-font-base gl-font-weight-normal">
+ {{ popoverText }}
+ </div>
+ </template>
+ <gl-button
+ data-testid="confirm-button"
+ variant="confirm"
+ type="submit"
+ @click="dismissPopover"
+ >{{ $options.i18n.confirmButtonText }}</gl-button
+ >
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 4d8c11f9669..6320e4ef266 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -17,3 +17,5 @@ export const issueState = {
issueType: undefined,
isDirty: false,
};
+
+export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed';
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index e677328cd2e..100abcbe1e5 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -174,6 +174,8 @@ export function initHeaderActions(store, type = '') {
reportedUserId: parseInt(el.dataset.reportedUserId, 10),
reportedFromUrl: el.dataset.reportedFromUrl,
submitAsSpamPath: el.dataset.submitAsSpamPath,
+ issuableEmailAddress: el.dataset.issuableEmailAddress,
+ fullPath: el.dataset.projectPath,
},
render: (createElement) => createElement(HeaderActions),
});
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
index 3bfbfea7f22..a6081303bf8 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
@@ -12,6 +12,7 @@ export function confirmAction(
modalHtmlMessage,
title,
hideCancel,
+ size,
} = {},
) {
return new Promise((resolve) => {
@@ -36,6 +37,7 @@ export function confirmAction(
title,
modalHtmlMessage,
hideCancel,
+ size,
},
on: {
confirmed() {
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
index ea91ccec546..24be1485379 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -56,6 +56,11 @@ export default {
required: false,
default: false,
},
+ size: {
+ type: String,
+ required: false,
+ default: 'sm',
+ },
},
computed: {
primaryAction() {
@@ -103,9 +108,9 @@ export default {
<template>
<gl-modal
ref="modal"
- size="sm"
modal-id="confirmationModal"
body-class="gl-display-flex"
+ :size="size"
:title="title"
:action-primary="primaryAction"
:action-cancel="cancelAction"
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 297b8ae1fc2..58e4553d00d 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -56,8 +56,10 @@ Sidebar.prototype.addEventListeners = function () {
const layoutPage = document.querySelector('.layout-page');
const rightSidebar = document.querySelector('.js-right-sidebar');
- updateSidebarClasses(layoutPage, rightSidebar);
- window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ if (rightSidebar.classList.contains('right-sidebar-merge-requests')) {
+ updateSidebarClasses(layoutPage, rightSidebar);
+ window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ }
}
};
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 1eff4db3970..06876546fa4 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,8 +1,9 @@
<script>
import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -45,10 +46,8 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
- isMergeRequest() {
- return (
- this.getNoteableData.targetType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar
- );
+ isMovedMrSidebar() {
+ return this.glFeatures.movedMrSidebar;
},
issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === TYPE_ISSUE;
@@ -60,7 +59,6 @@ export default {
lockStatus() {
return this.isLocked ? this.$options.locked : this.$options.unlocked;
},
-
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
@@ -89,8 +87,13 @@ export default {
fullPath: this.fullPath,
})
.then(() => {
- if (this.isMergeRequest) {
- toast(this.isLocked ? __('Merge request locked.') : __('Merge request unlocked.'));
+ if (this.isMovedMrSidebar) {
+ toast(
+ sprintf(__('%{issuableDisplayName} %{lockStatus}.'), {
+ issuableDisplayName: capitalizeFirstCharacter(this.issuableDisplayName),
+ lockStatus: this.isLocked ? __('locked') : __('unlocked'),
+ }),
+ );
}
})
.catch(() => {
@@ -113,14 +116,14 @@ export default {
</script>
<template>
- <li v-if="isMergeRequest" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" @click="toggleLocked">
+ <li v-if="isMovedMrSidebar" class="gl-dropdown-item">
+ <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
<span class="gl-dropdown-item-text-wrapper">
<template v-if="isLocked">
- {{ __('Unlock merge request') }}
+ {{ sprintf(__('Unlock %{issuableType}'), { issuableType: issuableDisplayName }) }}
</template>
<template v-else>
- {{ __('Lock merge request') }}
+ {{ sprintf(__('Lock %{issuableType}'), { issuableType: issuableDisplayName }) }}
</template>
</span>
</button>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 344fa880131..f2b960ed02c 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,12 +1,7 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import {
- TYPE_EPIC,
- TYPE_MERGE_REQUEST,
- WORKSPACE_GROUP,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -91,8 +86,8 @@ export default {
},
},
computed: {
- isMergeRequest() {
- return this.issuableType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar;
+ isMovedMrSidebar() {
+ return this.glFeatures.movedMrSidebar;
},
isLoading() {
return this.$apollo.queries?.subscribed?.loading || this.loading;
@@ -148,7 +143,7 @@ export default {
});
}
- if (this.isMergeRequest) {
+ if (this.isMovedMrSidebar) {
toast(subscribed ? __('Notifications turned on.') : __('Notifications turned off.'));
}
},
@@ -187,7 +182,7 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item">
+ <gl-dropdown-form v-if="isMovedMrSidebar" class="gl-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 540d57bb5ce..74843bcc006 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -17,6 +17,7 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -785,6 +786,21 @@ export function mountAssigneesDropdown() {
});
}
+function mountNewIssuePopover() {
+ const el = document.querySelector('.js-sidebar-header-popover');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'NewHeaderActionsPopover',
+ render: (createElement) =>
+ createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }),
+ });
+}
+
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
@@ -812,6 +828,7 @@ export function mountSidebar(mediator, store) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
+ mountNewIssuePopover();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index f2ec8f589ce..952ff9b18e9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -1,10 +1,21 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlButton,
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ GlDropdown,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
export default {
components: {
GlButton,
+ GlPopover,
+ GlSprintf,
+ GlLink,
GlDropdown,
GlDropdownItem,
},
@@ -82,30 +93,46 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-flex-start">
<template v-if="hasOneOption">
- <gl-button
- v-for="(btn, index) in tertiaryButtons"
- :id="btn.id"
- :key="index"
- v-gl-tooltip.hover
- :title="setTooltip(btn)"
- :href="btn.href"
- :target="btn.target"
- :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
- :data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
- :data-method="btn.dataMethod"
- :icon="btn.icon"
- :data-testid="btn.testId || 'extension-actions-button'"
- :variant="btn.variant || 'confirm'"
- :loading="btn.loading"
- :disabled="btn.loading"
- category="tertiary"
- size="small"
- class="gl-md-display-block gl-float-left"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-button>
+ <span v-for="(btn, index) in tertiaryButtons" :key="index">
+ <gl-button
+ :id="btn.id"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ {{ btn.text }}
+ </gl-button>
+ <gl-popover v-if="btn.popoverTarget" :target="btn.popoverTarget">
+ <template #title> {{ btn.popoverTitle }} </template>
+
+ <span v-if="btn.popoverLink">
+ <gl-sprintf :message="btn.popoverText">
+ <template #link="{ content }">
+ <gl-link class="gl-font-sm" :href="btn.popoverLink" target="_blank">
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-else>
+ {{ btn.popoverText }}
+ </span>
+ </gl-popover>
+ </span>
</template>
<template v-if="hasMultipleOptions">
<gl-dropdown
@@ -134,30 +161,46 @@ export default {
{{ btn.text }}
</gl-dropdown-item>
</gl-dropdown>
- <gl-button
- v-for="(btn, index) in tertiaryButtons"
- :id="btn.id"
- :key="index"
- v-gl-tooltip.hover
- :title="setTooltip(btn)"
- :href="btn.href"
- :target="btn.target"
- :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
- :data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
- :data-method="btn.dataMethod"
- :icon="btn.icon"
- :data-testid="btn.testId || 'extension-actions-button'"
- :variant="btn.variant || 'confirm'"
- :loading="btn.loading"
- :disabled="btn.loading"
- category="tertiary"
- size="small"
- class="gl-display-none gl-md-display-block gl-float-left"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-button>
+ <span v-for="(btn, index) in tertiaryButtons" :key="index">
+ <gl-button
+ :id="btn.id"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ {{ btn.text }}
+ </gl-button>
+ <gl-popover v-if="btn.popoverTarget" :target="btn.popoverTarget">
+ <template #title> {{ btn.popoverTitle }} </template>
+
+ <span v-if="btn.popoverLink">
+ <gl-sprintf :message="btn.popoverText">
+ <template #link="{ content }">
+ <gl-link class="gl-font-sm" :href="btn.popoverLink" target="_blank">
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-else>
+ {{ btn.popoverText }}
+ </span>
+ </gl-popover>
+ </span>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index 6b9823e0f3b..feee132629f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCollapsibleListbox, GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { updateText } from '~/lib/utils/text_markdown';
import savedRepliesQuery from './saved_replies.query.graphql';
@@ -16,11 +16,8 @@ export default {
},
components: {
GlCollapsibleListbox,
- GlIcon,
GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ GlTooltip,
},
props: {
newCommentTemplatePath: {
@@ -45,6 +42,9 @@ export default {
return savedReplies.map((r) => ({ value: r.id, text: r.name, content: r.content }));
},
},
+ mounted() {
+ this.tooltipTarget = this.$el.querySelector('.js-comment-template-toggle');
+ },
methods: {
fetchCommentTemplates() {
this.shouldFetchCommentTemplates = true;
@@ -75,53 +75,49 @@ export default {
</script>
<template>
- <gl-collapsible-listbox
- :header-text="__('Insert comment template')"
- :items="filteredSavedReplies"
- placement="right"
- searchable
- class="comment-template-dropdown"
- :searching="$apollo.queries.savedReplies.loading"
- @shown="fetchCommentTemplates"
- @search="setCommentTemplateSearch"
- @select="onSelect"
- >
- <template #toggle>
- <gl-button
- v-gl-tooltip
- :title="__('Insert comment template')"
- :aria-label="__('Insert comment template')"
- category="tertiary"
- class="gl-px-3!"
- data-testid="comment-template-dropdown-toggle"
- @keydown.prevent
- >
- <gl-icon name="comment-lines" class="gl-mr-0!" />
- <gl-icon name="chevron-down" />
- </gl-button>
- </template>
- <template #list-item="{ item }">
- <div class="gl-display-flex js-comment-template-content">
- <div class="gl-text-truncate">
- <strong>{{ item.text }}</strong
- ><span class="gl-ml-2">{{ item.content }}</span>
+ <span>
+ <gl-collapsible-listbox
+ :header-text="__('Insert comment template')"
+ :items="filteredSavedReplies"
+ :toggle-text="__('Insert comment template')"
+ text-sr-only
+ toggle-class="js-comment-template-toggle"
+ icon="comment-lines"
+ category="tertiary"
+ placement="right"
+ searchable
+ class="comment-template-dropdown"
+ :searching="$apollo.queries.savedReplies.loading"
+ @shown="fetchCommentTemplates"
+ @search="setCommentTemplateSearch"
+ @select="onSelect"
+ >
+ <template #list-item="{ item }">
+ <div class="gl-display-flex js-comment-template-content">
+ <div class="gl-text-truncate">
+ <strong>{{ item.text }}</strong
+ ><span class="gl-ml-2">{{ item.content }}</span>
+ </div>
</div>
- </div>
- </template>
- <template #footer>
- <div
- class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3"
- >
- <gl-button
- :href="newCommentTemplatePath"
- category="tertiary"
- block
- class="gl-justify-content-start! gl-mt-0! gl-mb-0! gl-px-3!"
- >{{ __('Add a new comment template') }}</gl-button
+ </template>
+ <template #footer>
+ <div
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3"
>
- </div>
- </template>
- </gl-collapsible-listbox>
+ <gl-button
+ :href="newCommentTemplatePath"
+ category="tertiary"
+ block
+ class="gl-justify-content-start! gl-mt-0! gl-mb-0! gl-px-3!"
+ >{{ __('Add a new comment template') }}</gl-button
+ >
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+ <gl-tooltip :target="() => tooltipTarget">
+ {{ __('Insert comment template') }}
+ </gl-tooltip>
+ </span>
</template>
<style>
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index de8142924f9..74f61faa9ae 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -74,3 +74,7 @@
color: $gl-text-color;
}
}
+
+.new-header-popover {
+ z-index: 999;
+}
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index e0fb95a1359..1b98fd4df07 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -165,3 +165,13 @@
border: 0;
}
}
+
+.merge-request-notification-toggle {
+ .gl-toggle {
+ @include gl-ml-auto;
+ }
+
+ .gl-toggle-label {
+ @include gl-font-weight-normal;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index a394c59c508..711585ea713 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -110,6 +110,11 @@ class ApplicationController < ActionController::Base
render plain: e.message, status: :too_many_requests
end
+ rescue_from Gitlab::Git::ResourceExhaustedError do |e|
+ response.headers.merge!(e.headers)
+ render plain: e.message, status: :too_many_requests
+ end
+
content_security_policy do |p|
next if p.directives.blank?
next unless Gitlab::CurrentSettings.snowplow_enabled? && !Gitlab::CurrentSettings.snowplow_collector_hostname.blank?
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index bf59a0a2400..8630519e028 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -76,6 +76,13 @@ class GraphqlController < ApplicationController
render_error(exception.message, status: :forbidden)
end
+ rescue_from Gitlab::Git::ResourceExhaustedError do |exception|
+ log_exception(exception)
+
+ response.headers.merge!(exception.headers)
+ render_error(exception.message, status: :too_many_requests)
+ end
+
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 3842a88d15b..7121096bd77 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -10,6 +10,7 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index a1cd4c49bf0..2cc2c957f21 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -67,6 +67,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
diff --git a/app/finders/packages/conan/package_finder.rb b/app/finders/packages/conan/package_finder.rb
index 210b37635b3..161a3d0d409 100644
--- a/app/finders/packages/conan/package_finder.rb
+++ b/app/finders/packages/conan/package_finder.rb
@@ -3,25 +3,43 @@
module Packages
module Conan
class PackageFinder
- attr_reader :current_user, :query
+ MAX_PACKAGES_COUNT = 500
- def initialize(current_user, params)
+ def initialize(current_user, params, project: nil)
@current_user = current_user
@query = params[:query]
+ @project = project
end
def execute
- packages_for_current_user.installable.with_name_like(query).order_name_asc if query
+ return ::Packages::Package.none unless query
+
+ packages
end
private
+ attr_reader :current_user, :query, :project
+
def packages
- Packages::Package.conan
+ base
+ .conan
+ .installable
+ .preload_conan_metadatum
+ .with_name_like(query)
+ .limit_recent(MAX_PACKAGES_COUNT)
+ end
+
+ def base
+ project ? packages_of_project : packages_for_current_user
+ end
+
+ def packages_of_project
+ project.packages
end
def packages_for_current_user
- packages.for_projects(projects_visible_to_current_user)
+ Packages::Package.for_projects(projects_visible_to_current_user)
end
def projects_visible_to_current_user
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 5012ac29816..14ca189f795 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -79,6 +79,8 @@ module Ci
def group_shared_runners_settings_data(group)
{
group_id: group.id,
+ group_name: group.name,
+ group_is_empty: (group.projects.empty? && group.children.empty?).to_s,
shared_runners_setting: group.shared_runners_setting,
parent_shared_runners_setting: group.parent&.shared_runners_setting,
runner_enabled_value: Namespace::SR_ENABLED,
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 72df2608de7..45b231ebdbe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -12,8 +12,8 @@ module IssuablesHelper
end
end
- def sidebar_gutter_collapsed_class
- return "right-sidebar-expanded" if moved_mr_sidebar_enabled?
+ def sidebar_gutter_collapsed_class(is_merge_request_with_flag)
+ return "right-sidebar-expanded" if is_merge_request_with_flag
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index db6ed91b085..2f002be632d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -153,7 +153,7 @@ module IssuesHelper
issue.moved_from.project.service_desk_enabled? && !issue.project.service_desk_enabled?
end
- def issue_header_actions_data(project, issuable, current_user)
+ def issue_header_actions_data(project, issuable, current_user, issuable_sidebar)
new_issuable_params = { issue: {}, add_related_issue: issuable.iid }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
@@ -177,7 +177,8 @@ module IssuesHelper
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issuable.author.id,
reported_from_url: issue_url(issuable),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable)
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable),
+ issuable_email_address: issuable_sidebar.nil? ? '' : issuable_sidebar[:create_note_email]
}
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 4bdf8f3fbd4..b394d24fa38 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -179,6 +179,10 @@ module MergeRequestsHelper
end
end
+ def moved_mr_sidebar_enabled?
+ Feature.enabled?(:moved_mr_sidebar, @project)
+ end
+
def diffs_tab_pane_data(project, merge_request, params)
{
"is-locked": merge_request.discussion_locked?,
@@ -272,10 +276,6 @@ module MergeRequestsHelper
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
- def moved_mr_sidebar_enabled?
- Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request)
- end
-
def single_file_file_by_file?
Feature.enabled?(:single_file_file_by_file, @project)
end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 58761fce952..8156090fd9c 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -9,10 +9,4 @@ module ProtectedBranchAccess
delegate :project, to: :protected_branch
end
-
- def check_access(user)
- return false if access_level == Gitlab::Access::NO_ACCESS
-
- super
- end
end
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 964a862d415..b841211c811 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -45,11 +45,23 @@ module ProtectedRefAccess
type == :role
end
- def check_access(user)
- return false unless user
+ def check_access(current_user)
+ return false if current_user.nil? || no_access?
- user.can?(:push_code, project) &&
- project.team.max_member_access(user.id) >= access_level
+ yield if block_given?
+
+ user_can_access?(current_user)
+ end
+
+ private
+
+ def no_access?
+ role? && access_level == Gitlab::Access::NO_ACCESS
+ end
+
+ def user_can_access?(current_user)
+ current_user.can?(:push_code, project) &&
+ project.team.max_member_access(current_user.id) >= access_level
end
end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 5ce44ab9388..a3946724fd3 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -156,6 +156,7 @@ class Packages::Package < ApplicationRecord
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) }
scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) }
+ scope :preload_conan_metadatum, -> { preload(:conan_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
index 785e7559212..5837f3a5afb 100644
--- a/app/models/protected_tag/create_access_level.rb
+++ b/app/models/protected_tag/create_access_level.rb
@@ -12,11 +12,9 @@ class ProtectedTag::CreateAccessLevel < ApplicationRecord
validate :validate_deploy_key_membership
def type
- if deploy_key.present?
- :deploy_key
- else
- super
- end
+ return :deploy_key if deploy_key.present?
+
+ super
end
def humanize
@@ -25,28 +23,28 @@ class ProtectedTag::CreateAccessLevel < ApplicationRecord
super
end
- def check_access(user)
- return false if access_level == Gitlab::Access::NO_ACCESS
-
- if user && deploy_key.present?
- return user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
+ def check_access(current_user)
+ super do
+ break enabled_deploy_key_for_user?(current_user) if deploy_key?
end
-
- super
end
private
+ def deploy_key?
+ type == :deploy_key
+ end
+
def validate_deploy_key_membership
return unless deploy_key
-
return if project.deploy_keys_projects.where(deploy_key: deploy_key).exists?
errors.add(:deploy_key, 'is not enabled for this project')
end
- def enabled_deploy_key_for_user?(deploy_key, user)
- deploy_key.user_id == user.id &&
+ def enabled_deploy_key_for_user?(current_user)
+ current_user.can?(:read_project, project) &&
+ deploy_key.user_id == current_user.id &&
DeployKey.with_write_access_for_project(protected_tag.project, deploy_key: deploy_key).any?
end
end
diff --git a/app/services/packages/conan/search_service.rb b/app/services/packages/conan/search_service.rb
index df22a895c00..c65c9a85da8 100644
--- a/app/services/packages/conan/search_service.rb
+++ b/app/services/packages/conan/search_service.rb
@@ -8,10 +8,6 @@ module Packages
WILDCARD = '*'
RECIPE_SEPARATOR = '@'
- def initialize(user, params)
- super(nil, user, params)
- end
-
def execute
ServiceResponse.success(payload: { results: search_results })
end
@@ -23,35 +19,34 @@ module Packages
return search_for_single_package(sanitized_query) if params[:query].include?(RECIPE_SEPARATOR)
- search_packages(build_query)
+ search_packages
end
def wildcard_query?
params[:query] == WILDCARD
end
- def build_query
- return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
-
- sanitized_query
- end
-
- def search_packages(query)
- ::Packages::Conan::PackageFinder.new(current_user, query: query).execute.map(&:conan_recipe)
+ def sanitized_query
+ @sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
end
def search_for_single_package(query)
- name, version, username, _ = query.split(%r{[@/]})
- full_path = Packages::Conan::Metadatum.full_path_from(package_username: username)
- project = Project.find_by_full_path(full_path)
- return unless Ability.allowed?(current_user, :read_package, project&.packages_policy_subject)
+ ::Packages::Conan::SinglePackageSearchService
+ .new(query, current_user)
+ .execute[:results]
+ end
- result = project.packages.with_name(name).with_version(version).order_created.last
- [result&.conan_recipe].compact
+ def search_packages
+ ::Packages::Conan::PackageFinder
+ .new(current_user, { query: build_query }, project: project)
+ .execute
+ .map(&:conan_recipe)
end
- def sanitized_query
- @sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
+ def build_query
+ return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
+
+ sanitized_query
end
end
end
diff --git a/app/services/packages/conan/single_package_search_service.rb b/app/services/packages/conan/single_package_search_service.rb
new file mode 100644
index 00000000000..e133b35c2cf
--- /dev/null
+++ b/app/services/packages/conan/single_package_search_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Packages
+ module Conan
+ class SinglePackageSearchService # rubocop:disable Search/NamespacedClass
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(query, current_user)
+ @name, @version, @username, _ = query.split(%r{[@/]})
+ @current_user = current_user
+ end
+
+ def execute
+ ServiceResponse.success(payload: { results: search_results })
+ end
+
+ private
+
+ attr_reader :name, :version, :username, :current_user
+
+ def search_results
+ return [] unless can_access_project_package?
+
+ [package&.conan_recipe].compact
+ end
+
+ def package
+ project
+ .packages
+ .with_name(name)
+ .with_version(version)
+ .order_created
+ .last
+ end
+
+ def project
+ Project.find_by_full_path(full_path)
+ end
+ strong_memoize_attr :project
+
+ def full_path
+ ::Packages::Conan::Metadatum.full_path_from(package_username: username)
+ end
+
+ def can_access_project_package?
+ Ability.allowed?(current_user, :read_package, project.try(:packages_policy_subject))
+ end
+ end
+ end
+end
diff --git a/app/views/groups/runners/register.html.haml b/app/views/groups/runners/register.html.haml
index a5296c38618..15d96bb80b6 100644
--- a/app/views/groups/runners/register.html.haml
+++ b/app/views/groups/runners/register.html.haml
@@ -2,6 +2,6 @@
- breadcrumb_title s_('Runners|Register')
- page_title s_('Runners|Register'), runner_name
- add_to_breadcrumbs _('Runners'), group_runners_path(@group)
-- add_to_breadcrumbs runner_name, register_group_runner_path(@runner)
+- add_to_breadcrumbs runner_name, register_group_runner_path(@group, @runner)
#js-group-register-runner{ data: { runner_id: @runner.id, runners_path: group_runners_path(@group) } }
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index b8ee62055f0..9bfa0e7a309 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,7 +1,8 @@
- display_issuable_type = issuable_display_type(@merge_request)
.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do
+ %span.js-sidebar-header-popover
+ = button_tag type: 'button', id: "new-actions-header-dropdown", class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do
= sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
%span.gl-dropdown-button-text= _('Merge request actions')
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index f54354674e2..82e95a6a8e8 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -9,14 +9,15 @@
- reviewers = local_assigns.fetch(:reviewers, nil)
- in_group_context_with_iterations = @project.group.present? && issuable_sidebar[:supports_iterations]
- is_merge_request = issuable_type === 'merge_request'
-- moved_sidebar_enabled = moved_mr_sidebar_enabled? && is_merge_request
+- moved_sidebar_enabled = moved_mr_sidebar_enabled?
+- is_merge_request_with_flag = is_merge_request && moved_sidebar_enabled
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class} #{'right-sidebar-merge-requests' if moved_sidebar_enabled}", 'aria-live' => 'polite', 'aria-label': issuable_type }
- .issuable-sidebar{ class: "#{'is-merge-request' if moved_sidebar_enabled}" }
- .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if moved_sidebar_enabled}" }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
+ .issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
+ .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
%button.btn.gl-button.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ type: "reset", class: "gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- - if signed_in && !moved_sidebar_enabled
+ - if signed_in && !is_merge_request_with_flag
.js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
@@ -81,17 +82,17 @@
.js-sidebar-participants-widget-root
- .block.with-sub-blocks
- - if !moved_sidebar_enabled
+ - if !moved_sidebar_enabled
+ .block.with-sub-blocks
.js-sidebar-reference-widget-root
- - if issuable_type == 'merge_request' && !moved_sidebar_enabled
- .sub-block.js-sidebar-source-branch
- .sidebar-collapsed-icon.js-dont-change-state
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
- %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
- = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ - if is_merge_request && !moved_sidebar_enabled
+ .sub-block.js-sidebar-source-branch
+ .sidebar-collapsed-icon.js-dont-change-state
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
+ %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
+ = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- if show_forwarding_email
.block
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index 9f7ed6b17c3..b6c0b73a83d 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -19,4 +19,4 @@
%a.btn.gl-button.btn-default.btn-icon.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
- .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user) }
+ .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar) }
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index cc1965945ac..5477b9395ea 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,7 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': _('Milestone') }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class(false), 'aria-live' => 'polite', 'aria-label': _('Milestone') }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => s_('MilestoneSidebar|Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
diff --git a/config/feature_flags/development/filter_vulnerability_findings_dismissed_on_default.yml b/config/feature_flags/development/filter_vulnerability_findings_dismissed_on_default.yml
index 598f7d499c4..93d79757511 100644
--- a/config/feature_flags/development/filter_vulnerability_findings_dismissed_on_default.yml
+++ b/config/feature_flags/development/filter_vulnerability_findings_dismissed_on_default.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/396747
milestone: '15.11'
type: development
group: group::threat insights
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/use_merge_base_for_security_widget.yml b/config/feature_flags/development/use_pipeline_over_multikey.yml
index 1257af54c78..35a84868fe4 100644
--- a/config/feature_flags/development/use_merge_base_for_security_widget.yml
+++ b/config/feature_flags/development/use_pipeline_over_multikey.yml
@@ -1,8 +1,8 @@
---
-name: use_merge_base_for_security_widget
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117594
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407762
+name: use_pipeline_over_multikey
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118884
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409436
milestone: '16.0'
type: development
-group: group::threat insights
-default_enabled: true
+group: group::scalability
+default_enabled: false
diff --git a/data/deprecations/16-0-graphql-CiRunnerUpgradeStatusType-renamed.yml b/data/deprecations/16-0-graphql-CiRunnerUpgradeStatusType-renamed.yml
new file mode 100644
index 00000000000..4f17f60fc85
--- /dev/null
+++ b/data/deprecations/16-0-graphql-CiRunnerUpgradeStatusType-renamed.yml
@@ -0,0 +1,24 @@
+- title: "CiRunnerUpgradeStatusType GraphQL type renamed to CiRunnerUpgradeStatus" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
+ removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
+ announcement_milestone: "16.0" # (required) The milestone when this feature was first announced as deprecated.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: pedropombeiro # (required) GitLab username of the person reporting the change
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409332 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The `CiRunnerUpgradeStatusType` GraphQL type has been renamed to `CiRunnerUpgradeStatus`. In GitLab 17.0,
+ the aliasing for the `CiRunnerUpgradeStatusType` type will be removed.
+#
+# OPTIONAL END OF SUPPORT FIELDS
+#
+# If an End of Support period applies, the announcement should be shared with GitLab Support
+# in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
+#
+ end_of_support_milestone: # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
+ #
+ # OTHER OPTIONAL FIELDS
+ #
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: https://docs.gitlab.com/ee/api/graphql/reference/index.html#cirunnerupgradestatus # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/doc/administration/geo/setup/database.md b/doc/administration/geo/setup/database.md
index cc5b28493a5..813ea492ef9 100644
--- a/doc/administration/geo/setup/database.md
+++ b/doc/administration/geo/setup/database.md
@@ -940,6 +940,8 @@ Omnibus automatically configures a tracking database when `roles(['geo_secondary
If you want to run this database in a highly available configuration, don't use the `geo_secondary_role` above.
Instead, follow the instructions below.
+If you want to run the Geo tracking database on a single node, see [Configure the Geo tracking database on the Geo secondary site](../replication/multiple_servers.md#step-3-configure-the-geo-tracking-database-on-the-geo-secondary-site).
+
A production-ready and secure setup for the tracking PostgreSQL DB requires at least three Consul nodes: two
Patroni nodes, and one PgBouncer node on the secondary site.
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index 663d4b206f7..ee5b99f6211 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -1199,7 +1199,7 @@ For more advanced setups refer to the [standalone Gitaly Cluster documentation](
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
-A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7292).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 2ff47eaa143..2bdd7ba0544 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -1218,7 +1218,7 @@ For more advanced setups refer to the [standalone Gitaly Cluster documentation](
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
-A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7292).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index b5c5717e56d..0fa0704954e 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -1148,7 +1148,7 @@ For more advanced setups refer to the [standalone Gitaly Cluster documentation](
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
-A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7292).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 90aab6873b6..a6f02336960 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -1212,7 +1212,7 @@ For more advanced setups refer to the [standalone Gitaly Cluster documentation](
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
-A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7292).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index 292cf537ba6..8b6fb13b9da 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -1144,7 +1144,7 @@ For more advanced setups refer to the [standalone Gitaly Cluster documentation](
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
-A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7292).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md
index a2b348fb642..4b9c26e8626 100644
--- a/doc/administration/reference_architectures/index.md
+++ b/doc/administration/reference_architectures/index.md
@@ -242,7 +242,7 @@ that to achieve full High Availability, a third-party PostgreSQL database soluti
We hope to offer a built-in solution for these restrictions in the future. In the meantime, a non-HA PostgreSQL server
can be set up using Omnibus GitLab as the specifications reflect. Refer to the following issues for more information:
-- [`omnibus-gitlab#5919`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+- [`omnibus-gitlab#7292`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7292).
- [`gitaly#3398`](https://gitlab.com/gitlab-org/gitaly/-/issues/3398).
## Recommended cloud providers and services
diff --git a/doc/api/packages/conan.md b/doc/api/packages/conan.md
index 0dc6f68ed4c..2766880ab70 100644
--- a/doc/api/packages/conan.md
+++ b/doc/api/packages/conan.md
@@ -123,7 +123,7 @@ GET <route-prefix>/users/authenticate
```
```shell
-curl --user <username>:<personal_access_token> "https://gitlab.example.com/packages/conan/v1/users/authenticate
+curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/packages/conan/v1/users/authenticate"
```
Example response:
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index b53de56a8e1..b87f34aa633 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -590,6 +590,7 @@ If you have a cache mismatch, follow these steps to troubleshoot.
| You use runners in autoscale mode without a distributed cache enabled. | Configure the autoscale runner to use a distributed cache. |
| The machine the runner is installed on is low on disk space or, if you've set up distributed cache, the S3 bucket where the cache is stored doesn't have enough space. | Make sure you clear some space to allow new caches to be stored. There's no automatic way to do this. |
| You use the same `key` for jobs where they cache different paths. | Use different cache keys so that the cache archive is stored to a different location and doesn't overwrite wrong caches. |
+| You have not enabled the [distributed runner caching on your runners](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching). | Set `Shared = false` and re-provision your runners. |
#### Cache mismatch example 1
diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md
index dc4dd501363..ae9e55f653f 100644
--- a/doc/integration/kerberos.md
+++ b/doc/integration/kerberos.md
@@ -356,6 +356,16 @@ as extensions to the Kerberos protocol may result in HTTP authentication headers
larger than the default size of 8 kB. Configure `large_client_header_buffers`
to a larger value in [the NGINX configuration](https://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers).
+### Use Keytabs created using AES-only encryption with Windows AD
+
+When you create a keytab with Advanced Encryption Standard (AES)-only encryption, you must select the **This account supports Kerberos AES <128/256> bit encryption** checkbox for that account in the AD server. Whether the checkbox is 128 or 256 bit depends on the encryption strength used when you created the keytab. To check this, on the Active Directory server:
+
+1. Open the **Users and Groups** tool.
+1. Locate the account that you used to create the keytab.
+1. Right-click the account and select **Properties**.
+1. In **Account Options** on the **Account** tab, select the appropriate AES encryption support checkbox.
+1. Save and close.
+
## Troubleshooting
### Using Google Chrome with Kerberos authentication against Windows AD
diff --git a/doc/operations/incident_management/manage_incidents.md b/doc/operations/incident_management/manage_incidents.md
index 338dacda166..9d0c8075ff9 100644
--- a/doc/operations/incident_management/manage_incidents.md
+++ b/doc/operations/incident_management/manage_incidents.md
@@ -226,6 +226,10 @@ When you close an incident that is linked to an [alert](alerts.md),
the linked alert's status changes to **Resolved**.
You are then credited with the alert's status change.
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action at the top of an incident, your project or instance might have
+enabled a feature flag for [moved actions](../../user/project/merge_requests/index.md#move-sidebar-actions)
+
### Automatically close incidents via recovery alerts
> [Introduced for HTTP integrations](https://gitlab.com/gitlab-org/gitlab/-/issues/13402) in GitLab 13.4.
@@ -249,6 +253,22 @@ When GitLab receives a recovery alert, it closes the associated incident.
This action is recorded as a system note on the incident indicating that it
was closed automatically by the GitLab Alert bot.
+## Delete an incident
+
+Prerequisites:
+
+- You must have the Owner role for a project.
+
+To delete an incident:
+
+1. In an incident, select **Incident actions** (**{ellipsis_v}**).
+1. Select **Delete incident**.
+
+Alternatively:
+
+1. In an incident, select **Edit title and description** (**{pencil}**).
+1. Select **Delete incident**.
+
## Other actions
Because incidents in GitLab are built on top of [issues](../../user/project/issues/index.md),
diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md
index 90ae556ce6b..84fbca7bfa8 100644
--- a/doc/subscriptions/gitlab_com/index.md
+++ b/doc/subscriptions/gitlab_com/index.md
@@ -164,7 +164,7 @@ Seats owed = 12 - 10 (Maximum users - users in subscription)
### Free Guest users **(ULTIMATE)**
In the **Ultimate** tier, users who are assigned the Guest role do not consume a seat.
-The user must not be assigned any other role, anywhere in the instance.
+The user must not be assigned any other role, anywhere in the instance or in the namespace for GitLab SaaS.
- If your project is private or internal, a user with the Guest role has
[a set of permissions](../../user/permissions.md#project-members-permissions).
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 09e0693509b..fc21f4692dc 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -121,6 +121,20 @@ If you rely on the order of the returned projects to be `id_asc`, change your sc
<div class="deprecation breaking-change" data-milestone="17.0">
+### CiRunnerUpgradeStatusType GraphQL type renamed to CiRunnerUpgradeStatus
+
+<div class="deprecation-notes">
+- Announced in: GitLab <span class="milestone">16.0</span>
+- [Breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/)
+</div>
+
+The `CiRunnerUpgradeStatusType` GraphQL type has been renamed to `CiRunnerUpgradeStatus`. In GitLab 17.0,
+the aliasing for the `CiRunnerUpgradeStatusType` type will be removed.
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### DAST ZAP advanced configuration variables deprecation
<div class="deprecation-notes">
diff --git a/doc/user/compliance/compliance_report/index.md b/doc/user/compliance/compliance_report/index.md
index 1962dfefdf5..fa342228b5b 100644
--- a/doc/user/compliance/compliance_report/index.md
+++ b/doc/user/compliance/compliance_report/index.md
@@ -18,6 +18,7 @@ See reports about compliance violations and compliance frameworks for the group.
> - GraphQL API [introduced](https://gitlab.com/groups/gitlab-org/-/epics/7222) in GitLab 14.9.
> - [Generally available](https://gitlab.com/groups/gitlab-org/-/epics/5237) in GitLab 14.10. [Feature flag `compliance_violations_report`](https://gitlab.com/gitlab-org/gitlab/-/issues/346266) removed.
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112111) to compliance violations report in GitLab 15.9.
+> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/394950) ability to create/edit compliance frameworks in GitLab 16.0 [with a flag](../../../administration/feature_flags.md) named `manage_compliance_frameworks_modals_refactor`. Disabled by default.
With compliance violations report, you can see a high-level view of merge request activity for all projects in the group.
diff --git a/doc/user/group/epics/img/button_close_epic.png b/doc/user/group/epics/img/button_close_epic.png
deleted file mode 100644
index aa1a889ea23..00000000000
--- a/doc/user/group/epics/img/button_close_epic.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 0dc87b7e4e4..1349d28c6a1 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -159,14 +159,13 @@ Prerequisites:
- You must have at least the Reporter role for the epic's group.
-Whenever you decide that there is no longer need for that epic,
-close the epic by:
+To close an epic, at the top of an epic, select **Close epic**.
-- Selecting **Close epic**.
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action at the top of an epic, your project or instance might have
+enabled a feature flag for [moved actions](../../project/merge_requests/index.md#move-sidebar-actions)
- ![close epic - button](img/button_close_epic.png)
-
-- Using the `/close` [quick action](../../project/quick_actions.md).
+You can also use the `/close` [quick action](../../project/quick_actions.md).
## Reopen a closed epic
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index 05daa525893..f06a7d83f92 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -315,8 +315,17 @@ To search by full or partial package name, or by exact recipe, run the
conan search He* --remote=gitlab
```
-The scope of your search includes all projects you have permission to access.
-This includes your private projects as well as all public projects.
+The scope of your search depends on your Conan remote configuration:
+
+- If you have a remote configured for your [instance](#add-a-remote-for-your-instance), your search includes
+all projects you have permission to access. This includes your private projects
+ as well as all public projects.
+
+- If you have a remote configured for a [project](#add-a-remote-for-your-project), your search includes all
+packages in the target project, as long as you have permission to access it.
+
+NOTE:
+The limit of the search results is 500 packages, and the results are sorted by the most recently published packages.
## Fetch Conan package information from the Package Registry
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index c0c967a3f18..89366c73b16 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -239,7 +239,7 @@ Turning this toggle off only unsubscribes you from updates related to this issue
Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action on the right sidebar, your project or instance may have
+If you don't see this action on the right sidebar, your project or instance might have
enabled a feature flag for [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions).
### Notification events on issues, merge requests, and epics
diff --git a/doc/user/project/issues/create_issues.md b/doc/user/project/issues/create_issues.md
index b6931149ede..4511c89b0ff 100644
--- a/doc/user/project/issues/create_issues.md
+++ b/doc/user/project/issues/create_issues.md
@@ -78,7 +78,7 @@ Prerequisites:
To create an issue from another issue:
-1. In an existing issue, select the vertical ellipsis (**{ellipsis_v}**).
+1. In an existing issue, select **Issue actions** (**{ellipsis_v}**).
1. Select **New related issue**.
1. Complete the [fields](#fields-in-the-new-issue-form).
The new issue form has a **Relate to issue #123** checkbox, where `123` is the ID of the
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 069bc4582c6..b532fd0c5b8 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -209,6 +209,10 @@ To close an issue, you can do the following:
- At the top of the issue, select **Close issue**.
- In an [issue board](../issue_board.md), drag an issue card from its list into the **Closed** list.
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action at the top of an issue, your project or instance might have
+enabled a feature flag for [moved actions](../merge_requests/index.md#move-sidebar-actions).
+
### Reopen a closed issue
Prerequisites:
@@ -344,7 +348,7 @@ Prerequisites:
To delete an issue:
-1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
+1. In an issue, select **Issue actions** (**{ellipsis_v}**).
1. Select **Delete issue**.
Alternatively:
@@ -362,7 +366,7 @@ You can promote an issue to an [epic](../../group/epics/index.md) in the immedia
To promote an issue to an epic:
-1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
+1. In an issue, select **Issue actions** (**{ellipsis_v}**).
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
@@ -472,6 +476,10 @@ You can now paste the reference into another description or comment.
Read more about issue references in [GitLab-Flavored Markdown](../../markdown.md#gitlab-specific-references).
+<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
+If you don't see this action on the right sidebar, your project or instance might have
+enabled a feature flag for [moved actions](../merge_requests/index.md#move-sidebar-actions).
+
## Copy issue email address
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18816) in GitLab 13.8.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index ddd3ea1cd05..acc64e513ce 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -267,7 +267,8 @@ after merging does not retarget open merge requests. This improvement is
<!-- When the `moved_mr_sidebar` feature flag is removed, delete this topic and update the steps for these actions
like in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87727/diffs?diff_id=522279685#5d9afba799c4af9920dab533571d7abb8b9e9163 -->
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/373757) to also move actions on issues, incidents, and epics in GitLab 16.0.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`.
@@ -282,6 +283,8 @@ When this feature flag is enabled, in the upper-right corner,
- [Lock discussion](../../discussions/index.md#prevent-comments-by-locking-the-discussion)
- Copy reference
+In GitLab 16.0 and later, similar action menus are available on issues, incidents, and epics.
+
When this feature flag is disabled, these actions are in the right sidebar.
## Merge request workflows
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index de2b82c28d3..c4b9af28220 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -50,7 +50,7 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from an issue
-1. On the issue, in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**).
+1. On the issue, in the upper-right corner, select **Issue actions** (**{ellipsis_v}**).
1. Select **Report abuse to administrator**.
1. Select a reason for reporting the user.
1. Complete an abuse report.
@@ -58,7 +58,7 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from a merge request
-1. On the merge request, in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**).
+1. On the merge request, in the upper-right corner, select **Merge request actions** (**{ellipsis_v}**).
1. Select **Report abuse to administrator**.
1. Select a reason for reporting this user.
1. Complete an abuse report.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d555c14504b..f50c705c3ea 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -134,6 +134,10 @@ module API
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
end
+ rescue_from Gitlab::Git::ResourceExhaustedError do |exception|
+ rack_response({ 'message' => exception.message }.to_json, 429, exception.headers)
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb
index 8c13b580092..5f302dba488 100644
--- a/lib/api/conan_instance_packages.rb
+++ b/lib/api/conan_instance_packages.rb
@@ -3,6 +3,12 @@
# Conan Instance-Level Package Manager Client API
module API
class ConanInstancePackages < ::API::Base
+ helpers do
+ def search_project
+ nil
+ end
+ end
+
namespace 'packages/conan/v1' do
include ::API::Concerns::Packages::ConanEndpoints
end
diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb
index e282443e85c..a63b3cfd619 100644
--- a/lib/api/conan_project_packages.rb
+++ b/lib/api/conan_project_packages.rb
@@ -3,6 +3,12 @@
# Conan Project-Level Package Manager Client API
module API
class ConanProjectPackages < ::API::Base
+ helpers do
+ def search_project
+ project
+ end
+ end
+
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
end
diff --git a/lib/api/concerns/packages/conan_endpoints.rb b/lib/api/concerns/packages/conan_endpoints.rb
index 82c97474b30..a7a98482bcc 100644
--- a/lib/api/concerns/packages/conan_endpoints.rb
+++ b/lib/api/concerns/packages/conan_endpoints.rb
@@ -82,7 +82,8 @@ module API
route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
get 'conans/search', urgency: :low do
- service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
+ service = ::Packages::Conan::SearchService.new(search_project, current_user, query: params[:q]).execute
+
service.payload
end
diff --git a/lib/gitlab/avatar_cache.rb b/lib/gitlab/avatar_cache.rb
index 30c8e089061..ed00a279299 100644
--- a/lib/gitlab/avatar_cache.rb
+++ b/lib/gitlab/avatar_cache.rb
@@ -65,7 +65,13 @@ module Gitlab
keys = emails.map { |email| email_key(email) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.unlink(*keys)
+ if ::Feature.enabled?(:use_pipeline_over_multikey)
+ redis.pipelined do |pipeline|
+ keys.each { |key| pipeline.unlink(key) }
+ end.sum
+ else
+ redis.unlink(*keys)
+ end
end
end
end
diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb
index 037a4c967e8..df93e6e91b4 100644
--- a/lib/gitlab/discussions_diff/highlight_cache.rb
+++ b/lib/gitlab/discussions_diff/highlight_cache.rb
@@ -41,7 +41,13 @@ module Gitlab
content =
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.mget(keys)
+ if ::Feature.enabled?(:use_pipeline_over_multikey)
+ redis.pipelined do |pipeline|
+ keys.each { |key| pipeline.get(key) }
+ end
+ else
+ redis.mget(keys)
+ end
end
end
@@ -66,7 +72,13 @@ module Gitlab
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(keys)
+ if ::Feature.enabled?(:use_pipeline_over_multikey)
+ redis.pipelined do |pipeline|
+ keys.each { |key| pipeline.del(key) }
+ end.sum
+ else
+ redis.del(keys)
+ end
end
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index eb204a7dd8e..ef5c242e68a 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -22,6 +22,21 @@ module Gitlab
InvalidRefFormatError = Class.new(BaseError)
ReferencesLockedError = Class.new(BaseError)
+ class ResourceExhaustedError < BaseError
+ def initialize(msg = nil, retry_after = 0)
+ super(msg)
+ @retry_after = retry_after
+ end
+
+ def headers
+ if @retry_after.to_i > 0
+ { "Retry-After" => @retry_after }
+ else
+ {}
+ end
+ end
+ end
+
class << self
include Gitlab::EncodingHelper
diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb
index 1d34f3c8eb2..20bcf3585e1 100644
--- a/lib/gitlab/git/wraps_gitaly_errors.rb
+++ b/lib/gitlab/git/wraps_gitaly_errors.rb
@@ -5,14 +5,40 @@ module Gitlab
module WrapsGitalyErrors
def wrapped_gitaly_errors(&block)
yield block
- rescue GRPC::NotFound => e
- raise Gitlab::Git::Repository::NoRepository, e
- rescue GRPC::InvalidArgument => e
- raise ArgumentError, e
- rescue GRPC::DeadlineExceeded => e
- raise Gitlab::Git::CommandTimedOut, e
rescue GRPC::BadStatus => e
- raise Gitlab::Git::CommandError, e
+ # The GRPC::BadStatus is the fundamental error that serves as the basis for all other gRPC error categories,
+ # including GRPC::InvalidArgument. It is essential to note that rescuing the specific exception class does not
+ # account for all possible cases. In this regard, a status exception can be directly generated from
+ # GRPC::BadStatus. Therefore, it is recommended that we capture and rescue the GRPC::BadStatus and assert the
+ # status code to ensure adequate coverage of error cases.
+ case e.code
+ when GRPC::Core::StatusCodes::NOT_FOUND
+ raise Gitlab::Git::Repository::NoRepository, e
+ when GRPC::Core::StatusCodes::INVALID_ARGUMENT
+ raise ArgumentError, e
+ when GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
+ raise Gitlab::Git::CommandTimedOut, e
+ when GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED
+ handle_resource_exhausted(e)
+ else
+ raise Gitlab::Git::CommandError, e
+ end
+ end
+
+ private
+
+ def handle_resource_exhausted(exception)
+ detail = Gitlab::GitalyClient.decode_detailed_error(exception)
+
+ case detail.class.name
+ when Gitaly::LimitError.name
+ retry_after = detail&.retry_after&.seconds
+ raise ResourceExhaustedError.new(
+ "Upstream Gitaly has been exhausted: #{detail.error_message}. Try again later", retry_after
+ )
+ else
+ raise ResourceExhaustedError, _("Upstream Gitaly has been exhausted. Try again later")
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 313334737c0..1af06cc7490 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -45,7 +45,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :access_check
access_check_error = detailed_error.access_check
# These messages were returned from internal/allowed API calls
@@ -82,7 +82,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :custom_hook
raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
fallback_message: e.details)
@@ -124,7 +124,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :custom_hook
raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
fallback_message: e.details)
@@ -188,7 +188,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :access_check
access_check_error = detailed_error.access_check
# These messages were returned from internal/allowed API calls
@@ -247,7 +247,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :access_check
access_check_error = detailed_error.access_check
# These messages were returned from internal/allowed API calls
@@ -329,7 +329,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :access_check
access_check_error = detailed_error.access_check
# These messages were returned from internal/allowed API calls
@@ -366,7 +366,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :resolve_revision, :rebase_conflict
# Theoretically, we could now raise specific errors based on the type
# of the detailed error. Most importantly, we get error details when
@@ -458,7 +458,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :access_check
access_check_error = detailed_error.access_check
# These messages were returned from internal/allowed API calls
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 525d7064dae..88c79eb8954 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -113,7 +113,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :tag_not_found
raise Gitlab::Git::UnknownRef, "tag does not exist: #{tag_name}"
else
@@ -135,7 +135,7 @@ module Gitlab
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
- case detailed_error&.error
+ case detailed_error.try(:error)
when :invalid_format
raise Gitlab::Git::InvalidRefFormatError, "references have an invalid format: #{detailed_error.invalid_format.refs.join(",")}"
when :references_locked
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index 2de3c07712f..110c5aa7a19 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -17,7 +17,11 @@ module Gitlab
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.pipelined do |pipeline|
- keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
+ if ::Feature.enabled?(:use_pipeline_over_multikey)
+ keys.each { |key| pipeline.unlink(key) }
+ else
+ keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
+ end
end
end
end
diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb
index 3d2ff5a68d2..623b254c4e0 100644
--- a/lib/gitlab/set_cache.rb
+++ b/lib/gitlab/set_cache.rb
@@ -22,7 +22,13 @@ module Gitlab
keys_to_expire = keys.map { |key| cache_key(key) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.unlink(*keys_to_expire)
+ if ::Feature.enabled?(:use_pipeline_over_multikey)
+ redis.pipelined do |pipeline|
+ keys_to_expire.each { |key| pipeline.unlink(key) }
+ end.sum
+ else
+ redis.unlink(*keys_to_expire)
+ end
end
end
end
diff --git a/lib/gitlab/utils/email.rb b/lib/gitlab/utils/email.rb
index c65d7165263..5eb57a66c63 100644
--- a/lib/gitlab/utils/email.rb
+++ b/lib/gitlab/utils/email.rb
@@ -8,19 +8,81 @@ module Gitlab
# Replaces most visible characters with * to obfuscate an email address
# deform adds a fix number of * to ensure the address cannot be guessed. Also obfuscates TLD with **
def obfuscated_email(email, deform: false)
- regex = ::Gitlab::UntrustedRegexp.new('^(..?)(.*)(@.?)(.*)(\..+)$')
- match = regex.match(email)
- return email unless match
-
- if deform
- # Ensure we can show two characters for the username, even if the username has
- # only one character. Boring solution is to just duplicate the character.
- email_start = match[1]
- email_start += email_start if email_start.length == 1
-
- email_start + '*' * 5 + match[3] + '*' * 5 + "#{match[5][0..1]}**"
- else
- match[1] + '*' * (match[2] || '').length + match[3] + '*' * (match[4] || '').length + match[5]
+ return email if email.empty?
+
+ masker_class = deform ? Deform : Symmetrical
+ masker_class.new(email).masked
+ end
+
+ class Masker
+ attr_reader :local_part, :sub_domain, :toplevel_domain, :at, :dot
+
+ def initialize(original)
+ @original = original
+ @local_part, @at, domain = original.rpartition('@')
+ @sub_domain, @dot, @toplevel_domain = domain.rpartition('.')
+
+ @at = nil if @at.empty?
+ @dot = nil if @dot.empty?
+ end
+
+ def masked
+ masked = [
+ local_part,
+ at,
+ sub_domain,
+ dot,
+ toplevel_domain
+ ].compact.join('')
+
+ masked = mask(@original, visible_length: 1) if masked == @original
+
+ masked
+ end
+
+ private
+
+ def mask(plain, visible_length:, star_length: nil)
+ return if plain.empty?
+ return plain if visible_length < 0
+
+ plain = enlarge_if_needed(plain, visible_length)
+
+ star_length = plain.length - visible_length if star_length.nil?
+
+ first = plain[0, visible_length]
+ stars = '*' * star_length
+
+ "#{first}#{stars}"
+ end
+
+ def enlarge_if_needed(string, min)
+ string.ljust(min, string.first)
+ end
+ end
+
+ class Symmetrical < Masker
+ def local_part
+ mask(@local_part, visible_length: 2)
+ end
+
+ def sub_domain
+ mask(@sub_domain, visible_length: 1)
+ end
+ end
+
+ # Implements https://design.gitlab.com/usability/obfuscation#email-addresses
+ class Deform < Masker
+ def local_part
+ mask(@local_part, visible_length: 2, star_length: 5)
+ end
+
+ def sub_domain
+ mask(@sub_domain, visible_length: 1, star_length: 5)
+ end
+
+ def toplevel_domain
+ mask(@toplevel_domain, visible_length: 1, star_length: 2)
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 64579d44aa3..b1fc8e8fa42 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -762,6 +762,9 @@ msgstr ""
msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."
msgstr ""
+msgid "%{issuableDisplayName} %{lockStatus}."
+msgstr ""
+
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
@@ -1841,10 +1844,10 @@ msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
-msgid "AI Generated Test File"
+msgid "AI actions"
msgstr ""
-msgid "AI actions"
+msgid "AI-generated test file"
msgstr ""
msgid "AISummary|Experiment"
@@ -11302,6 +11305,9 @@ msgstr ""
msgid "ComplianceReport|Do you want to refresh the filtered results to include your change?"
msgstr ""
+msgid "ComplianceReport|Edit the framework"
+msgstr ""
+
msgid "ComplianceReport|Framework successfully applied"
msgstr ""
@@ -12163,6 +12169,9 @@ msgstr ""
msgid "Copy %{http_label} clone URL"
msgstr ""
+msgid "Copy %{issueType} email address"
+msgstr ""
+
msgid "Copy %{name}"
msgstr ""
@@ -16290,6 +16299,9 @@ msgstr ""
msgid "Email a new %{name} to this project"
msgstr ""
+msgid "Email address copied"
+msgstr ""
+
msgid "Email address suffix"
msgstr ""
@@ -21762,6 +21774,12 @@ msgstr ""
msgid "Header message"
msgstr ""
+msgid "HeaderAction|Notifications and other %{issueType} actions have moved to this menu."
+msgstr ""
+
+msgid "HeaderAction|Okay!"
+msgstr ""
+
msgid "HeaderAction|incident"
msgstr ""
@@ -26720,6 +26738,9 @@ msgstr ""
msgid "Lock %{issuableDisplayName}"
msgstr ""
+msgid "Lock %{issuableType}"
+msgstr ""
+
msgid "Lock File?"
msgstr ""
@@ -26729,9 +26750,6 @@ msgstr ""
msgid "Lock memberships to SAML Group Links synchronization"
msgstr ""
-msgid "Lock merge request"
-msgstr ""
-
msgid "Lock not found"
msgstr ""
@@ -27723,9 +27741,6 @@ msgstr ""
msgid "Merge request events"
msgstr ""
-msgid "Merge request locked."
-msgstr ""
-
msgid "Merge request not merged"
msgstr ""
@@ -27735,9 +27750,6 @@ msgstr ""
msgid "Merge request status"
msgstr ""
-msgid "Merge request unlocked."
-msgstr ""
-
msgid "Merge request was scheduled to merge after pipeline succeeds"
msgstr ""
@@ -36881,6 +36893,9 @@ msgstr ""
msgid "Reference"
msgstr ""
+msgid "Reference copied"
+msgstr ""
+
msgid "References"
msgstr ""
@@ -38488,6 +38503,9 @@ msgstr ""
msgid "Runners|Architecture"
msgstr ""
+msgid "Runners|Are you sure you want to disable shared runners for %{groupName}?"
+msgstr ""
+
msgid "Runners|Assigned Group"
msgstr ""
@@ -38769,6 +38787,9 @@ msgstr ""
msgid "Runners|No spot. This is the default choice for Linux Docker executor."
msgstr ""
+msgid "Runners|No, keep shared runners enabled"
+msgstr ""
+
msgid "Runners|Not accepting jobs"
msgstr ""
@@ -38993,6 +39014,9 @@ msgstr ""
msgid "Runners|Shared runners are disabled in the group settings"
msgstr ""
+msgid "Runners|Shared runners will be disabled for all projects and subgroups in this group. If you proceed, you must manually re-enable shared runners in the settings of each project and subgroup."
+msgstr ""
+
msgid "Runners|Show only inherited"
msgstr ""
@@ -39163,6 +39187,9 @@ msgstr ""
msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot."
msgstr ""
+msgid "Runners|Yes, disable shared runners"
+msgstr ""
+
msgid "Runners|Yes, start deleting stale runners"
msgstr ""
@@ -43053,6 +43080,9 @@ msgstr ""
msgid "StatusCheck|Status checks all passed"
msgstr ""
+msgid "StatusCheck|Status checks are API calls to external systems that request the status of an external requirement. %{linkStart}Learn more.%{linkEnd}"
+msgstr ""
+
msgid "StatusCheck|Status checks are being fetched"
msgstr ""
@@ -43065,6 +43095,9 @@ msgstr ""
msgid "StatusCheck|Update status check"
msgstr ""
+msgid "StatusCheck|What is status check?"
+msgstr ""
+
msgid "StatusCheck|You are about to remove the %{name} status check."
msgstr ""
@@ -47606,10 +47639,10 @@ msgstr ""
msgid "Unlock"
msgstr ""
-msgid "Unlock account"
+msgid "Unlock %{issuableType}"
msgstr ""
-msgid "Unlock merge request"
+msgid "Unlock account"
msgstr ""
msgid "Unlock more features with GitLab Ultimate"
@@ -47870,6 +47903,9 @@ msgstr ""
msgid "Upstream"
msgstr ""
+msgid "Upstream Gitaly has been exhausted. Try again later"
+msgstr ""
+
msgid "Upvotes"
msgstr ""
@@ -53253,6 +53289,9 @@ msgstr ""
msgid "loading"
msgstr ""
+msgid "locked"
+msgstr ""
+
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
@@ -54163,6 +54202,9 @@ msgstr ""
msgid "unicode domains should use IDNA encoding"
msgstr ""
+msgid "unlocked"
+msgstr ""
+
msgid "updated"
msgstr ""
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index cdd088c2d5e..ce76be9f509 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1117,4 +1117,28 @@ RSpec.describe ApplicationController, feature_category: :shared do
end
end
end
+
+ context 'when Gitlab::Git::ResourceExhaustedError exception is raised' do
+ before do
+ sign_in user
+ end
+
+ controller(described_class) do
+ def index
+ raise Gitlab::Git::ResourceExhaustedError.new(
+ "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later", 50
+ )
+ end
+ end
+
+ it 'returns a plaintext error response with 429 status' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.body).to include(
+ "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later"
+ )
+ expect(response.headers['Retry-After']).to eq(50)
+ end
+ end
end
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index cd72dbe394a..4bc7e11ec6b 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -65,6 +65,22 @@ RSpec.describe GraphqlController, feature_category: :integrations do
)
expect(response).to have_gitlab_http_status(:forbidden)
end
+
+ it 'handles Gitlab::Git::ResourceExhaustedError', :aggregate_failures do
+ allow(controller).to receive(:execute) do
+ raise Gitlab::Git::ResourceExhaustedError.new("Upstream Gitaly has been exhausted. Try again later", 50)
+ end
+
+ post :execute
+
+ expect(json_response).to include(
+ 'errors' => include(
+ a_hash_including('message' => 'Upstream Gitaly has been exhausted. Try again later')
+ )
+ )
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.headers['Retry-After']).to be(50)
+ end
end
describe 'POST #execute' do
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index 272656fb4ca..98c1f9baf12 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
before do
sign_in(reporter1)
+ stub_feature_flags(moved_mr_sidebar: false)
end
describe 'report abuse to administrator' do
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 0074b4b1eb0..dc280133a20 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
+ include CookieHelper
+
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
@@ -12,6 +14,8 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
+
visit(merge_request_path(merge_request))
end
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
index 709919d0196..a166ff46177 100644
--- a/spec/features/incidents/incident_details_spec.rb
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -94,6 +94,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the incident details page when the `issue_type` is set to incident' do
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
wait_for_requests
@@ -113,6 +114,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
+ set_cookie('new-actions-popover-viewed', 'true')
visit incident_project_issues_path(project, incident)
wait_for_requests
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index 47865d2b6ba..fb9addff1a2 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
before do
sign_in(user)
+ stub_feature_flags(moved_mr_sidebar: false)
end
context 'when a user is a team member' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 2bd5373b715..665c7307231 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
+ include CookieHelper
+
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
@@ -45,6 +47,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
before do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue_to_edit)
wait_for_requests
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index d5f90bb9260..29a61d584ee 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -98,6 +98,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
project.add_developer(user_to_be_deleted)
sign_in(user_to_be_deleted)
+ stub_feature_flags(moved_mr_sidebar: false)
visit project_issue_path(project, issue)
wait_for_requests
@@ -129,7 +130,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
describe 'when an issue `issue_type` is edited' do
before do
sign_in(user)
-
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
wait_for_requests
end
@@ -163,7 +164,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
describe 'when an incident `issue_type` is edited' do
before do
sign_in(user)
-
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, incident)
wait_for_requests
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 2ae347d4f9e..ee71181fba2 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include MobileHelpers
include Features::InviteMembersModalHelpers
+ include CookieHelper
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
@@ -20,6 +21,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'when signed in' do
before do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
context 'when concerning the assignee', :js do
@@ -205,6 +207,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'as an allowed user' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
project.add_developer(user)
visit_issue(project, issue)
end
@@ -293,6 +296,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'as a guest' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
project.add_guest(user)
visit_issue(project, issue)
end
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index c6cedbc83cd..4ef58918a2b 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do
+ include CookieHelper
+
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
@@ -18,6 +20,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
project.add_developer(user)
project_with_milestones.add_developer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
context "from edit page" do
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 904fafdf56a..00b04c10d33 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is not logged in' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
visit(project_issue_path(project, issue))
end
@@ -20,9 +21,9 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is logged in' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
project.add_developer(user)
sign_in(user)
-
visit(project_issue_path(project, issue))
end
@@ -52,6 +53,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is logged in without edit permission' do
before do
+ stub_feature_flags(moved_mr_sidebar: false)
sign_in(user2)
visit(project_issue_path(project, issue))
diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb
index d4ccc4a93b5..3bcc8255ab7 100644
--- a/spec/features/merge_request/user_manages_subscription_spec.rb
+++ b/spec/features/merge_request/user_manages_subscription_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User manages subscription', :js, feature_category: :code_review_workflow do
+ include CookieHelper
+
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -10,7 +12,7 @@ RSpec.describe 'User manages subscription', :js, feature_category: :code_review_
before do
stub_feature_flags(moved_mr_sidebar: moved_mr_sidebar_enabled)
-
+ set_cookie('new-actions-popover-viewed', 'true')
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 7cb1c95f6dc..601310cbacf 100644
--- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
+ include CookieHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -11,6 +12,7 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
before do
project.add_maintainer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
describe 'for fork' do
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
index ad2ceeb23e2..21c62b0d0d8 100644
--- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do
+ include CookieHelper
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -10,6 +12,7 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_
before do
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_merge_request_path(project, merge_request)
wait_for_requests
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 0de59ea21c5..dae28cbb05c 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do
include ListboxHelpers
+ include CookieHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -17,6 +18,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
before do
project.add_maintainer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
end
it 'selects the source branch sha when a tag with the same name exists' do
diff --git a/spec/features/profiles/user_uses_comment_template_spec.rb b/spec/features/profiles/user_uses_comment_template_spec.rb
index b426e3fb433..704d02e94f4 100644
--- a/spec/features/profiles/user_uses_comment_template_spec.rb
+++ b/spec/features/profiles/user_uses_comment_template_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'User uses comment template', :js,
it 'applies comment template' do
visit project_merge_request_path(merge_request.project, merge_request)
- find('[data-testid="comment-template-dropdown-toggle"]').click
+ find('.js-comment-template-toggle').click
wait_for_requests
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index adf410ce6e8..77f88994bfb 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'issuable templates', :js, feature_category: :projects do
include ProjectForksHelper
+ include CookieHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -12,6 +13,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :projects do
before do
project.add_maintainer(user)
sign_in user
+ set_cookie('new-actions-popover-viewed', 'true')
end
context 'user creates an issue using templates' do
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
index 55e7f5897bc..a18cdf27294 100644
--- a/spec/features/reportable_note/issue_spec.rb
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning do
+ include CookieHelper
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -11,7 +13,7 @@ RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning
before do
project.add_maintainer(user)
sign_in(user)
-
+ set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
end
diff --git a/spec/finders/packages/conan/package_finder_spec.rb b/spec/finders/packages/conan/package_finder_spec.rb
index f25a62225a8..787cb256486 100644
--- a/spec/finders/packages/conan/package_finder_spec.rb
+++ b/spec/finders/packages/conan/package_finder_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ::Packages::Conan::PackageFinder do
+RSpec.describe ::Packages::Conan::PackageFinder, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
@@ -15,7 +15,8 @@ RSpec.describe ::Packages::Conan::PackageFinder do
describe '#execute' do
let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" }
- let(:finder) { described_class.new(user, query: query) }
+ let(:finder) { described_class.new(user, params) }
+ let(:params) { { query: query } }
subject { finder.execute }
@@ -40,7 +41,7 @@ RSpec.describe ::Packages::Conan::PackageFinder do
end
with_them do
- let(:expected_packages) { packages_visible ? [conan_package, conan_package2] : [] }
+ let(:expected_packages) { packages_visible ? [conan_package2, conan_package] : [] }
let(:user) { role == :anonymous ? nil : super() }
before do
@@ -50,5 +51,23 @@ RSpec.describe ::Packages::Conan::PackageFinder do
it { is_expected.to eq(expected_packages) }
end
+
+ context 'with project' do
+ subject { described_class.new(user, params, project: project).execute }
+
+ it { is_expected.to match_array([conan_package2, conan_package]) }
+
+ it 'respects the limit' do
+ stub_const("#{described_class}::MAX_PACKAGES_COUNT", 1)
+
+ expect(subject).to match_array([conan_package2])
+ end
+
+ context 'with a different project' do
+ let_it_be(:project) { private_project }
+
+ it { is_expected.to match_array([private_package]) }
+ end
+ end
end
end
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index e92493315f7..5daa21fd618 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -2,12 +2,17 @@ import { GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { updateGroup } from '~/api/groups_api';
+import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
+import { I18N_CONFIRM_MESSAGE } from '~/group_settings/constants';
+
jest.mock('~/api/groups_api');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const GROUP_ID = '99';
+const GROUP_NAME = 'My group';
const RUNNER_ENABLED_VALUE = 'enabled';
const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_and_overridable';
@@ -19,6 +24,8 @@ describe('group_settings/components/shared_runners_form', () => {
wrapper = shallowMountExtended(SharedRunnersForm, {
provide: {
groupId: GROUP_ID,
+ groupName: GROUP_NAME,
+ groupIsEmpty: false,
sharedRunnersSetting: RUNNER_ENABLED_VALUE,
parentSharedRunnersSetting: null,
runnerEnabledValue: RUNNER_ENABLED_VALUE,
@@ -41,10 +48,12 @@ describe('group_settings/components/shared_runners_form', () => {
};
beforeEach(() => {
+ confirmAction.mockResolvedValue(true);
updateGroup.mockResolvedValue({});
});
afterEach(() => {
+ confirmAction.mockReset();
updateGroup.mockReset();
});
@@ -110,8 +119,9 @@ describe('group_settings/components/shared_runners_form', () => {
it('does not update settings while loading', async () => {
findSharedRunnersToggle().vm.$emit('change', true);
+ await nextTick();
findSharedRunnersToggle().vm.$emit('change', false);
- await waitForPromises();
+ await nextTick();
expect(updateGroup).toHaveBeenCalledTimes(1);
});
@@ -134,6 +144,8 @@ describe('group_settings/components/shared_runners_form', () => {
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
+ expect(confirmAction).not.toHaveBeenCalled();
+
expect(getSharedRunnersSetting()).toEqual(RUNNER_ENABLED_VALUE);
expect(findOverrideToggle().props('disabled')).toBe(true);
});
@@ -142,17 +154,59 @@ describe('group_settings/components/shared_runners_form', () => {
findSharedRunnersToggle().vm.$emit('change', false);
await waitForPromises();
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+ expect(confirmAction).toHaveBeenCalledWith(
+ I18N_CONFIRM_MESSAGE,
+ expect.objectContaining({
+ title: expect.stringContaining(GROUP_NAME),
+ }),
+ );
+
expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE);
expect(findOverrideToggle().props('disabled')).toBe(false);
});
+
+ describe('when user cancels operation', () => {
+ beforeEach(() => {
+ confirmAction.mockResolvedValue(false);
+ });
+
+ it('sends no payload when turned off', async () => {
+ findSharedRunnersToggle().vm.$emit('change', false);
+ await waitForPromises();
+
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+ expect(confirmAction).toHaveBeenCalledWith(
+ I18N_CONFIRM_MESSAGE,
+ expect.objectContaining({
+ title: expect.stringContaining(GROUP_NAME),
+ }),
+ );
+
+ expect(updateGroup).not.toHaveBeenCalled();
+ expect(findOverrideToggle().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when group is empty', () => {
+ beforeEach(() => {
+ createComponent({ groupIsEmpty: true });
+ });
+
+ it('confirmation is not shown when turned off', async () => {
+ findSharedRunnersToggle().vm.$emit('change', false);
+ await waitForPromises();
+
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE);
+ });
+ });
});
describe('"Override the group setting" toggle', () => {
- beforeEach(() => {
+ it('enabling the override toggle sends correct payload', async () => {
createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE });
- });
- it('enabling the override toggle sends correct payload', async () => {
findOverrideToggle().vm.$emit('change', true);
await waitForPromises();
@@ -160,6 +214,8 @@ describe('group_settings/components/shared_runners_form', () => {
});
it('disabling the override toggle sends correct payload', async () => {
+ createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE });
+
findOverrideToggle().vm.$emit('change', false);
await waitForPromises();
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index db3435855f6..a5ba512434c 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -2,6 +2,8 @@ import Vue, { nextTick } from 'vue';
import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
@@ -14,17 +16,22 @@ import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutatio
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
+import updateIssueMutation from '~/issues/show/queries/update_issue.mutation.graphql';
+import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/alert');
jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() }));
+jest.mock('~/vue_shared/plugins/global_toast');
describe('HeaderActions component', () => {
let dispatchEventSpy;
- let mutateMock;
let wrapper;
let visitUrlSpy;
Vue.use(Vuex);
+ Vue.use(VueApollo);
const store = createStore();
@@ -45,15 +52,28 @@ describe('HeaderActions component', () => {
reportedUserId: 1,
reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
+ issuableEmailAddress: null,
+ fullPath: 'full-path',
};
- const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
+ const updateIssueMutationResponse = {
+ data: {
+ updateIssue: {
+ errors: [],
+ issuable: {
+ id: 'gid://gitlab/Issue/511',
+ state: STATUS_OPEN,
+ },
+ },
+ },
+ };
const promoteToEpicMutationResponse = {
data: {
promoteToEpic: {
errors: [],
epic: {
+ id: 'gid://gitlab/Epic/1',
webPath: '/groups/gitlab-org/-/epics/1',
},
},
@@ -69,6 +89,20 @@ describe('HeaderActions component', () => {
},
};
+ const mockIssueReferenceData = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/7',
+ issuable: {
+ id: 'gid://gitlab/Issue/511',
+ reference: 'flightjs/Flight#33',
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+ };
+
const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`);
const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`);
@@ -77,33 +111,54 @@ describe('HeaderActions component', () => {
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`);
+ const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
+ const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
+ const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
+ const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`);
const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
+ const issueReferenceSuccessHandler = jest.fn().mockResolvedValue(mockIssueReferenceData);
+ const updateIssueMutationResponseHandler = jest
+ .fn()
+ .mockResolvedValue(updateIssueMutationResponse);
+ const promoteToEpicMutationSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(promoteToEpicMutationResponse);
+ const promoteToEpicMutationErrorHandler = jest
+ .fn()
+ .mockResolvedValue(promoteToEpicMutationErrorResponse);
+
const mountComponent = ({
props = {},
issueState = STATUS_OPEN,
blockedByIssues = [],
- mutateResponse = {},
+ movedMrSidebarEnabled = false,
+ promoteToEpicHandler = promoteToEpicMutationSuccessResponseHandler,
} = {}) => {
- mutateMock = jest.fn().mockResolvedValue(mutateResponse);
-
store.dispatch('setNoteableData', {
blocked_by_issues: blockedByIssues,
state: issueState,
});
+ const handlers = [
+ [issueReferenceQuery, issueReferenceSuccessHandler],
+ [updateIssueMutation, updateIssueMutationResponseHandler],
+ [promoteToEpicMutation, promoteToEpicHandler],
+ ];
+
return shallowMount(HeaderActions, {
+ apolloProvider: createMockApollo(handlers),
store,
provide: {
...defaultProps,
...props,
- },
- mocks: {
- $apollo: {
- mutate: mutateMock,
+ glFeatures: {
+ movedMrSidebar: movedMrSidebarEnabled,
},
},
stubs: {
@@ -138,7 +193,6 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({
props: { issueType },
issueState,
- mutateResponse: updateIssueMutationResponse,
});
});
@@ -149,23 +203,19 @@ describe('HeaderActions component', () => {
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: newIssueState,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: newIssueState,
+ },
+ });
});
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
@@ -290,28 +340,25 @@ describe('HeaderActions component', () => {
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
- beforeEach(() => {
+ beforeEach(async () => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- mutateResponse: promoteToEpicMutationResponse,
+ promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+
+ await waitForPromises();
});
it('invokes GraphQL mutation when clicked', () => {
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- mutation: promoteToEpicMutation,
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- },
- },
- }),
- );
+ expect(promoteToEpicMutationSuccessResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ },
+ });
});
it('shows a success message and tells the user they are being redirected', () => {
@@ -329,14 +376,16 @@ describe('HeaderActions component', () => {
});
describe('when response contains errors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- mutateResponse: promoteToEpicMutationErrorResponse,
+ promoteToEpicHandler: promoteToEpicMutationErrorHandler,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+
+ await waitForPromises();
});
it('shows an error message', () => {
@@ -349,21 +398,17 @@ describe('HeaderActions component', () => {
describe('when `toggle.issuable.state` event is emitted', () => {
it('invokes a method to toggle the issue state', () => {
- wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse });
+ wrapper = mountComponent();
eventHub.$emit('toggle.issuable.state');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ });
});
});
@@ -392,17 +437,13 @@ describe('HeaderActions component', () => {
it('calls apollo mutation when primary button is clicked', () => {
findModal().vm.$emit('primary');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid.toString(),
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid.toString(),
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ });
});
describe.each`
@@ -434,8 +475,6 @@ describe('HeaderActions component', () => {
});
describe('abuse category selector', () => {
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
-
beforeEach(() => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
@@ -445,7 +484,7 @@ describe('HeaderActions component', () => {
});
it('opens the drawer', async () => {
- findDesktopDropdownItems().at(2).vm.$emit('click');
+ findReportAbuseSelectorItem().vm.$emit('click');
await nextTick();
@@ -453,10 +492,160 @@ describe('HeaderActions component', () => {
});
it('closes the drawer', async () => {
- await findDesktopDropdownItems().at(2).vm.$emit('click');
+ await findReportAbuseSelectorItem().vm.$emit('click');
await findAbuseCategorySelector().vm.$emit('close-drawer');
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
});
+
+ describe('notification toggle', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${true}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Notification toggle`, () => {
+ expect(findNotificationWidget().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('lock issue option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${false}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Lock issue option`, () => {
+ expect(findLockIssueWidget().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('copy reference option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${true}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ 'when movedMrSidebarFlagEnabled is "$movedMrSidebarEnabled" with issue type "$issueType"',
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Copy reference option`, () => {
+ expect(findCopyRefenceDropdownItem().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+
+ describe('clicking when visible', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType: TYPE_ISSUE,
+ },
+ movedMrSidebarEnabled: true,
+ });
+ });
+
+ it('shows toast message', () => {
+ findCopyRefenceDropdownItem().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Reference copied');
+ });
+ });
+ });
+
+ describe('copy email option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | issuableEmailAddress | visible
+ ${true} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${true}
+ ${true} | ${TYPE_ISSUE} | ${''} | ${false}
+ ${true} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${''} | ${false}
+ ${false} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${false}
+ `(
+ 'when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" issue type is "$issueType" and issuableEmailAddress="$issuableEmailAddress"',
+ ({ movedMrSidebarEnabled, issueType, issuableEmailAddress, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ issuableEmailAddress,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Copy email option`, () => {
+ expect(findCopyEmailItem().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+
+ describe('clicking when visible', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType: TYPE_ISSUE,
+ issuableEmailAddress: 'mock-email-address',
+ },
+ movedMrSidebarEnabled: true,
+ });
+ });
+
+ it('shows toast message', () => {
+ findCopyEmailItem().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Email address copied');
+ });
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
new file mode 100644
index 00000000000..bf3e81c7d3a
--- /dev/null
+++ b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
@@ -0,0 +1,77 @@
+import { GlPopover } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
+import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
+import * as utils from '~/lib/utils/common_utils';
+
+describe('NewHeaderActionsPopover', () => {
+ let wrapper;
+
+ const createComponent = ({ issueType = TYPE_ISSUE, movedMrSidebarEnabled = true }) => {
+ wrapper = shallowMountExtended(NewHeaderActionsPopover, {
+ propsData: {
+ issueType,
+ },
+ stubs: {
+ GlPopover,
+ },
+ provide: {
+ glFeatures: {
+ movedMrSidebar: movedMrSidebarEnabled,
+ },
+ },
+ });
+ };
+
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findConfirmButton = () => wrapper.findByTestId('confirm-button');
+
+ it('should not be visible when the feature flag :moved_mr_sidebar is disabled', () => {
+ createComponent({ movedMrSidebarEnabled: false });
+ expect(findPopover().exists()).toBe(false);
+ });
+
+ describe('without the popover cookie', () => {
+ beforeEach(() => {
+ utils.setCookie = jest.fn();
+
+ createComponent({});
+ });
+
+ it('renders the popover with correct text', () => {
+ expect(findPopover().exists()).toBe(true);
+ expect(findPopover().text()).toContain('issue actions');
+ });
+
+ it('does not call setCookie', () => {
+ expect(utils.setCookie).not.toHaveBeenCalled();
+ });
+
+ describe('when the confirm button is clicked', () => {
+ beforeEach(() => {
+ findConfirmButton().vm.$emit('click');
+ });
+
+ it('sets the popover cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true);
+ });
+
+ it('hides the popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the popover cookie', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getCookie').mockReturnValue('true');
+
+ createComponent({});
+ });
+
+ it('does not render the popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
index 9b790e739fb..fab5a7b8844 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
@@ -66,6 +66,7 @@ describe('confirmAction', () => {
modalHtmlMessage: '<strong>Hello</strong>',
title: 'title',
hideCancel: true,
+ size: 'md',
};
await renderRootComponent('', options);
expect(modal.props()).toEqual(
@@ -79,6 +80,7 @@ describe('confirmAction', () => {
modalHtmlMessage: options.modalHtmlMessage,
title: options.title,
hideCancel: options.hideCancel,
+ size: 'md',
}),
);
});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index c135180c9df..9dcb850076c 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -14,6 +14,7 @@ describe('Confirm Modal', () => {
secondaryText,
secondaryVariant,
title,
+ size,
hideCancel = false,
} = {}) => {
wrapper = mount(ConfirmModal, {
@@ -24,6 +25,7 @@ describe('Confirm Modal', () => {
secondaryVariant,
hideCancel,
title,
+ size,
},
});
};
@@ -91,5 +93,17 @@ describe('Confirm Modal', () => {
expect(findGlModal().props().title).toBe(title);
});
+
+ it('should set modal size to `sm` by default', () => {
+ createComponent();
+
+ expect(findGlModal().props('size')).toBe('sm');
+ });
+
+ it('should set modal size when `size` prop is set', () => {
+ createComponent({ size: 'md' });
+
+ expect(findGlModal().props('size')).toBe('md');
+ });
});
});
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index d26ef7298ce..5e766e9a41c 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -29,6 +29,7 @@ describe('IssuableLockForm', () => {
const findEditForm = () => wrapper.findComponent(EditForm);
const findSidebarLockStatusTooltip = () =>
getBinding(findSidebarCollapseIcon().element, 'gl-tooltip');
+ const findIssuableLockClickable = () => wrapper.find('[data-testid="issuable-lock"]');
const initStore = (isLocked) => {
if (issuableType === ISSUABLE_TYPE_ISSUE) {
@@ -48,7 +49,7 @@ describe('IssuableLockForm', () => {
store.getters.getNoteableData.discussion_locked = isLocked;
};
- const createComponent = ({ props = {} }, movedMrSidebar = false) => {
+ const createComponent = ({ props = {}, movedMrSidebar = false }) => {
wrapper = shallowMount(IssuableLockForm, {
store,
provide: {
@@ -169,11 +170,27 @@ describe('IssuableLockForm', () => {
`('displays $message when merge request is $locked', async ({ locked, message }) => {
initStore(locked);
- createComponent({}, true);
+ createComponent({ movedMrSidebar: true });
await wrapper.find('.dropdown-item').trigger('click');
expect(toast).toHaveBeenCalledWith(message);
});
});
+
+ describe('moved_mr_sidebar flag', () => {
+ describe('when the flag is off', () => {
+ it('does not show the non editable lock status', () => {
+ createComponent({ movedMrSidebar: false });
+ expect(findIssuableLockClickable().exists()).toBe(false);
+ });
+ });
+
+ describe('when the flag is on', () => {
+ it('does not show the non editable lock status', () => {
+ createComponent({ movedMrSidebar: true });
+ expect(findIssuableLockClickable().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
index bfa9b7dd706..aea25abb324 100644
--- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -49,7 +49,7 @@ describe('Comment templates dropdown', () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
- wrapper.findByTestId('comment-template-dropdown-toggle').trigger('click');
+ wrapper.find('.js-comment-template-toggle').trigger('click');
await waitForPromises();
@@ -60,7 +60,7 @@ describe('Comment templates dropdown', () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
- wrapper.findByTestId('comment-template-dropdown-toggle').trigger('click');
+ wrapper.find('.js-comment-template-toggle').trigger('click');
await waitForPromises();
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index a01002a2a1e..ab3f0b193ac 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -106,6 +106,8 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
describe '#group_shared_runners_settings_data' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
+ let_it_be(:group_with_project) { create(:group, parent: parent) }
+ let_it_be(:project) { create(:project, group: group_with_project) }
let(:runner_constants) do
{
@@ -118,6 +120,8 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
it 'returns group data for top level group' do
result = {
group_id: parent.id,
+ group_name: parent.name,
+ group_is_empty: 'false',
shared_runners_setting: Namespace::SR_ENABLED,
parent_shared_runners_setting: nil
}.merge(runner_constants)
@@ -128,12 +132,26 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
it 'returns group data for child group' do
result = {
group_id: group.id,
+ group_name: group.name,
+ group_is_empty: 'true',
shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
parent_shared_runners_setting: Namespace::SR_ENABLED
}.merge(runner_constants)
expect(helper.group_shared_runners_settings_data(group)).to eq result
end
+
+ it 'returns group data for child group with project' do
+ result = {
+ group_id: group_with_project.id,
+ group_name: group_with_project.name,
+ group_is_empty: 'false',
+ shared_runners_setting: Namespace::SR_ENABLED,
+ parent_shared_runners_setting: Namespace::SR_ENABLED
+ }.merge(runner_constants)
+
+ expect(helper.group_shared_runners_settings_data(group_with_project)).to eq result
+ end
end
describe '#group_runners_data_attributes' do
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index d940c696fb3..38cbb5a1d66 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IssuesHelper do
+ include Features::MergeRequestHelpers
+
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:issue) { create(:issue, project: project) }
@@ -235,10 +237,13 @@ RSpec.describe IssuesHelper do
describe '#issue_header_actions_data' do
let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, :opened, source_project: project, author: current_user) }
+ let(:issuable_sidebar_issue) { serialize_issuable_sidebar(current_user, project, merge_request) }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
+ allow(helper).to receive(:issuable_sidebar).and_return(issuable_sidebar_issue)
end
it 'returns expected result' do
@@ -257,10 +262,11 @@ RSpec.describe IssuesHelper do
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issue.author.id,
reported_from_url: issue_url(issue),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issue),
+ issuable_email_address: issuable_sidebar_issue[:create_note_email]
}
- expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected)
+ expect(helper.issue_header_actions_data(project, issue, current_user, issuable_sidebar_issue)).to include(expected)
end
end
diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb
index c7b8225b866..b8290fa2337 100644
--- a/spec/helpers/sessions_helper_spec.rb
+++ b/spec/helpers/sessions_helper_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe SessionsHelper do
end
describe '#send_rate_limited?' do
- let_it_be(:user) { build(:user) }
+ let(:user) { build_stubbed(:user) }
subject { helper.send_rate_limited?(user) }
@@ -77,30 +77,14 @@ RSpec.describe SessionsHelper do
end
describe '#obfuscated_email' do
- subject { helper.obfuscated_email(email) }
-
- context 'when an email address is normal length' do
- let(:email) { 'alex@gitlab.com' }
-
- it { is_expected.to eq('al**@g*****.com') }
- end
-
- context 'when an email address contains multiple top level domains' do
- let(:email) { 'alex@gl.co.uk' }
+ let(:email) { 'mail@example.com' }
- it { is_expected.to eq('al**@g****.uk') }
- end
-
- context 'when an email address is very short' do
- let(:email) { 'a@b.c' }
-
- it { is_expected.to eq('a@b.c') }
- end
+ subject { helper.obfuscated_email(email) }
- context 'when an email address is even shorter' do
- let(:email) { 'a@b' }
+ it 'delegates to Gitlab::Utils::Email.obfuscated_email' do
+ expect(Gitlab::Utils::Email).to receive(:obfuscated_email).with(email).and_call_original
- it { is_expected.to eq('a@b') }
+ expect(subject).to eq('ma**@e******.com')
end
end
end
diff --git a/spec/lib/gitlab/avatar_cache_spec.rb b/spec/lib/gitlab/avatar_cache_spec.rb
index ffe6f81b6e7..a57d811edaf 100644
--- a/spec/lib/gitlab/avatar_cache_spec.rb
+++ b/spec/lib/gitlab/avatar_cache_spec.rb
@@ -62,40 +62,52 @@ RSpec.describe Gitlab::AvatarCache, :clean_gitlab_redis_cache do
end
describe "#delete_by_email" do
- subject { described_class.delete_by_email(*emails) }
+ shared_examples 'delete emails' do
+ subject { described_class.delete_by_email(*emails) }
- before do
- perform_fetch
- end
+ before do
+ perform_fetch
+ end
- context "no emails, somehow" do
- let(:emails) { [] }
+ context "no emails, somehow" do
+ let(:emails) { [] }
- it { is_expected.to eq(0) }
- end
+ it { is_expected.to eq(0) }
+ end
- context "single email" do
- let(:emails) { "foo@bar.com" }
+ context "single email" do
+ let(:emails) { "foo@bar.com" }
- it "removes the email" do
- expect(read(key, "20:2:true")).to eq(avatar_path)
+ it "removes the email" do
+ expect(read(key, "20:2:true")).to eq(avatar_path)
- expect(subject).to eq(1)
+ expect(subject).to eq(1)
- expect(read(key, "20:2:true")).to eq(nil)
+ expect(read(key, "20:2:true")).to eq(nil)
+ end
end
- end
- context "multiple emails" do
- let(:emails) { ["foo@bar.com", "missing@baz.com"] }
+ context "multiple emails" do
+ let(:emails) { ["foo@bar.com", "missing@baz.com"] }
- it "removes the emails it finds" do
- expect(read(key, "20:2:true")).to eq(avatar_path)
+ it "removes the emails it finds" do
+ expect(read(key, "20:2:true")).to eq(avatar_path)
- expect(subject).to eq(1)
+ expect(subject).to eq(1)
- expect(read(key, "20:2:true")).to eq(nil)
+ expect(read(key, "20:2:true")).to eq(nil)
+ end
+ end
+ end
+
+ context 'when feature flag disabled' do
+ before do
+ stub_feature_flags(use_pipeline_over_multikey: false)
end
+
+ it_behaves_like 'delete emails'
end
+
+ it_behaves_like 'delete emails'
end
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
index 9ccae754a92..82bba31193b 100644
--- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
@@ -61,12 +61,12 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction, feature_cate
context 'lock_fiber' do
it 'acquires lock successfully' do
- check_exclusive_lock_query = """
+ check_exclusive_lock_query = <<~QUERY
SELECT 1
FROM pg_locks l
JOIN pg_class t ON l.relation = t.oid
WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
- """
+ QUERY
expect(connection.execute(check_exclusive_lock_query).to_a).to be_present
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 7fe6362634b..7e0435c815b 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -61,12 +61,12 @@ RSpec.describe Gitlab::Database::WithLockRetries, feature_category: :database do
context 'lock_fiber' do
it 'acquires lock successfully' do
- check_exclusive_lock_query = """
+ check_exclusive_lock_query = <<~QUERY
SELECT 1
FROM pg_locks l
JOIN pg_class t ON l.relation = t.oid
WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
- """
+ QUERY
expect(connection.execute(check_exclusive_lock_query).to_a).to be_present
end
diff --git a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
index 30981e4bd7d..0dc0f50b104 100644
--- a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
@@ -41,57 +41,81 @@ RSpec.describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cach
end
describe '#read_multiple' do
- it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do
- described_class.write_multiple(mapping)
+ shared_examples 'read multiple keys' do
+ it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do
+ described_class.write_multiple(mapping)
- found = described_class.read_multiple(mapping.keys)
+ found = described_class.read_multiple(mapping.keys)
- expect(found.size).to eq(2)
- expect(found.first.size).to eq(2)
- expect(found.first).to all(be_a(Gitlab::Diff::Line))
- end
+ expect(found.size).to eq(2)
+ expect(found.first.size).to eq(2)
+ expect(found.first).to all(be_a(Gitlab::Diff::Line))
+ end
- it 'returns nil when cached key is not found' do
- described_class.write_multiple(mapping)
+ it 'returns nil when cached key is not found' do
+ described_class.write_multiple(mapping)
- found = described_class.read_multiple([2, 3])
+ found = described_class.read_multiple([2, 3])
- expect(found.size).to eq(2)
+ expect(found.size).to eq(2)
- expect(found.first).to eq(nil)
- expect(found.second.size).to eq(2)
- expect(found.second).to all(be_a(Gitlab::Diff::Line))
- end
+ expect(found.first).to eq(nil)
+ expect(found.second.size).to eq(2)
+ expect(found.second).to all(be_a(Gitlab::Diff::Line))
+ end
- it 'returns lines which rich_text are HTML-safe' do
- described_class.write_multiple(mapping)
+ it 'returns lines which rich_text are HTML-safe' do
+ described_class.write_multiple(mapping)
+
+ found = described_class.read_multiple(mapping.keys)
+ rich_texts = found.flatten.map(&:rich_text)
+
+ expect(rich_texts).to all(be_html_safe)
+ end
+ end
- found = described_class.read_multiple(mapping.keys)
- rich_texts = found.flatten.map(&:rich_text)
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(use_pipeline_over_multikey: false)
+ end
- expect(rich_texts).to all(be_html_safe)
+ it_behaves_like 'read multiple keys'
end
+
+ it_behaves_like 'read multiple keys'
end
describe '#clear_multiple' do
- it 'removes all named keys' do
- described_class.write_multiple(mapping)
+ shared_examples 'delete multiple keys' do
+ it 'removes all named keys' do
+ described_class.write_multiple(mapping)
- described_class.clear_multiple(mapping.keys)
+ described_class.clear_multiple(mapping.keys)
- expect(described_class.read_multiple(mapping.keys)).to all(be_nil)
- end
+ expect(described_class.read_multiple(mapping.keys)).to all(be_nil)
+ end
- it 'only removed named keys' do
- to_clear, to_leave = mapping.keys
+ it 'only removed named keys' do
+ to_clear, to_leave = mapping.keys
- described_class.write_multiple(mapping)
- described_class.clear_multiple([to_clear])
+ described_class.write_multiple(mapping)
+ described_class.clear_multiple([to_clear])
- cleared, left = described_class.read_multiple([to_clear, to_leave])
+ cleared, left = described_class.read_multiple([to_clear, to_leave])
- expect(cleared).to be_nil
- expect(left).to all(be_a(Gitlab::Diff::Line))
+ expect(cleared).to be_nil
+ expect(left).to all(be_a(Gitlab::Diff::Line))
+ end
end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(use_pipeline_over_multikey: false)
+ end
+
+ it_behaves_like 'delete multiple keys'
+ end
+
+ it_behaves_like 'delete multiple keys'
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index e78e01ae129..06904849ef5 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1858,8 +1858,8 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
context 'when Gitaly returns Internal error' do
before do
- expect(repository.gitaly_ref_client)
- .to receive(:find_tag)
+ expect(Gitlab::GitalyClient)
+ .to receive(:call)
.and_raise(GRPC::Internal, "tag not found")
end
@@ -1868,8 +1868,8 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
context 'when Gitaly returns tag_not_found error' do
before do
- expect(repository.gitaly_ref_client)
- .to receive(:find_tag)
+ expect(Gitlab::GitalyClient)
+ .to receive(:call)
.and_raise(new_detailed_error(GRPC::Core::StatusCodes::NOT_FOUND,
"tag was not found",
Gitaly::FindTagError.new(tag_not_found: Gitaly::ReferenceNotFoundError.new)))
diff --git a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
index e551dfaa1c5..c321d4bbdb9 100644
--- a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
+++ b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
@@ -2,24 +2,81 @@
require 'spec_helper'
-RSpec.describe Gitlab::Git::WrapsGitalyErrors do
+RSpec.describe Gitlab::Git::WrapsGitalyErrors, feature_category: :gitaly do
subject(:wrapper) do
klazz = Class.new { include Gitlab::Git::WrapsGitalyErrors }
klazz.new
end
describe "#wrapped_gitaly_errors" do
- mapping = {
- GRPC::NotFound => Gitlab::Git::Repository::NoRepository,
- GRPC::InvalidArgument => ArgumentError,
- GRPC::DeadlineExceeded => Gitlab::Git::CommandTimedOut,
- GRPC::BadStatus => Gitlab::Git::CommandError
- }
-
- mapping.each do |grpc_error, error|
- it "wraps #{grpc_error} in a #{error}" do
- expect { wrapper.wrapped_gitaly_errors { raise grpc_error, 'wrapped' } }
- .to raise_error(error)
+ where(:original_error, :wrapped_error) do
+ [
+ [GRPC::NotFound, Gitlab::Git::Repository::NoRepository],
+ [GRPC::InvalidArgument, ArgumentError],
+ [GRPC::DeadlineExceeded, Gitlab::Git::CommandTimedOut],
+ [GRPC::BadStatus, Gitlab::Git::CommandError]
+ ]
+ end
+
+ with_them do
+ it "wraps #{params[:original_error]} in a #{params[:wrapped_error]}" do
+ expect { wrapper.wrapped_gitaly_errors { raise original_error, 'wrapped' } }
+ .to raise_error(wrapped_error)
+ end
+ end
+
+ context 'when wrap GRPC::ResourceExhausted' do
+ context 'with Gitaly::LimitError detail' do
+ let(:original_error) do
+ new_detailed_error(
+ GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED,
+ 'resource exhausted',
+ Gitaly::LimitError.new(
+ error_message: "maximum time in concurrency queue reached",
+ retry_after: Google::Protobuf::Duration.new(seconds: 5, nanos: 1500)
+ )
+ )
+ end
+
+ it "wraps in a Gitlab::Git::ResourceExhaustedError with error message" do
+ expect { wrapper.wrapped_gitaly_errors { raise original_error } }.to raise_error do |wrapped_error|
+ expect(wrapped_error).to be_a(Gitlab::Git::ResourceExhaustedError)
+ expect(wrapped_error.message).to eql(
+ "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later"
+ )
+ expect(wrapped_error.headers).to eql({ 'Retry-After' => 5 })
+ end
+ end
+ end
+
+ context 'with Gitaly::LimitError detail without retry after' do
+ let(:original_error) do
+ new_detailed_error(
+ GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED,
+ 'resource exhausted',
+ Gitaly::LimitError.new(error_message: "maximum time in concurrency queue reached")
+ )
+ end
+
+ it "wraps in a Gitlab::Git::ResourceExhaustedError with error message" do
+ expect { wrapper.wrapped_gitaly_errors { raise original_error } }.to raise_error do |wrapped_error|
+ expect(wrapped_error).to be_a(Gitlab::Git::ResourceExhaustedError)
+ expect(wrapped_error.message).to eql(
+ "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later"
+ )
+ expect(wrapped_error.headers).to eql({})
+ end
+ end
+ end
+
+ context 'without Gitaly::LimitError detail' do
+ it("wraps in a Gitlab::Git::ResourceExhaustedError with default message") {
+ expect { wrapper.wrapped_gitaly_errors { raise GRPC::ResourceExhausted } }.to raise_error do |wrapped_error|
+ expect(wrapped_error).to be_a(Gitlab::Git::ResourceExhaustedError)
+ expect(wrapped_error.message).to eql("Upstream Gitaly has been exhausted. Try again later")
+ expect(wrapped_error.headers).to eql({})
+ end
+ }
end
end
diff --git a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb
index 207ac1c0eaa..a78d15134fa 100644
--- a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb
+++ b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb
@@ -46,17 +46,29 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
end
describe '#clear_cache!', :use_clean_rails_redis_caching do
- it 'deletes the cached items' do
- # Cached key and value
- Rails.cache.write('test_item', 'test_value')
- # Add key to set
- cache.write(cache_prefix, 'test_item')
+ shared_examples 'clears cache' do
+ it 'deletes the cached items' do
+ # Cached key and value
+ Rails.cache.write('test_item', 'test_value')
+ # Add key to set
+ cache.write(cache_prefix, 'test_item')
- expect(cache.read(cache_prefix)).to contain_exactly('test_item')
- cache.clear_cache!(cache_prefix)
+ expect(cache.read(cache_prefix)).to contain_exactly('test_item')
+ cache.clear_cache!(cache_prefix)
+
+ expect(cache.read(cache_prefix)).to be_empty
+ end
+ end
- expect(cache.read(cache_prefix)).to be_empty
+ context 'when featuer flag disabled' do
+ before do
+ stub_feature_flags(use_pipeline_over_multikey: false)
+ end
+
+ it_behaves_like 'clears cache'
end
+
+ it_behaves_like 'clears cache'
end
describe '#include?' do
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index c93fd884347..65a50b68c44 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -72,48 +72,60 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
end
describe '#expire' do
- subject { cache.expire(*keys) }
+ shared_examples 'expires varying amount of keys' do
+ subject { cache.expire(*keys) }
- before do
- cache.write(:foo, ['value'])
- cache.write(:bar, ['value2'])
- end
+ before do
+ cache.write(:foo, ['value'])
+ cache.write(:bar, ['value2'])
+ end
- it 'actually wrote the values' do
- expect(cache.read(:foo)).to contain_exactly('value')
- expect(cache.read(:bar)).to contain_exactly('value2')
- end
+ it 'actually wrote the values' do
+ expect(cache.read(:foo)).to contain_exactly('value')
+ expect(cache.read(:bar)).to contain_exactly('value2')
+ end
- context 'single key' do
- let(:keys) { %w(foo) }
+ context 'single key' do
+ let(:keys) { %w(foo) }
- it { is_expected.to eq(1) }
+ it { is_expected.to eq(1) }
- it 'deletes the given key from the cache' do
- subject
+ it 'deletes the given key from the cache' do
+ subject
- expect(cache.read(:foo)).to be_empty
+ expect(cache.read(:foo)).to be_empty
+ end
end
- end
- context 'multiple keys' do
- let(:keys) { %w(foo bar) }
+ context 'multiple keys' do
+ let(:keys) { %w(foo bar) }
- it { is_expected.to eq(2) }
+ it { is_expected.to eq(2) }
- it 'deletes the given keys from the cache' do
- subject
+ it 'deletes the given keys from the cache' do
+ subject
- expect(cache.read(:foo)).to be_empty
- expect(cache.read(:bar)).to be_empty
+ expect(cache.read(:foo)).to be_empty
+ expect(cache.read(:bar)).to be_empty
+ end
+ end
+
+ context 'no keys' do
+ let(:keys) { [] }
+
+ it { is_expected.to eq(0) }
end
end
- context 'no keys' do
- let(:keys) { [] }
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(use_pipeline_over_multikey: false)
+ end
- it { is_expected.to eq(0) }
+ it_behaves_like 'expires varying amount of keys'
end
+
+ it_behaves_like 'expires varying amount of keys'
end
describe '#exist?' do
diff --git a/spec/lib/gitlab/utils/email_spec.rb b/spec/lib/gitlab/utils/email_spec.rb
index d7a881d8655..c81c2558f70 100644
--- a/spec/lib/gitlab/utils/email_spec.rb
+++ b/spec/lib/gitlab/utils/email_spec.rb
@@ -8,13 +8,20 @@ RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do
describe '.obfuscated_email' do
where(:input, :output) do
- 'alex@gitlab.com' | 'al**@g*****.com'
- 'alex@gl.co.uk' | 'al**@g****.uk'
- 'a@b.c' | 'a@b.c'
- 'q@example.com' | 'q@e******.com'
- 'q@w.' | 'q@w.'
- 'a@b' | 'a@b'
- 'no mail' | 'no mail'
+ 'alex@gitlab.com' | 'al**@g*****.com'
+ 'alex@gl.co.uk' | 'al**@g****.uk'
+ 'a@b.c' | 'aa@b.c'
+ 'qqwweerrttyy@example.com' | 'qq**********@e******.com'
+ 'getsuperfancysupport@paywhatyouwant.accounting' | 'ge******************@p*************.accounting'
+ 'q@example.com' | 'qq@e******.com'
+ 'q@w.' | 'qq@w.'
+ 'a@b' | 'aa@b'
+ 'trun"@"e@example.com' | 'tr******@e******.com'
+ '@' | '@'
+ 'n' | 'n'
+ 'no mail' | 'n******'
+ 'truncated@exa' | 'tr*******@exa'
+ '' | ''
end
with_them do
@@ -29,9 +36,14 @@ RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do
'qqwweerrttyy@example.com' | 'qq*****@e*****.c**'
'getsuperfancysupport@paywhatyouwant.accounting' | 'ge*****@p*****.a**'
'q@example.com' | 'qq*****@e*****.c**'
- 'q@w.' | 'q@w.'
- 'a@b' | 'a@b'
- 'no mail' | 'no mail'
+ 'q@w.' | 'qq*****@w*****.'
+ 'a@b' | 'aa*****@b**'
+ 'trun"@"e@example.com' | 'tr*****@e*****.c**'
+ '@' | '@'
+ 'no mail' | 'n**'
+ 'n' | 'n**'
+ 'truncated@exa' | 'tr*****@e**'
+ '' | ''
end
with_them do
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index fcf60f0559a..3291eba48d3 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -847,6 +847,14 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
is_expected.to match_array([package])
end
end
+
+ describe '.preload_conan_metadatum' do
+ subject { described_class.preload_conan_metadatum }
+
+ it 'loads conan metadatum' do
+ expect(subject.first.association(:conan_metadatum)).to be_loaded
+ end
+ end
end
describe '.without_nuget_temporary_name' do
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index d5ad0779bd9..219c7dbdbc5 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -359,4 +359,26 @@ RSpec.describe API::API, feature_category: :system_access do
end
end
end
+
+ describe 'Handle Gitlab::Git::ResourceExhaustedError exception' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ project.add_maintainer(user)
+ allow(Gitlab::GitalyClient).to receive(:call).with(any_args).and_raise(
+ Gitlab::Git::ResourceExhaustedError.new("Upstream Gitaly has been exhausted. Try again later", 50)
+ )
+ end
+
+ it 'returns 429 status with exhausted' do
+ get api("/projects/#{project.id}/repository/commits", user)
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.headers['Retry-After']).to be(50)
+ expect(json_response).to eql(
+ 'message' => 'Upstream Gitaly has been exhausted. Try again later'
+ )
+ end
+ end
end
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
index 814745f9e29..06f175233db 100644
--- a/spec/requests/api/conan_project_packages_spec.rb
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -33,6 +33,29 @@ RSpec.describe API::ConanProjectPackages, feature_category: :package_registry do
subject { get api(url), params: params }
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+
+ get api(url), params: params
+ end
+
+ subject { json_response['results'] }
+
+ context 'with a matching name' do
+ let(:params) { { q: package.conan_recipe } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'with a * wildcard' do
+ let(:params) { { q: "#{package.name[0, 3]}*" } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+ end
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do
diff --git a/spec/services/packages/conan/search_service_spec.rb b/spec/services/packages/conan/search_service_spec.rb
index 9e8be164d8c..83ece404d5f 100644
--- a/spec/services/packages/conan/search_service_spec.rb
+++ b/spec/services/packages/conan/search_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Packages::Conan::SearchService, feature_category: :package_regist
let!(:conan_package) { create(:conan_package, project: project) }
let!(:conan_package2) { create(:conan_package, project: project) }
- subject { described_class.new(user, query: query) }
+ subject { described_class.new(project, user, query: query) }
before do
project.add_developer(user)
@@ -24,7 +24,7 @@ RSpec.describe Packages::Conan::SearchService, feature_category: :package_regist
result = subject.execute
expect(result.status).to eq :success
- expect(result.payload).to eq(results: [conan_package.conan_recipe, conan_package2.conan_recipe])
+ expect(result.payload).to eq(results: [conan_package2.conan_recipe, conan_package.conan_recipe])
end
end
@@ -71,5 +71,29 @@ RSpec.describe Packages::Conan::SearchService, feature_category: :package_regist
expect(result.payload).to eq(results: [])
end
end
+
+ context 'for project' do
+ let_it_be(:project2) { create(:project, :public) }
+ let(:query) { conan_package.name }
+ let!(:conan_package3) { create(:conan_package, name: conan_package.name, project: project2) }
+
+ context 'when passing a project' do
+ it 'returns only packages of the given project' do
+ result = subject.execute
+
+ expect(result.status).to eq :success
+ expect(result[:results]).to match_array([conan_package.conan_recipe])
+ end
+ end
+
+ context 'when passing a project with nil' do
+ it 'returns all packages' do
+ result = described_class.new(nil, user, query: query).execute
+
+ expect(result.status).to eq :success
+ expect(result[:results]).to eq([conan_package3.conan_recipe, conan_package.conan_recipe])
+ end
+ end
+ end
end
end
diff --git a/spec/services/packages/conan/single_package_search_service_spec.rb b/spec/services/packages/conan/single_package_search_service_spec.rb
new file mode 100644
index 00000000000..1d95d1d4f64
--- /dev/null
+++ b/spec/services/packages/conan/single_package_search_service_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Conan::SinglePackageSearchService, feature_category: :package_registry do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+
+ let!(:conan_package) { create(:conan_package, project: project) }
+ let!(:conan_package2) { create(:conan_package, project: project) }
+
+ describe '#execute' do
+ context 'with a valid query and user with permissions' do
+ before do
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:can_access_project_package?).and_return(true)
+ end
+ end
+
+ it 'returns the correct package' do
+ [conan_package, conan_package2].each do |package|
+ result = described_class.new(package.conan_recipe, user).execute
+
+ expect(result.status).to eq :success
+ expect(result[:results]).to match_array([package.conan_recipe])
+ end
+ end
+ end
+
+ context 'with a user without permissions' do
+ before do
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:can_access_project_package?).and_return(false)
+ end
+ end
+
+ it 'returns an empty array' do
+ result = described_class.new(conan_package.conan_recipe, user).execute
+
+ expect(result.status).to eq :success
+ expect(result[:results]).to match_array([])
+ end
+ end
+ end
+end