diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-29 06:13:40 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-29 06:13:40 +0300 |
commit | 41310fed8a373f2869cdd5946605e28a977706f1 (patch) | |
tree | 55390a2f6c782dec7363f13ade1c1c49a7163332 | |
parent | 46f9d8c35974925bf3d01380e10dd920e6dcaaf3 (diff) |
Add latest changes from gitlab-org/gitlab@master
51 files changed, 542 insertions, 280 deletions
diff --git a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue index 419efcba46d..4a30878bec5 100644 --- a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue +++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue @@ -146,7 +146,7 @@ export default { // BE returns zero based index, we need to add one to match the line numbers in the DOM const firstSearchResult = `#L${this.searchResults[0].lineNumber + 1}`; - const logLine = document.querySelector(`.log-line ${firstSearchResult}`); + const logLine = document.querySelector(`.js-log-line ${firstSearchResult}`); if (logLine) { setTimeout(() => scrollToElement(logLine)); diff --git a/app/assets/javascripts/ci/job_details/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue index fa4a12b3dd3..eba8727c939 100644 --- a/app/assets/javascripts/ci/job_details/components/log/line.vue +++ b/app/assets/javascripts/ci/job_details/components/log/line.vue @@ -66,7 +66,11 @@ export default { return h( 'div', { - class: ['js-line', 'log-line', { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }], + class: [ + 'js-log-line', + 'log-line', + { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }, + ], }, [ h(LineNumber, { diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue index e647ab4ac0b..f2e9e09c334 100644 --- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue +++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue @@ -62,7 +62,7 @@ export default { <template> <div - class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative" + class="js-log-line log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative" :class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }" role="button" @click="handleOnClick" diff --git a/app/assets/javascripts/comment_templates/components/form.vue b/app/assets/javascripts/comment_templates/components/form.vue index c29482eab7a..5a5d221591a 100644 --- a/app/assets/javascripts/comment_templates/components/form.vue +++ b/app/assets/javascripts/comment_templates/components/form.vue @@ -93,7 +93,7 @@ export default { this.$emit('saved'); this.updateCommentTemplate = { name: '', content: '' }; this.showValidation = false; - this.track_event('i_code_review_saved_replies_create'); + this.trackEvent('i_code_review_saved_replies_create'); } }, }) diff --git a/app/assets/javascripts/custom_emoji/components/app.vue b/app/assets/javascripts/custom_emoji/components/app.vue index 405a296397f..00b904fbea4 100644 --- a/app/assets/javascripts/custom_emoji/components/app.vue +++ b/app/assets/javascripts/custom_emoji/components/app.vue @@ -8,7 +8,7 @@ export default {}; <h4 class="gl-mt-0"> {{ __('Custom emoji') }} </h4> - <p>{{ __('Custom emoji will be available to use in every project in group.') }}</p> + <p>{{ __('Custom emoji will be available to use in every project in the group.') }}</p> <router-view /> </div> </div> diff --git a/app/assets/javascripts/emoji/awards_app/index.js b/app/assets/javascripts/emoji/awards_app/index.js index f3d72c2dba5..8559f982bf4 100644 --- a/app/assets/javascripts/emoji/awards_app/index.js +++ b/app/assets/javascripts/emoji/awards_app/index.js @@ -9,7 +9,7 @@ export default (el) => { if (!el) return null; const { - dataset: { path }, + dataset: { path, newCustomEmojiPath }, } = el; const canAwardEmoji = parseBoolean(el.dataset.canAwardEmoji); @@ -17,6 +17,9 @@ export default (el) => { el, name: 'AwardsListRoot', store: createstore(), + provide: { + newCustomEmojiPath, + }, computed: { ...mapState(['currentUserId', 'canAwardEmoji', 'awards']), }, diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue index fcc54f17466..238f0d81b22 100644 --- a/app/assets/javascripts/emoji/components/picker.vue +++ b/app/assets/javascripts/emoji/components/picker.vue @@ -1,6 +1,6 @@ <!-- eslint-disable vue/multi-word-component-names --> <script> -import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; +import { GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; import { findLastIndex } from 'lodash'; import VirtualList from 'vue-virtual-scroll-list'; import { CATEGORY_NAMES, getEmojiCategoryMap, state } from '~/emoji'; @@ -13,11 +13,17 @@ export default { components: { GlIcon, GlDropdown, + GlDropdownItem, GlSearchBoxByType, VirtualList, Category, EmojiList, }, + inject: { + newCustomEmojiPath: { + default: '', + }, + }, props: { toggleClass: { type: [Array, String, Object], @@ -167,6 +173,11 @@ export default { </virtual-list> </template> </emoji-list> + <template v-if="newCustomEmojiPath" #footer> + <gl-dropdown-item :href="newCustomEmojiPath"> + {{ __('Create new emoji') }} + </gl-dropdown-item> + </template> </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index 04167518d3f..265e2a2f880 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -44,6 +44,7 @@ export default () => { reportAbusePath: notesDataset.reportAbusePath, newCommentTemplatePath: notesDataset.newCommentTemplatePath, mrFilter: true, + newCustomEmojiPath: notesDataset.newCustomEmojiPath, }, data() { const noteableData = JSON.parse(notesDataset.noteableData); diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 363383fd7ad..ccfd4f2c502 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -202,6 +202,9 @@ export default { isDisabled() { return !this.updatedNoteBody.length || this.isSubmitting; }, + isInternalNote() { + return this.discussionNote.internal || this.discussion.confidential; + }, discussionNote() { const discussionNote = this.discussion.id ? this.getDiscussionLastNote(this.discussion) @@ -355,10 +358,7 @@ export default { </div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> - <comment-field-layout - :noteable-data="getNoteableData" - :is-internal-note="discussionNote.internal" - > + <comment-field-layout :noteable-data="getNoteableData" :is-internal-note="isInternalNote"> <markdown-editor ref="markdownEditor" :enable-content-editor="enableContentEditor" diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 724b47bf44b..f9fbe6659ee 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -62,6 +62,7 @@ export default ({ editorAiActions = [] } = {}) => { newCommentTemplatePath: notesDataset.newCommentTemplatePath, resourceGlobalId: convertToGraphQLId(noteableData.noteableType, noteableData.id), editorAiActions: editorAiActions.map((factory) => factory(noteableData)), + newCustomEmojiPath: notesDataset.newCustomEmojiPath, }, data() { return { diff --git a/app/assets/javascripts/pages/groups/custom_emoji/index.js b/app/assets/javascripts/pages/groups/custom_emoji/index.js new file mode 100644 index 00000000000..dd02a6f5348 --- /dev/null +++ b/app/assets/javascripts/pages/groups/custom_emoji/index.js @@ -0,0 +1,3 @@ +import { initCustomEmojis } from '~/custom_emoji/custom_emoji_bundle'; + +requestIdleCallback(initCustomEmojis); diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index da948cc85b6..1eee7a932a4 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -52,7 +52,7 @@ export function initTrackProductAnalyticsExpanded() { const $analyticsSection = $('#js-product-analytics-settings'); $analyticsSection.on('click.toggleSection', '.js-settings-toggle', () => { if (isExpanded($analyticsSection)) { - InternalEvents.track_event('user_viewed_cluster_configuration'); + InternalEvents.trackEvent('user_viewed_cluster_configuration'); } }); } diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js index 9bd0200cad1..131cdac282f 100644 --- a/app/assets/javascripts/tracking/internal_events.js +++ b/app/assets/javascripts/tracking/internal_events.js @@ -17,7 +17,7 @@ const InternalEvents = { * @param {string} event * @param {object} data */ - track_event(event, data = {}) { + trackEvent(event, data = {}) { const { context, ...rest } = data; const defaultContext = { @@ -42,8 +42,8 @@ const InternalEvents = { mixin() { return { methods: { - track_event(event, data = {}) { - InternalEvents.track_event(event, data); + trackEvent(event, data = {}) { + InternalEvents.trackEvent(event, data); }, }, }; @@ -62,7 +62,7 @@ const InternalEvents = { // eslint-disable-next-line no-param-reassign parent.internalEventsTrackingBound = true; - const handler = { name: 'click', func: (e) => InternalEventHandler(e, this.track_event) }; + const handler = { name: 'click', func: (e) => InternalEventHandler(e, this.trackEvent) }; parent.addEventListener(handler.name, handler.func); return handler; }, @@ -81,7 +81,7 @@ const InternalEvents = { loadEvents.forEach((element) => { const action = createInternalEventPayload(element); if (action) { - this.track_event(action); + this.trackEvent(action); } }); 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 f7f5ccdbf31..d99b90fa561 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 @@ -64,8 +64,8 @@ export default { const savedReply = this.savedReplies.find((r) => r.id === id); if (savedReply) { this.$emit('select', savedReply.content); - this.track_event(TRACKING_SAVED_REPLIES_USE); - this.track_event( + this.trackEvent(TRACKING_SAVED_REPLIES_USE); + this.trackEvent( isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER, ); } diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue index 38d8d239a7e..c0e87f0bb6e 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue @@ -69,6 +69,7 @@ export default { badge-tooltip-prop="name" :badge-sr-only-text="assigneesCollapsedTooltip" :class="assigneesContainerClass" + class="gl-white-space-nowrap" > <template #avatar="{ avatar }"> <gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name"> diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 9b22e4cebb2..d3986f31d52 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -63,6 +63,7 @@ gl-emoji { border-bottom-color: $blue-500; } -.emoji-picker .gl-dropdown-inner > :last-child { +.emoji-picker .gl-dropdown-contents > :last-child { padding-bottom: 0; + overflow-y: hidden; } diff --git a/app/controllers/groups/custom_emoji_controller.rb b/app/controllers/groups/custom_emoji_controller.rb new file mode 100644 index 00000000000..f202c9febba --- /dev/null +++ b/app/controllers/groups/custom_emoji_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Groups + class CustomEmojiController < Groups::ApplicationController + feature_category :code_review_workflow + urgency :low + + before_action do + render_404 unless Feature.enabled?(:custom_emoji) + end + end +end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index e552b01f7ba..de15d6cabc0 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -197,6 +197,14 @@ module GroupsHelper end end + def new_custom_emoji_path(group) + return unless Feature.enabled?(:custom_emoji) + return unless group + return unless can?(current_user, :create_custom_emoji, group) + + new_group_custom_emoji_path(group) + end + private def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 11a964128bc..05d8929c2a6 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -195,27 +195,6 @@ module ProjectsHelper { branch_name: tag.strong(truncate(sanitize(branch_name))), link_to_autodeploy_doc: link_to_autodeploy_doc } end - def project_list_cache_key(project, pipeline_status: true) - key = [ - project.star_count, - project.route.cache_key, - project.cache_key, - project.last_activity_date, - controller.controller_name, - controller.action_name, - Gitlab::CurrentSettings.cache_key, - "cross-project:#{can?(current_user, :read_cross_project)}", - max_project_member_access_cache_key(project), - pipeline_status, - Gitlab::I18n.locale, - 'v2.6' - ] - - key << pipeline_status_cache_key(project.pipeline_status) if pipeline_status && project.pipeline_status.has_status? - - key - end - def load_pipeline_status(projects) Gitlab::Cache::Ci::ProjectPipelineStatus .load_in_batch_for_projects(projects) @@ -371,18 +350,6 @@ module ProjectsHelper false end - def grafana_integration_url - @project.grafana_integration&.grafana_url - end - - def grafana_integration_masked_token - @project.grafana_integration&.masked_token - end - - def grafana_integration_enabled? - @project.grafana_integration&.enabled? - end - def project_license_name(project) key = "project:#{project.id}:license_name" @@ -477,10 +444,6 @@ module ProjectsHelper configure_oauth_import_message('Bitbucket', help_page_path("integration/bitbucket")) end - def import_from_gitlab_message - configure_oauth_import_message('GitLab.com', help_page_path("integration/gitlab")) - end - def show_inactive_project_deletion_banner?(project) return false unless project.present? && project.saved? return false unless delete_inactive_projects? @@ -672,30 +635,6 @@ module ProjectsHelper end end - def project_last_activity(project) - if project.last_activity_at - time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') - else - s_("ProjectLastActivity|Never") - end - end - - def project_status_css_class(status) - case status - when "started" - "table-active" - when "failed" - "table-danger" - when "finished" - "table-success" - end - end - - def readme_cache_key - sha = @project.commit.try(:sha) || 'nil' - [@project.full_path, sha, "readme"].join('-') - end - def current_ref @ref || @repository.try(:root_ref) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1ff003e94b9..bdaf2191e59 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1253,6 +1253,7 @@ class MergeRequest < ApplicationRecord [ ::MergeRequests::Mergeability::CheckOpenStatusService, ::MergeRequests::Mergeability::CheckDraftStatusService, + ::MergeRequests::Mergeability::CheckNotPreparingService, ::MergeRequests::Mergeability::CheckBrokenStatusService, ::MergeRequests::Mergeability::CheckDiscussionsStatusService, ::MergeRequests::Mergeability::CheckCiStatusService @@ -1554,7 +1555,7 @@ class MergeRequest < ApplicationRecord def mergeable_ci_state? return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true) - return false unless actual_head_pipeline + return true unless actual_head_pipeline return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped? actual_head_pipeline.success? diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index 89370bd8abb..5fc84e5aad7 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -40,11 +40,7 @@ module Commits Gitlab::Git::CommandError => ex Gitlab::ErrorTracking.log_exception(ex) - if Feature.enabled?(:errors_utf_8_encoding) - error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message)) - else - error(ex.message) - end + error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message)) end private diff --git a/app/services/merge_requests/mergeability/check_not_preparing_service.rb b/app/services/merge_requests/mergeability/check_not_preparing_service.rb new file mode 100644 index 00000000000..1ffa8117f97 --- /dev/null +++ b/app/services/merge_requests/mergeability/check_not_preparing_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module MergeRequests + module Mergeability + class CheckNotPreparingService < CheckBaseService + def execute + if !merge_request.preparing? + success + else + failure(reason: failure_reason) + end + end + + def skip? + false + end + + def cacheable? + false + end + + private + + def failure_reason + :preparing + end + end + end +end diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 5062599c261..254f05e3bcc 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,8 +1,9 @@ - api_awards_path = local_assigns.fetch(:api_awards_path, nil) +- new_custom_emoji_path = local_assigns.fetch(:new_custom_emoji_path, nil) - if api_awards_path .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between.gl-pt-3 - #js-vue-awards-block{ data: { path: api_awards_path, can_award_emoji: can?(current_user, :award_emoji, awardable).to_s } } + #js-vue-awards-block{ data: { path: api_awards_path, can_award_emoji: can?(current_user, :award_emoji, awardable).to_s, new_custom_emoji_path: new_custom_emoji_path } } = yield - else - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) diff --git a/app/views/groups/custom_emoji/index.html.haml b/app/views/groups/custom_emoji/index.html.haml new file mode 100644 index 00000000000..d8c28ac8959 --- /dev/null +++ b/app/views/groups/custom_emoji/index.html.haml @@ -0,0 +1,8 @@ +- page_title _('Custom emoji') + +#js-custom-emojis-root.row.gl-mt-5{ data: { base_path: group_custom_emoji_index_path(@group), group_path: @group.full_path } } + .col-12 + %h4.gl-mt-0 + = page_title + %p= _('Custom emoji will be available to use in every project in the group.') + = gl_loading_icon(size: 'lg') diff --git a/app/views/projects/blob/_viewer_switcher.html.haml b/app/views/projects/blob/_viewer_switcher.html.haml index 043b8629289..4eba82e6382 100644 --- a/app/views/projects/blob/_viewer_switcher.html.haml +++ b/app/views/projects/blob/_viewer_switcher.html.haml @@ -3,10 +3,10 @@ - rich_viewer = blob.rich_viewer .btn-group.js-blob-viewer-switcher.gl-ml-3{ role: "group" }> - - simple_label = "Display #{simple_viewer.switcher_title}" + - simple_label = format(_("Display %{viewer_type}"), viewer_type: simple_viewer.switcher_title) %button.btn.gl-button.btn-default.btn-icon.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => simple_label, title: simple_label, data: { viewer: 'simple', container: 'body' } }> = sprite_icon(simple_viewer.switcher_icon) - - rich_label = "Display #{rich_viewer.switcher_title}" + - rich_label = format(_("Display %{viewer_type}"), viewer_type: rich_viewer.switcher_title) %button.btn.gl-button.btn-default.btn-icon.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => rich_label, title: rich_label, data: { viewer: 'rich', container: 'body' } }> = sprite_icon(rich_viewer.switcher_icon) diff --git a/app/views/projects/issues/_details_content.html.haml b/app/views/projects/issues/_details_content.html.haml index 51ffb68f4e5..881e6863040 100644 --- a/app/views/projects/issues/_details_content.html.haml +++ b/app/views/projects/issues/_details_content.html.haml @@ -16,7 +16,7 @@ = edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') .js-issue-widgets - = render 'projects/issues/emoji_block', issuable: issuable, api_awards_path: api_awards_path + = render 'projects/issues/emoji_block', issuable: issuable, api_awards_path: api_awards_path, new_custom_emoji_path: new_custom_emoji_path(@project.group) .js-issue-widgets = render 'projects/issues/sentry_stack_trace', issuable: issuable diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index c6e5102889a..5cb7fa8816e 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -13,4 +13,5 @@ current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json, can_add_timeline_events: "#{can?(current_user, :admin_incident_management_timeline_event, @issue)}", report_abuse_path: add_category_abuse_reports_path, - new_comment_template_path: profile_comment_templates_path } } + new_comment_template_path: profile_comment_templates_path, + new_custom_emoji_path: new_custom_emoji_path(@project.group) } } diff --git a/app/views/projects/issues/_emoji_block.html.haml b/app/views/projects/issues/_emoji_block.html.haml index 7eb3c0f5c9f..f9eee9ec99e 100644 --- a/app/views/projects/issues/_emoji_block.html.haml +++ b/app/views/projects/issues/_emoji_block.html.haml @@ -1,8 +1,9 @@ - api_awards_path = local_assigns.fetch(:api_awards_path, nil) +- new_custom_emoji_path = local_assigns.fetch(:new_custom_emoji_path, nil) .emoji-block.emoji-block-sticky .row.gl-m-0.gl-justify-content-space-between .js-noteable-awards - = render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path + = render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path, new_custom_emoji_path: new_custom_emoji_path .new-branch-col.gl-font-size-0.gl-my-2 = render 'new_branch' if show_new_branch_button? diff --git a/app/views/projects/merge_requests/_awards_block.html.haml b/app/views/projects/merge_requests/_awards_block.html.haml index c1952793e72..f657f683a6d 100644 --- a/app/views/projects/merge_requests/_awards_block.html.haml +++ b/app/views/projects/merge_requests/_awards_block.html.haml @@ -1,2 +1,2 @@ .emoji-block.emoji-list-container.js-noteable-awards - = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request) + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request), new_custom_emoji_path: new_custom_emoji_path(@project.group) diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml index dfb18b52021..f99396e4155 100644 --- a/app/views/projects/merge_requests/_page.html.haml +++ b/app/views/projects/merge_requests/_page.html.haml @@ -83,7 +83,8 @@ current_user_data: @current_user_data, is_locked: @merge_request.discussion_locked.to_s, report_abuse_path: add_category_abuse_reports_path, - new_comment_template_path: profile_comment_templates_path } } + new_comment_template_path: profile_comment_templates_path, + new_custom_emoji_path: new_custom_emoji_path(@project.group) } } - if moved_mr_sidebar_enabled? = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 58e86ebffa0..7ff798d7324 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -6,6 +6,6 @@ #js-snippet-view{ data: { 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } } .gl-px-0.gl-py-2 - = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet) + = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet), new_custom_emoji_path: new_custom_emoji_path(@project.group) #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index ac5e65747d5..75151da8071 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -45,7 +45,7 @@ - if !explore_projects_tab? && access&.nonzero? -# haml-lint:disable UnnecessaryStringOutput = ' ' # prevent haml from eating the space between elements - %span.user-access-role.gl-display-block.gl-m-0{ data: { qa_selector: 'user_role_content' } }= Gitlab::Access.human_access(access) + %span.user-access-role.gl-display-block.gl-m-0{ data: { qa_selector: 'user_role_content' } }= localized_project_human_access(access) - if !explore_projects_tab? = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-3!' diff --git a/config/feature_flags/development/errors_utf_8_encoding.yml b/config/feature_flags/development/errors_utf_8_encoding.yml deleted file mode 100644 index 50ecd0ad664..00000000000 --- a/config/feature_flags/development/errors_utf_8_encoding.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: errors_utf_8_encoding -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129217 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422061 -milestone: '16.4' -type: development -group: group::source code -default_enabled: true diff --git a/config/routes/group.rb b/config/routes/group.rb index 87e885e59a2..6cad7f9093b 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -78,6 +78,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do post :toggle_subscription, on: :member end + resources :custom_emoji, only: [:index, :new], action: :index + resources :packages, only: [:index, :show] resources :milestones, constraints: { id: %r{[^/]+} } do diff --git a/doc/development/internal_analytics/internal_event_tracking/migration.md b/doc/development/internal_analytics/internal_event_tracking/migration.md index 4b8a726768f..7e6734a5bfd 100644 --- a/doc/development/internal_analytics/internal_event_tracking/migration.md +++ b/doc/development/internal_analytics/internal_event_tracking/migration.md @@ -71,7 +71,7 @@ import { InternalEvents } from '~/tracking'; mixins: [InternalEvents.mixin()] ... ... -this.track_event('action') +this.trackEvent('action') ``` You can use [this MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123901/diffs) as an example. It migrates the `devops_adoption_app` component to use Internal Events Tracking. diff --git a/doc/development/internal_analytics/internal_event_tracking/quick_start.md b/doc/development/internal_analytics/internal_event_tracking/quick_start.md index f44ac0c7286..0540094aa5e 100644 --- a/doc/development/internal_analytics/internal_event_tracking/quick_start.md +++ b/doc/development/internal_analytics/internal_event_tracking/quick_start.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Quick start for Internal Event Tracking -In an effort to provide a more efficient, scalable, and unified tracking API, GitLab is deprecating existing RedisHLL and Snowplow tracking. Instead, we're implementing a new `track_event` method. +In an effort to provide a more efficient, scalable, and unified tracking API, GitLab is deprecating existing RedisHLL and Snowplow tracking. Instead, we're implementing a new `track_event` (Backend) and `trackEvent`(Frontend) method. With this approach, we can update both RedisHLL counters and send Snowplow events without worrying about the underlying implementation. In order to instrument your code with Internal Events Tracking need three things: @@ -79,13 +79,13 @@ To implement Vue component tracking: }; ``` -1. Call the `track_event` method. Tracking options can be passed as the second parameter: +1. Call the `trackEvent` method. Tracking options can be passed as the second parameter: ```javascript - this.track_event('i_code_review_user_apply_suggestion'); + this.trackEvent('i_code_review_user_apply_suggestion'); ``` - Or use the `track_event` method in the template: + Or use the `trackEvent` method in the template: ```html <template> @@ -94,7 +94,7 @@ To implement Vue component tracking: <div v-if="expanded"> <p>Hello world!</p> - <button @click="track_event('i_code_review_user_apply_suggestion')">Track another event</button> + <button @click="trackEvent('i_code_review_user_apply_suggestion')">Track another event</button> </div> </div> </template> @@ -106,7 +106,7 @@ For tracking events directly from arbitrary frontend JavaScript code, a module f ```javascript import { InternalEvents } from '~/tracking'; -InternalEvents.track_event('i_code_review_user_apply_suggestion'); +InternalEvents.trackEvent('i_code_review_user_apply_suggestion'); ``` #### Data-track attribute diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 8679f17eb9b..2e4f68285c9 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -95,12 +95,11 @@ module Gitlab end def self.use_cluster_shared_state? - Gitlab::SafeRequestStore[:use_cluster_shared_state] ||= - Feature.enabled?(:use_cluster_shared_state_for_exclusive_lease) + Feature.enabled?(:use_cluster_shared_state_for_exclusive_lease, Feature.current_request) end def self.use_double_lock? - Gitlab::SafeRequestStore[:use_double_lock] ||= Feature.enabled?(:enable_exclusive_lease_double_lock_rw) + Feature.enabled?(:enable_exclusive_lease_double_lock_rw, Feature.current_request) end def initialize(key, uuid: nil, timeout:) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c04f7490dab..c56cdb6389a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13926,6 +13926,9 @@ msgstr "" msgid "Create new directory" msgstr "" +msgid "Create new emoji" +msgstr "" + msgid "Create new file" msgstr "" @@ -14436,7 +14439,7 @@ msgstr "" msgid "Custom emoji" msgstr "" -msgid "Custom emoji will be available to use in every project in group." +msgid "Custom emoji will be available to use in every project in the group." msgstr "" msgid "Custom hostname (for private commit emails)" @@ -17292,6 +17295,9 @@ msgstr[1] "" msgid "Display" msgstr "" +msgid "Display %{viewer_type}" +msgstr "" + msgid "Display alerts from all configured monitoring tools." msgstr "" @@ -36648,9 +36654,6 @@ msgstr "" msgid "ProjectFileTree|Show more" msgstr "" -msgid "ProjectLastActivity|Never" -msgstr "" - msgid "ProjectList|Starred" msgstr "" diff --git a/spec/features/merge_request/user_creates_custom_emoji_spec.rb b/spec/features/merge_request/user_creates_custom_emoji_spec.rb new file mode 100644 index 00000000000..35593836dab --- /dev/null +++ b/spec/features/merge_request/user_creates_custom_emoji_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge request > User creates custom emoji', :js, feature_category: :code_review_workflow do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository, namespace: group) } + let_it_be(:merge_request) { create(:merge_request, source_project: project, author: user) } + + context 'with user who has permissions' do + before_all do + group.add_owner(user) + end + + before do + sign_in(user) + + visit project_merge_request_path(project, merge_request) + wait_for_requests + end + + it 'shows link to create custom emoji' do + first('.add-reaction-button').click + + wait_for_requests + + click_link 'Create new emoji' + + wait_for_requests + + find_by_testid("custom-emoji-name-input").set 'parrot' + find_by_testid("custom-emoji-url-input").set 'https://example.com' + + click_button 'Save' + + wait_for_requests + + expect(page).to have_content(':parrot:') + end + end + + context 'with user who does not have permissions' do + before do + sign_in(user) + + visit project_merge_request_path(project, merge_request) + wait_for_requests + end + + it 'shows link to create custom emoji' do + first('.add-reaction-button').click + + wait_for_requests + + expect(page).not_to have_link('Create new emoji') + end + end +end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 96cad397441..73cd0d699c9 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -335,7 +335,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category: # Wait for the `ci_status` and `merge_check` requests wait_for_requests - expect(page).not_to have_selector('.accept-merge-request') + expect(page).to have_selector('.accept-merge-request') end end diff --git a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js index e3d5c448338..b4d22c0d29a 100644 --- a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js +++ b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import CollapsibleSection from '~/ci/job_details/components/log/collapsible_section.vue'; +import LogLine from '~/ci/job_details/components/log/line.vue'; import LogLineHeader from '~/ci/job_details/components/log/line_header.vue'; import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data'; @@ -9,9 +10,9 @@ describe('Job Log Collapsible Section', () => { const jobLogEndpoint = 'jobs/335'; - const findCollapsibleLine = () => wrapper.find('.collapsible-line'); - const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg'); const findLogLineHeader = () => wrapper.findComponent(LogLineHeader); + const findLogLineHeaderSvg = () => findLogLineHeader().find('svg'); + const findLogLines = () => wrapper.findAllComponents(LogLine); const createComponent = (props = {}) => { wrapper = mount(CollapsibleSection, { @@ -30,11 +31,16 @@ describe('Job Log Collapsible Section', () => { }); it('renders clickable header line', () => { - expect(findCollapsibleLine().attributes('role')).toBe('button'); + expect(findLogLineHeader().text()).toBe('2 foo'); + expect(findLogLineHeader().attributes('role')).toBe('button'); }); - it('renders an icon with the closed state', () => { - expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-right-icon'); + it('renders an icon with a closed state', () => { + expect(findLogLineHeaderSvg().attributes('data-testid')).toBe('chevron-lg-right-icon'); + }); + + it('does not render collapsed lines', () => { + expect(findLogLines()).toHaveLength(0); }); }); @@ -47,15 +53,17 @@ describe('Job Log Collapsible Section', () => { }); it('renders clickable header line', () => { - expect(findCollapsibleLine().attributes('role')).toBe('button'); + expect(findLogLineHeader().text()).toContain('foo'); + expect(findLogLineHeader().attributes('role')).toBe('button'); }); it('renders an icon with the open state', () => { - expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-down-icon'); + expect(findLogLineHeaderSvg().attributes('data-testid')).toBe('chevron-lg-down-icon'); }); - it('renders collapsible lines content', () => { - expect(wrapper.findAll('.js-line').length).toEqual(collapsibleSectionOpened.lines.length); + it('renders collapsible lines', () => { + expect(findLogLines().at(0).text()).toContain('this is a collapsible nested section'); + expect(findLogLines()).toHaveLength(collapsibleSectionOpened.lines.length); }); }); @@ -65,7 +73,7 @@ describe('Job Log Collapsible Section', () => { jobLogEndpoint, }); - findCollapsibleLine().trigger('click'); + findLogLineHeader().trigger('click'); await nextTick(); expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 3c461f2b382..83779f191f3 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -4,6 +4,7 @@ import batchComments from '~/batch_comments/stores/modules/batch_comments'; import NoteForm from '~/notes/components/note_form.vue'; import createStore from '~/notes/stores'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import CommentFieldLayout from '~/notes/components/comment_field_layout.vue'; import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete'; import eventHub from '~/environments/event_hub'; import { mountExtended } from 'helpers/vue_test_utils_helper'; @@ -239,6 +240,21 @@ describe('issue_note_form component', () => { property: 'Issue_note', }); }); + + describe('when discussion is confidential', () => { + beforeEach(() => { + createComponentWrapper({ + discussion: { + ...discussionMock, + confidential: true, + }, + }); + }); + + it('passes correct confidentiality to CommentFieldLayout', () => { + expect(wrapper.findComponent(CommentFieldLayout).props('isInternalNote')).toBe(true); + }); + }); }); }); diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js index b291eba61f5..67c0ba90d40 100644 --- a/spec/frontend/notes/mock_data.js +++ b/spec/frontend/notes/mock_data.js @@ -321,6 +321,7 @@ export const discussionMock = { individual_note: false, resolvable: true, active: true, + confidential: false, }; export const loggedOutnoteableData = { diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js index 6e773fde4db..d1ec70303cd 100644 --- a/spec/frontend/tracking/internal_events_spec.js +++ b/spec/frontend/tracking/internal_events_spec.js @@ -26,18 +26,18 @@ Tracker.enabled = jest.fn(); const event = 'TestEvent'; describe('InternalEvents', () => { - describe('track_event', () => { - it('track_event calls API.trackInternalEvent with correct arguments', () => { - InternalEvents.track_event(event); + describe('trackEvent', () => { + it('trackEvent calls API.trackInternalEvent with correct arguments', () => { + InternalEvents.trackEvent(event); expect(API.trackInternalEvent).toHaveBeenCalledTimes(1); expect(API.trackInternalEvent).toHaveBeenCalledWith(event); }); - it('track_event calls tracking.event functions with correct arguments', () => { + it('trackEvent calls tracking.event functions with correct arguments', () => { const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn); - InternalEvents.track_event(event, { context: extraContext }); + InternalEvents.trackEvent(event, { context: extraContext }); expect(trackingSpy).toHaveBeenCalledTimes(1); expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, { @@ -66,10 +66,10 @@ describe('InternalEvents', () => { `, methods: { handleButton1Click() { - this.track_event(event); + this.trackEvent(event); }, handleButton2Click() { - this.track_event(event, extraContext); + this.trackEvent(event, extraContext); }, }, mixins: [InternalEvents.mixin()], @@ -79,8 +79,8 @@ describe('InternalEvents', () => { wrapper = shallowMountExtended(Component); }); - it('this.track_event function calls InternalEvent`s track function with an event', async () => { - const trackEventSpy = jest.spyOn(InternalEvents, 'track_event'); + it('this.trackEvent function calls InternalEvent`s track function with an event', async () => { + const trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent'); await wrapper.findByTestId('button1').trigger('click'); @@ -88,9 +88,9 @@ describe('InternalEvents', () => { expect(trackEventSpy).toHaveBeenCalledWith(event, {}); }); - it("this.track_event function calls InternalEvent's track function with an event and data", async () => { + it("this.trackEvent function calls InternalEvent's track function with an event and data", async () => { const data = extraContext; - const trackEventSpy = jest.spyOn(InternalEvents, 'track_event'); + const trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent'); await wrapper.findByTestId('button2').trigger('click'); @@ -147,7 +147,7 @@ describe('InternalEvents', () => { describe('tracking', () => { let trackEventSpy; beforeEach(() => { - trackEventSpy = jest.spyOn(InternalEvents, 'track_event'); + trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent'); }); it('should track event if action exists', () => { diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 1b5f23a5e8e..2cdb934eb0a 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -555,4 +555,44 @@ RSpec.describe GroupsHelper do it { is_expected.to contain_exactly([_("Only HTTP(S)"), "http"]) } end end + + describe '#new_custom_emoji_path' do + subject { helper.new_custom_emoji_path(group) } + + let_it_be(:group) { create(:group) } + + context 'with feature flag disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it { is_expected.to eq(nil) } + end + + context 'with feature flag enabled' do + context 'with nil group' do + let(:group) { nil } + + it { is_expected.to eq(nil) } + end + + context 'with current_user who has no permissions' do + before do + allow(helper).to receive(:current_user).and_return(create(:user)) + end + + it { is_expected.to eq(nil) } + end + + context 'with current_user who has permissions' do + before do + user = create(:user) + group.add_owner(user) + allow(helper).to receive(:current_user).and_return(user) + end + + it { is_expected.to eq(new_group_custom_emoji_path(group)) } + end + end + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index f415140b45d..90d998e17c3 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -77,14 +77,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end end - describe "#project_status_css_class" do - it "returns appropriate class" do - expect(project_status_css_class("started")).to eq("table-active") - expect(project_status_css_class("failed")).to eq("table-danger") - expect(project_status_css_class("finished")).to eq("table-success") - end - end - describe "can_change_visibility_level?" do let_it_be(:user) { create(:project_member, :reporter, user: create(:user), project: project).user } @@ -126,82 +118,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end end - describe "readme_cache_key" do - let(:project) { project_with_repo } - - it "returns a valid cach key" do - expect(helper.send(:readme_cache_key)).to eq("#{project.full_path}-#{project.commit.id}-readme") - end - - it "returns a valid cache key if HEAD does not exist" do - allow(project).to receive(:commit) { nil } - - expect(helper.send(:readme_cache_key)).to eq("#{project.full_path}-nil-readme") - end - end - - describe "#project_list_cache_key", :clean_gitlab_redis_cache do - let(:project) { project_with_repo } - - before do - allow(helper).to receive(:can?).with(user, :read_cross_project) { true } - allow(user).to receive(:max_member_access_for_project).and_return(40) - allow(Gitlab::I18n).to receive(:locale).and_return('es') - end - - it "includes the route" do - expect(helper.project_list_cache_key(project)).to include(project.route.cache_key) - end - - it "includes the project" do - expect(helper.project_list_cache_key(project)).to include(project.cache_key) - end - - it "includes the last activity date" do - expect(helper.project_list_cache_key(project)).to include(project.last_activity_date) - end - - it "includes the controller name" do - expect(helper.controller).to receive(:controller_name).and_return("testcontroller") - - expect(helper.project_list_cache_key(project)).to include("testcontroller") - end - - it "includes the controller action" do - expect(helper.controller).to receive(:action_name).and_return("testaction") - - expect(helper.project_list_cache_key(project)).to include("testaction") - end - - it "includes the application settings" do - settings = Gitlab::CurrentSettings.current_application_settings - - expect(helper.project_list_cache_key(project)).to include(settings.cache_key) - end - - it "includes a version" do - expect(helper.project_list_cache_key(project).last).to start_with('v') - end - - it 'includes whether or not the user can read cross project' do - expect(helper.project_list_cache_key(project)).to include('cross-project:true') - end - - it "includes the pipeline status when there is a status" do - create(:ci_pipeline, :success, project: project, sha: project.commit.sha) - - expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success") - end - - it "includes the user locale" do - expect(helper.project_list_cache_key(project)).to include('es') - end - - it "includes the user max member access" do - expect(helper.project_list_cache_key(project)).to include('access:40') - end - end - describe '#load_pipeline_status' do it 'loads the pipeline status in batch' do helper.load_pipeline_status([project]) @@ -804,42 +720,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end end - describe '#grafana_integration_url' do - subject { helper.grafana_integration_url } - - it { is_expected.to eq(nil) } - - context 'grafana integration exists' do - let!(:grafana_integration) { create(:grafana_integration, project: project) } - - it { is_expected.to eq(grafana_integration.grafana_url) } - end - end - - describe '#grafana_integration_token' do - subject { helper.grafana_integration_masked_token } - - it { is_expected.to eq(nil) } - - context 'grafana integration exists' do - let!(:grafana_integration) { create(:grafana_integration, project: project) } - - it { is_expected.to eq(grafana_integration.masked_token) } - end - end - - describe '#grafana_integration_enabled?' do - subject { helper.grafana_integration_enabled? } - - it { is_expected.to eq(nil) } - - context 'grafana integration exists' do - let!(:grafana_integration) { create(:grafana_integration, project: project) } - - it { is_expected.to eq(grafana_integration.enabled) } - end - end - describe '#project_license_name(project)', :request_store do let_it_be(:repository) { project.repository } @@ -1195,14 +1075,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do it_behaves_like 'configure import method modal' end - describe '#import_from_gitlab_message' do - let(:import_method) { 'GitLab.com' } - - subject { helper.import_from_gitlab_message } - - it_behaves_like 'configure import method modal' - end - describe '#show_inactive_project_deletion_banner?' do shared_examples 'does not show the banner' do |pass_project: true| it { expect(helper.show_inactive_project_deletion_banner?(pass_project ? project : nil)).to be(false) } diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index c8325c5b359..2230cb9b90f 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -347,7 +347,6 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share # simulates transition stub_feature_flags({ flag => true }) - Gitlab::SafeRequestStore.clear! expect(lease.ttl).not_to eq(nil) expect(lease.exists?).to be_truthy @@ -362,12 +361,11 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share # simulates transition stub_feature_flags({ flag => true }) - Gitlab::SafeRequestStore.clear! expect(lease.renew).to be_truthy end - it 'retains renew behaviour' do + it 'retains cancel behaviour' do lease = described_class.new(unique_key, timeout: 3600) uuid = lease.try_obtain lease.cancel @@ -377,7 +375,6 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share # simulates transition stub_feature_flags({ flag => true }) - Gitlab::SafeRequestStore.clear! expect(lease.try_obtain).to be_falsey lease.cancel @@ -404,4 +401,189 @@ RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_share it_behaves_like 'retains behaviours across transitions', :use_cluster_shared_state_for_exclusive_lease end end + + describe 'using current_request actor' do + shared_context 'when double lock is enabled for the current request' do + before do + stub_feature_flags( + enable_exclusive_lease_double_lock_rw: Feature.current_request, + use_cluster_shared_state_for_exclusive_lease: false + ) + end + end + + shared_context 'when cutting over to ClusterSharedState for the current request' do + before do + stub_feature_flags( + enable_exclusive_lease_double_lock_rw: true, + use_cluster_shared_state_for_exclusive_lease: Feature.current_request + ) + end + end + + describe '#try_obtain' do + let(:lease) { described_class.new(unique_key, timeout: 3600) } + + shared_examples 'acquires both locks' do + it do + Gitlab::Redis::SharedState.with { |r| expect(r).to receive(:set).and_call_original } + Gitlab::Redis::ClusterSharedState.with { |r| expect(r).to receive(:set).and_call_original } + + expect(lease.try_obtain).to be_truthy + end + end + + shared_examples 'only acquires one lock' do + it do + used_store.with { |r| expect(r).to receive(:set).and_call_original } + unused_store.with { |r| expect(r).not_to receive(:set) } + + expect(lease.try_obtain).to be_truthy + end + end + + context 'when double lock is enabled for the current request' do + include_context 'when double lock is enabled for the current request' + it_behaves_like 'acquires both locks' + + context 'for a different request' do + before do + stub_with_new_feature_current_request + end + + let(:used_store) { Gitlab::Redis::SharedState } + let(:unused_store) { Gitlab::Redis::ClusterSharedState } + + it_behaves_like 'only acquires one lock' + end + end + + context 'when cutting over to ClusterSharedState for the current request' do + include_context 'when cutting over to ClusterSharedState for the current request' + + let(:used_store) { Gitlab::Redis::ClusterSharedState } + let(:unused_store) { Gitlab::Redis::SharedState } + + it_behaves_like 'only acquires one lock' + + context 'for a different request' do + before do + stub_with_new_feature_current_request + end + + it_behaves_like 'acquires both locks' + end + end + end + + describe '.with_write_redis' do + shared_examples 'writes to both stores in order' do + it do + first_store.with { |r| expect(r).to receive(:eval).ordered } + second_store.with { |r| expect(r).to receive(:eval).ordered } + + described_class.with_write_redis { |r| r.eval(described_class::LUA_CANCEL_SCRIPT) } + end + end + + shared_examples 'only writes to one store' do + it do + used_store.with { |r| expect(r).to receive(:eval) } + unused_store.with { |r| expect(r).not_to receive(:eval) } + + described_class.with_write_redis { |r| r.eval(described_class::LUA_CANCEL_SCRIPT) } + end + end + + context 'when double lock is enabled for the current request' do + include_context 'when double lock is enabled for the current request' + let(:first_store) { Gitlab::Redis::SharedState } + let(:second_store) { Gitlab::Redis::ClusterSharedState } + + it_behaves_like 'writes to both stores in order' + + context 'for a different request' do + before do + stub_with_new_feature_current_request + end + + let(:used_store) { Gitlab::Redis::SharedState } + let(:unused_store) { Gitlab::Redis::ClusterSharedState } + + it_behaves_like 'only writes to one store' + end + end + + context 'when cutting over to ClusterSharedState for the current request' do + include_context 'when cutting over to ClusterSharedState for the current request' + let(:first_store) { Gitlab::Redis::ClusterSharedState } + let(:second_store) { Gitlab::Redis::SharedState } + + it_behaves_like 'writes to both stores in order' + + context 'for a different request' do + before do + stub_with_new_feature_current_request + end + + let(:first_store) { Gitlab::Redis::SharedState } + let(:second_store) { Gitlab::Redis::ClusterSharedState } + + it_behaves_like 'writes to both stores in order' + end + end + end + + describe '.with_read_redis' do + shared_examples 'reads from both stores' do + it do + Gitlab::Redis::SharedState.with { |r| expect(r).to receive(:get) } + Gitlab::Redis::ClusterSharedState.with { |r| expect(r).to receive(:get) } + + described_class.with_read_redis { |r| r.get(described_class.redis_shared_state_key("foobar")) } + end + end + + shared_examples 'only reads from one store' do + it do + used_store.with { |r| expect(r).to receive(:get) } + unused_store.with { |r| expect(r).not_to receive(:get) } + + described_class.with_read_redis { |r| r.get(described_class.redis_shared_state_key("foobar")) } + end + end + + context 'when double lock is enabled for the current request' do + include_context 'when double lock is enabled for the current request' + it_behaves_like 'reads from both stores' + + context 'for a different request' do + before do + stub_with_new_feature_current_request + end + + let(:used_store) { Gitlab::Redis::SharedState } + let(:unused_store) { Gitlab::Redis::ClusterSharedState } + + it_behaves_like 'only reads from one store' + end + end + + context 'when cutting over to ClusterSharedState for the current request' do + include_context 'when cutting over to ClusterSharedState for the current request' + let(:used_store) { Gitlab::Redis::ClusterSharedState } + let(:unused_store) { Gitlab::Redis::SharedState } + + it_behaves_like 'only reads from one store' + + context 'for a different request' do + before do + stub_with_new_feature_current_request + end + + it_behaves_like 'reads from both stores' + end + end + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 53b7bcd4d7e..7d96926fae8 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3662,7 +3662,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev allow(subject).to receive(:head_pipeline).and_return(nil) end - it { expect(subject.mergeable_ci_state?).to be_falsey } + it { expect(subject.mergeable_ci_state?).to be_truthy } end end @@ -3703,7 +3703,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev allow(subject).to receive(:head_pipeline).and_return(nil) end - it { expect(subject.mergeable_ci_state?).to be_falsey } + it { expect(subject.mergeable_ci_state?).to be_truthy } end end diff --git a/spec/requests/groups/custom_emoji_controller_spec.rb b/spec/requests/groups/custom_emoji_controller_spec.rb new file mode 100644 index 00000000000..d12cd8e42ac --- /dev/null +++ b/spec/requests/groups/custom_emoji_controller_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::CustomEmojiController, feature_category: :code_review_workflow do + let_it_be(:group) { create(:group) } + + describe 'GET #index' do + context 'with custom_emoji feature flag disabled' do + before do + stub_feature_flags(custom_emoji: false) + + get group_custom_emoji_index_url(group) + end + + it { expect(response).to have_gitlab_http_status(:not_found) } + end + + context 'with custom_emoji feature flag enabled' do + before do + get group_custom_emoji_index_url(group) + end + + it { expect(response).to have_gitlab_http_status(:ok) } + end + end +end diff --git a/spec/services/merge_requests/mergeability/check_not_preparing_service_spec.rb b/spec/services/merge_requests/mergeability/check_not_preparing_service_spec.rb new file mode 100644 index 00000000000..04db96e95ea --- /dev/null +++ b/spec/services/merge_requests/mergeability/check_not_preparing_service_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::CheckNotPreparingService, feature_category: :code_review_workflow do + let(:service) { described_class.new(merge_request: merge_request, params: {}) } + let(:merge_request) { build(:merge_request, merge_status: merge_status) } + let(:merge_status_value) { MergeRequest.state_machines[:merge_status].states[merge_status].value } + let(:merge_status) { :unchecked } + + describe '#execute' do + subject(:result) { service.execute } + + it 'is success when not preparing' do + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + end + + context 'when the merge request is preparing' do + let(:merge_status) { :preparing } + + specify :aggregate_failures do + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:reason]).to eq(:preparing) + end + end + end + + describe '#skip?' do + subject { service.skip? } + + it { is_expected.to eq false } + end + + describe '#cacheable?' do + subject { service.cacheable? } + + it { is_expected.to eq false } + end +end diff --git a/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb b/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb index 66bcb948cb6..632881dc361 100644 --- a/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb +++ b/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb @@ -26,10 +26,10 @@ RSpec.describe ::MergeRequests::Mergeability::DetailedMergeStatusService, featur context 'when merge status is preparing and merge request diff is persisted' do let(:merge_request) { create(:merge_request, merge_status: :preparing) } - it 'returns :checking' do + it 'returns :preparing' do allow(merge_request.merge_request_diff).to receive(:persisted?).and_return(true) - expect(detailed_merge_status).to eq(:mergeable) + expect(detailed_merge_status).to eq(:preparing) end end @@ -69,7 +69,7 @@ RSpec.describe ::MergeRequests::Mergeability::DetailedMergeStatusService, featur end end - context 'when all but the ci check fails' do + context 'when ci check is required' do let(:merge_request) { create(:merge_request) } before do @@ -78,7 +78,7 @@ RSpec.describe ::MergeRequests::Mergeability::DetailedMergeStatusService, featur context 'when pipeline does not exist' do it 'returns the failure reason' do - expect(detailed_merge_status).to eq(:ci_must_pass) + expect(detailed_merge_status).to eq(:mergeable) end end |