Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-09-29 03:13:40 +00:00
parent 46f9d8c359
commit 41310fed8a
51 changed files with 542 additions and 280 deletions

View File

@ -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));

View File

@ -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, {

View File

@ -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"

View File

@ -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');
}
},
})

View File

@ -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>

View File

@ -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']),
},

View File

@ -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>

View File

@ -44,6 +44,7 @@ export default () => {
reportAbusePath: notesDataset.reportAbusePath,
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
mrFilter: true,
newCustomEmojiPath: notesDataset.newCustomEmojiPath,
},
data() {
const noteableData = JSON.parse(notesDataset.noteableData);

View File

@ -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"

View File

@ -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 {

View File

@ -0,0 +1,3 @@
import { initCustomEmojis } from '~/custom_emoji/custom_emoji_bundle';
requestIdleCallback(initCustomEmojis);

View File

@ -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');
}
});
}

View File

@ -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);
}
});

View File

@ -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,
);
}

View File

@ -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">

View File

@ -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;
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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) } }

View File

@ -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?

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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!'

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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:)

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);
});
});
});
});

View File

@ -321,6 +321,7 @@ export const discussionMock = {
individual_note: false,
resolvable: true,
active: true,
confidential: false,
};
export const loggedOutnoteableData = {

View File

@ -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', () => {

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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