Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
46f9d8c359
commit
41310fed8a
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export default () => {
|
|||
reportAbusePath: notesDataset.reportAbusePath,
|
||||
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
|
||||
mrFilter: true,
|
||||
newCustomEmojiPath: notesDataset.newCustomEmojiPath,
|
||||
},
|
||||
data() {
|
||||
const noteableData = JSON.parse(notesDataset.noteableData);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { initCustomEmojis } from '~/custom_emoji/custom_emoji_bundle';
|
||||
|
||||
requestIdleCallback(initCustomEmojis);
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) } }
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ export const discussionMock = {
|
|||
individual_note: false,
|
||||
resolvable: true,
|
||||
active: true,
|
||||
confidential: false,
|
||||
};
|
||||
|
||||
export const loggedOutnoteableData = {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue