Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-11-19 15:27:48 +00:00
parent 2bae6e5b96
commit 23b5f76606
74 changed files with 838 additions and 397 deletions

View File

@ -378,7 +378,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/lib/gitlab/code_owners/entry.rb'
- 'ee/lib/gitlab/code_owners/loader.rb'
- 'ee/lib/gitlab/custom_file_templates.rb'
- 'ee/lib/gitlab/elastic/document_reference.rb'
- 'ee/lib/gitlab/elastic/indexer.rb'
- 'ee/lib/gitlab/elastic/project_search_results.rb'
- 'ee/lib/gitlab/expiring_subscription_message.rb'

View File

@ -940,7 +940,6 @@ Layout/LineLength:
- 'ee/lib/ee/sidebars/projects/menus/security_compliance_menu.rb'
- 'ee/lib/elastic/latest/custom_language_analyzers.rb'
- 'ee/lib/elastic/latest/git_instance_proxy.rb'
- 'ee/lib/elastic/latest/issue_instance_proxy.rb'
- 'ee/lib/elastic/latest/note_class_proxy.rb'
- 'ee/lib/elastic/latest/repository_class_proxy.rb'
- 'ee/lib/elastic/latest/repository_instance_proxy.rb'
@ -1434,7 +1433,6 @@ Layout/LineLength:
- 'ee/spec/models/concerns/ee/noteable_spec.rb'
- 'ee/spec/models/concerns/ee/project_security_scanners_information_spec.rb'
- 'ee/spec/models/concerns/elastic/application_versioned_search_spec.rb'
- 'ee/spec/models/concerns/elastic/issue_spec.rb'
- 'ee/spec/models/concerns/elastic/merge_request_spec.rb'
- 'ee/spec/models/concerns/elastic/note_spec.rb'
- 'ee/spec/models/concerns/elastic/repository_spec.rb'

View File

@ -5,7 +5,6 @@ Lint/RedundantCopDisableDirective:
# Temporarily disabled due to too many offenses
Enabled: false
Exclude:
- 'app/controllers/concerns/enforces_two_factor_authentication.rb'
- 'app/controllers/groups/milestones_controller.rb'
- 'app/models/concerns/integrations/base/integration.rb'
- 'app/models/work_items/type.rb'
@ -13,15 +12,12 @@ Lint/RedundantCopDisableDirective:
- 'app/services/ci/runners/set_runner_associated_projects_service.rb'
- 'app/services/work_items/data_sync/clone_service.rb'
- 'app/services/work_items/data_sync/move_service.rb'
- 'app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb'
- 'config/initializers/warden.rb'
- 'db/migrate/20241021082113_create_partitioned_ci_runners.rb'
- 'db/migrate/20241024204816_create_partitioned_ci_runner_managers.rb'
- 'db/post_migrate/20241021163518_drop_user_canonical_emails_table.rb'
- 'ee/app/controllers/ee/projects/settings/ci_cd_controller.rb'
- 'ee/app/models/ee/audit_events/project_audit_event.rb'
- 'ee/app/services/search/zoekt/routing_service.rb'
- 'ee/app/services/security/token_revocation_service.rb'
- 'ee/app/services/vulnerabilities/statistics/adjustment_service.rb'
- 'ee/db/geo/migrate/20210504143244_add_verification_to_merge_request_diff_registry.rb'
- 'ee/spec/features/groups/settings/domain_verification_spec.rb'
@ -33,9 +29,6 @@ Lint/RedundantCopDisableDirective:
- 'ee/spec/support/shared_examples/features/dashboard_saml_reauth_banner_shared_examples.rb'
- 'ee/spec/views/admin/users/show.html.haml_spec.rb'
- 'lib/api/submodules.rb'
- 'lib/gitlab/auth/current_user_mode.rb'
- 'lib/gitlab/cleanup/personal_access_tokens.rb'
- 'lib/gitlab/lfs_token.rb'
- 'lib/gitlab/popen/runner.rb'
- 'qa/qa/resource/user_runners.rb'
- 'qa/qa/service/docker_run/gitlab_runner.rb'

View File

@ -246,7 +246,6 @@ Lint/UnusedMethodArgument:
- 'ee/lib/ee/gitlab/auth/ldap/sync/proxy.rb'
- 'ee/lib/ee/gitlab/geo_git_access.rb'
- 'ee/lib/elastic/as_json.rb'
- 'ee/lib/elastic/latest/issue_instance_proxy.rb'
- 'ee/lib/elastic/latest/milestone_instance_proxy.rb'
- 'ee/lib/elastic/latest/note_class_proxy.rb'
- 'ee/lib/elastic/latest/note_instance_proxy.rb'

View File

@ -207,7 +207,6 @@ Style/RedundantSelf:
- 'ee/lib/gitlab/auth/smartcard.rb'
- 'ee/lib/gitlab/ci/reports/license_scanning/report.rb'
- 'ee/lib/gitlab/elastic/client.rb'
- 'ee/lib/gitlab/elastic/document_reference.rb'
- 'ee/lib/gitlab/elastic/helper.rb'
- 'ee/lib/gitlab/geo.rb'
- 'ee/lib/gitlab/geo/oauth/login_state.rb'

View File

@ -1 +1 @@
85bae8b147813e5a2e79f1a96ab0c30dee8ad6a1
48b28c1cc502cdcafb209491ce21b84c42358c03

View File

@ -4,6 +4,7 @@ import { Mousetrap, MOUSETRAP_COPY_KEYBOARD_SHORTCUT } from '~/lib/mousetrap';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import {
COPY_BUTTON_ACTION,
DOWNLOAD_BUTTON_ACTION,
@ -33,7 +34,7 @@ export default {
recoveryCodeDownloadFilename: RECOVERY_CODE_DOWNLOAD_FILENAME,
i18n,
mousetrap: null,
components: { GlSprintf, GlButton, GlAlert, ClipboardButton, GlCard },
components: { GlSprintf, GlButton, GlAlert, ClipboardButton, GlCard, PageHeading },
mixins: [Tracking.mixin()],
props: {
codes: {
@ -100,20 +101,19 @@ export default {
<template>
<div>
<h1 class="page-title gl-text-size-h-display">
{{ $options.i18n.pageTitle }}
</h1>
<hr />
<page-heading :heading="$options.i18n.pageTitle">
<template #description>
<gl-sprintf :message="$options.i18n.pageDescription">
<template #bold="{ content }"
><strong>{{ content }}</strong></template
>
</gl-sprintf>
</template>
</page-heading>
<gl-alert variant="info" :dismissible="false">
{{ $options.i18n.alertTitle }}
</gl-alert>
<p class="gl-mt-5">
<gl-sprintf :message="$options.i18n.pageDescription">
<template #bold="{ content }"
><strong>{{ content }}</strong></template
>
</gl-sprintf>
</p>
<gl-card class="codes-to-print gl-my-5" data-testid="recovery-codes">
<ul class="gl-m-0 gl-pl-5">

View File

@ -18,7 +18,6 @@ import {
I18N_DEVICE_NAME_PLACEHOLDER,
I18N_ERROR_HTTP,
I18N_ERROR_UNSUPPORTED_BROWSER,
I18N_INFO_TEXT,
I18N_NOTICE,
I18N_PASSWORD,
I18N_PASSWORD_DESCRIPTION,
@ -61,7 +60,6 @@ export default {
I18N_DEVICE_NAME_PLACEHOLDER,
I18N_ERROR_HTTP,
I18N_ERROR_UNSUPPORTED_BROWSER,
I18N_INFO_TEXT,
I18N_NOTICE,
I18N_PASSWORD,
I18N_PASSWORD_DESCRIPTION,
@ -132,22 +130,13 @@ export default {
</script>
<template>
<div>
<div class="gl-mb-5">
<template v-if="isCurrentState($options.STATE_UNSUPPORTED)">
<gl-alert variant="danger" :dismissible="false">{{ errorMessage }}</gl-alert>
</template>
<template v-else-if="isCurrentState($options.STATE_READY)">
<div class="row">
<div class="col-md-5 gl-mb-5">
<gl-button variant="confirm" @click="onRegister">{{
$options.I18N_BUTTON_SETUP
}}</gl-button>
</div>
<div class="col-md-7">
<p>{{ $options.I18N_INFO_TEXT }}</p>
</div>
</div>
<gl-button variant="confirm" @click="onRegister">{{ $options.I18N_BUTTON_SETUP }}</gl-button>
</template>
<template v-else-if="isCurrentState($options.STATE_WAITING)">
@ -169,53 +158,50 @@ export default {
</gl-sprintf>
</gl-alert>
<div class="row">
<gl-form method="post" :action="targetPath" class="col-md-9" data-testid="create-webauthn">
<gl-form-group
v-if="passwordRequired"
:description="$options.I18N_PASSWORD_DESCRIPTION"
:label="$options.I18N_PASSWORD"
label-for="webauthn-registration-current-password"
>
<gl-form-input
id="webauthn-registration-current-password"
v-model="form.password"
name="current_password"
type="password"
autocomplete="current-password"
data-testid="current-password-input"
/>
</gl-form-group>
<gl-form method="post" :action="targetPath" data-testid="create-webauthn">
<gl-form-group
v-if="passwordRequired"
:description="$options.I18N_PASSWORD_DESCRIPTION"
:label="$options.I18N_PASSWORD"
label-for="webauthn-registration-current-password"
>
<gl-form-input
id="webauthn-registration-current-password"
v-model="form.password"
name="current_password"
type="password"
autocomplete="current-password"
data-testid="current-password-input"
/>
</gl-form-group>
<gl-form-group
:description="$options.I18N_DEVICE_NAME_DESCRIPTION"
:label="$options.I18N_DEVICE_NAME"
label-for="device-name"
>
<gl-form-input
id="device-name"
v-model="form.deviceName"
name="device_registration[name]"
:placeholder="$options.I18N_DEVICE_NAME_PLACEHOLDER"
data-testid="device-name-input"
/>
</gl-form-group>
<gl-form-group
:description="$options.I18N_DEVICE_NAME_DESCRIPTION"
:label="$options.I18N_DEVICE_NAME"
label-for="device-name"
>
<gl-form-input
id="device-name"
v-model="form.deviceName"
name="device_registration[name]"
:placeholder="$options.I18N_DEVICE_NAME_PLACEHOLDER"
data-testid="device-name-input"
/>
</gl-form-group>
<input type="hidden" name="device_registration[device_response]" :value="credentials" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input type="hidden" name="device_registration[device_response]" :value="credentials" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<gl-button type="submit" :disabled="disabled" variant="confirm">{{
$options.I18N_BUTTON_REGISTER
}}</gl-button>
</gl-form>
</div>
<gl-button type="submit" :disabled="disabled" variant="confirm">{{
$options.I18N_BUTTON_REGISTER
}}</gl-button>
</gl-form>
</template>
<template v-else-if="isCurrentState($options.STATE_ERROR)">
<gl-alert
variant="danger"
:dismissible="false"
class="gl-mb-5"
:secondary-button-text="$options.I18N_BUTTON_TRY_AGAIN"
@secondaryAction="onRegister"
>

View File

@ -15,9 +15,6 @@ export const I18N_ERROR_HTTP = __(
export const I18N_ERROR_UNSUPPORTED_BROWSER = __(
"Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
);
export const I18N_INFO_TEXT = __(
'Your device needs to be set up. Plug it in (if needed) and click the button on the left.',
);
export const I18N_NOTICE = __(
'You must save your recovery codes after you first register a two-factor authenticator, so you do not lose access to your account. %{linkStart}See the documentation on managing your WebAuthn device for more information.%{linkEnd}',
);

View File

@ -51,6 +51,10 @@ const initMermaid = () => {
},
secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'htmlLabels'],
securityLevel: 'strict',
dompurifyConfig: {
ADD_TAGS: ['foreignObject'],
HTML_INTEGRATION_POINTS: { foreignobject: true },
},
});
};

View File

@ -1,13 +1,11 @@
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { GlButton } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { provideWebIdeLink } from 'ee_else_ce/pages/projects/shared/web_ide_link/provide_web_ide_link';
import TableOfContents from '~/blob/components/table_contents.vue';
import { BlobViewer, initAuxiliaryViewer } from '~/blob/viewer/index';
import { __ } from '~/locale';
import GpgBadges from '~/gpg_badges';
import createDefaultClient from '~/lib/graphql';
import initBlob from '~/pages/projects/init_blob';
@ -18,7 +16,6 @@ import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
import createStore from '~/code_navigation/store';
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import { generateHistoryUrl } from '~/repository/utils/url_utility';
import RefSelector from '~/ref/components/ref_selector.vue';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
@ -208,30 +205,3 @@ if (tableContentsEl) {
},
});
}
const treeHistoryLinkEl = document.getElementById('js-commit-history-link');
if (treeHistoryLinkEl) {
const { historyLink } = treeHistoryLinkEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: treeHistoryLinkEl,
router,
render(h) {
const url = generateHistoryUrl(
historyLink,
this.$route.params.path,
this.$route.meta.refType || this.$route.query.ref_type,
);
return h(
GlButton,
{
attrs: {
href: url.href,
category: 'tertiary',
},
},
[__('History')],
);
},
});
}

View File

@ -8,6 +8,7 @@ import { sanitize } from '~/lib/dompurify';
import { InternalEvents } from '~/tracking';
import { FIND_FILE_BUTTON_CLICK } from '~/tracking/constants';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { generateHistoryUrl } from '~/repository/utils/url_utility';
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import RefSelector from '~/ref/components/ref_selector.vue';
import Breadcrumbs from '~/repository/components/header_area/breadcrumbs.vue';
@ -18,6 +19,7 @@ export default {
i18n: {
compare: __('Compare'),
findFile: __('Find file'),
history: __('History'),
},
components: {
GlButton,
@ -61,6 +63,10 @@ export default {
required: false,
default: null,
},
historyLink: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
@ -70,6 +76,15 @@ export default {
isTreeView() {
return this.$route.name !== 'blobPathDecoded';
},
historyPath() {
const url = generateHistoryUrl(
this.historyLink,
this.$route.params.path,
this.$route.meta.refType || this.$route.query.ref_type,
);
return url.href;
},
getRefType() {
return this.$route.query.ref_type;
},
@ -151,6 +166,9 @@ export default {
class="shortcuts-compare"
>{{ $options.i18n.compare }}</gl-button
>
<gl-button v-if="!isReadmeView" :href="historyPath" data-testid="tree-history-control">{{
$options.i18n.history
}}</gl-button>
<gl-button
v-gl-tooltip.html="findFileTooltip"
:aria-keyshortcuts="findFileShortcutKey"

View File

@ -177,12 +177,7 @@ export default {
{{ $options.i18n.blame }}
</gl-button>
<gl-button
data-testid="history"
:href="blobInfo.historyPath"
:class="$options.buttonClassList"
class="gl-block sm:gl-hidden"
>
<gl-button data-testid="history" :href="blobInfo.historyPath" :class="$options.buttonClassList">
{{ $options.i18n.history }}
</gl-button>

View File

@ -67,11 +67,6 @@ export default {
required: false,
default: null,
},
historyUrl: {
type: String,
required: false,
default: '',
},
},
data() {
return {
@ -113,18 +108,17 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" size="md" color="dark" class="m-auto gl-min-h-8 gl-py-6" />
<commit-info v-else-if="commit" :commit="commit">
<div class="commit-actions gl-flex gl-items-start">
<div class="commit-actions gl-flex-align gl-flex gl-flex-row gl-items-center">
<signature-badge v-if="commit.signature" :signature="commit.signature" />
<div v-if="commit.pipeline" class="gl-ml-5 gl-flex gl-h-7 gl-items-center">
<div v-if="commit.pipeline" class="gl-ml-5">
<ci-icon
:status="commit.pipeline.detailedStatus"
:aria-label="statusTitle"
class="js-commit-pipeline"
/>
</div>
<gl-button-group class="js-commit-sha-group gl-ml-4 gl-flex gl-items-center">
<gl-button-group class="js-commit-sha-group gl-ml-4">
<gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
showCommitId
}}</gl-button>
@ -134,14 +128,6 @@ export default {
class="input-group-text"
/>
</gl-button-group>
<gl-button
category="tertiary"
data-testid="last-commit-history"
:href="historyUrl"
class="gl-ml-4"
>
{{ __('History') }}
</gl-button>
</div>
</commit-info>
</template>

View File

@ -116,24 +116,16 @@ export default function setupVueRepositoryList() {
});
};
const lastCommitEl = document.getElementById('js-last-commit');
const initLastCommitApp = () =>
new Vue({
el: lastCommitEl,
el: document.getElementById('js-last-commit'),
router,
apolloProvider,
render(h) {
const historyUrl = generateHistoryUrl(
lastCommitEl.dataset.historyLink,
this.$route.params.path,
this.$route.meta.refType || this.$route.query.ref_type,
);
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
refType: this.$route.meta.refType || this.$route.query.ref_type,
historyUrl: historyUrl.href,
},
});
},

View File

@ -8,6 +8,7 @@ export default function initHeaderApp(isReadmeView = false) {
const headerEl = document.getElementById('js-repository-blob-header-app');
if (headerEl) {
const {
historyLink,
ref,
escapedRef,
refType,
@ -57,6 +58,7 @@ export default function initHeaderApp(isReadmeView = false) {
props: {
refType,
currentRef: ref,
historyLink,
// BlobControls:
projectPath,
// RefSelector:

View File

@ -1,4 +1,5 @@
<script>
import { GlIntersectionObserver } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TokenPermissions from './token_permissions.vue';
import OutboundTokenAccess from './outbound_token_access.vue';
@ -6,21 +7,33 @@ import InboundTokenAccess from './inbound_token_access.vue';
import AuthLog from './auth_log.vue';
export default {
name: 'TokenAccessApp',
components: {
GlIntersectionObserver,
AuthLog,
InboundTokenAccess,
OutboundTokenAccess,
TokenPermissions,
},
mixins: [glFeatureFlagsMixin()],
data() {
return {
isVisible: false,
};
},
methods: {
updateVisible({ isIntersecting }) {
this.isVisible = isIntersecting;
},
},
};
</script>
<template>
<div>
<token-permissions v-if="glFeatures.allowPushRepositoryForJobToken" />
<inbound-token-access />
<auth-log />
<outbound-token-access />
</div>
<gl-intersection-observer @update="updateVisible">
<template v-if="isVisible">
<token-permissions v-if="glFeatures.allowPushRepositoryForJobToken" />
<inbound-token-access />
<auth-log />
<outbound-token-access />
</template>
</gl-intersection-observer>
</template>

View File

@ -43,7 +43,6 @@
.commit-content {
padding-right: 10px;
white-space: normal;
min-width: 0;
.commit-title {
display: flex;

View File

@ -73,7 +73,7 @@ module EnforcesTwoFactorAuthentication
end
def two_factor_verifier
@two_factor_verifier ||= Gitlab::Auth::TwoFactorAuthVerifier.new(current_user, request) # rubocop:disable Gitlab/ModuleWithInstanceVariables
@two_factor_verifier ||= Gitlab::Auth::TwoFactorAuthVerifier.new(current_user, request)
end
def mfa_help_page_url

View File

@ -12,6 +12,7 @@ module Integrations
field :send_from_committer_email,
type: :checkbox,
title: -> { s_("EmailsOnPushService|Send from committer") },
description: -> { s_("EmailsOnPushService|Send from committer") },
help: -> do
@help ||= begin
domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
@ -23,15 +24,18 @@ module Integrations
field :disable_diffs,
type: :checkbox,
title: -> { s_("EmailsOnPushService|Disable code diffs") },
help: -> { s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") }
help: -> { s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
description: -> { s_("EmailsOnPushService|Disable code diffs") }
field :branches_to_be_notified,
type: :select,
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
description: -> { _('Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default value is `default`.') },
choices: branch_choices
field :recipients,
type: :textarea,
required: true,
placeholder: -> { s_('EmailsOnPushService|tanuki@example.com gitlab@example.com') },
help: -> { s_('EmailsOnPushService|Emails separated by whitespace.') }

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class SentNotification < ApplicationRecord
include EachBatch
belongs_to :project
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"

View File

@ -2,6 +2,7 @@
class Subscription < ApplicationRecord
include FromUnion
include EachBatch
belongs_to :user
belongs_to :project

View File

@ -8,7 +8,7 @@ module WorkItems
attr_reader :work_item, :service_response, :target_namespace
# work_item - original work item
# target_namespace - ProjectNamespace(not Project) or Group
# target_namespace - ProjectNamespace, Group or Project
# current_user - user performing the move/clone action
def initialize(work_item:, target_namespace:, current_user: nil, params: {})
@work_item = work_item
@ -22,11 +22,7 @@ module WorkItems
return verification_response if verification_response.error?
::ApplicationRecord.transaction do
@service_response = data_sync_action
end
service_response
data_sync_action
end
private

View File

@ -5,11 +5,64 @@ module WorkItems
module Widgets
class Notifications < Base
def after_save_commit
# copy Notifications
return unless params[:operation] == :move
return unless target_work_item.get_widget(:notifications)
work_item.subscriptions.each_batch(of: BATCH_SIZE) do |subscriptions_batch|
::Subscription.insert_all(new_work_item_subscriptions(subscriptions_batch))
end
# This replicates current move Issues::MoveService behaviour. This should be changed though to
# move sent_notifications for any work_item that is being moved.
return unless work_item.from_service_desk?
# When moving sent notifications for any work item this can entail updating many records in some instances.
# We should consider moving this to an async worker rather than have it run in a single request.
work_item.sent_notifications.each_batch(of: BATCH_SIZE) do |sent_notifications_batch|
# SentNotification does not have a sharding key yet.
#
# However when it has one it would potentially break
# the immutability of the sharding key, because we need to update the record in place to keep the same
# reply_key, which is a unique key.
#
# The alternative implies to create a new unique key and a mapping between the two keys, which requires
# adding a new table or array column to keep the old keys, i.e. move development effort.
::SentNotification.upsert_all(
new_work_item_sent_notifications(sent_notifications_batch), unique_by: :reply_key
)
end
end
def post_move_cleanup
# do it
work_item.subscriptions.each_batch(of: BATCH_SIZE) do |subscriptions_batch|
subscriptions_batch.delete_all
end
# When moving sent notifications for any work item this can entail deleting many records in some instances.
# We should consider moving this to an async worker rather than have it run in a single request.
work_item.sent_notifications.each_batch(of: BATCH_SIZE) do |sent_notifications_batch|
sent_notifications_batch.delete_all
end
end
private
def new_work_item_subscriptions(subscriptions_batch)
subscriptions_batch.map do |subscription|
subscription.attributes.except("id").tap do |ep|
ep["subscribable_id"] = target_work_item.id
ep["project_id"] = target_work_item.project_id
end
end
end
def new_work_item_sent_notifications(sent_notifications_batch)
sent_notifications_batch.map do |sent_notification|
sent_notification.attributes.except("id").tap do |ep|
ep["noteable_id"] = target_work_item.id
ep["project_id"] = target_work_item.project_id
end
end
end
end
end

View File

@ -1,8 +1,11 @@
- breadcrumb_title _('Two-Factor Authentication')
- page_title _('Two-Factor Authentication'), _('Account')
- title = _('Two-Factor Authentication')
- breadcrumb_title title
- page_title title, _('Account')
- add_to_breadcrumbs _('Account'), profile_account_path
- troubleshooting_link = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication_troubleshooting.md'), target: '_blank', rel: 'noopener noreferrer'
= render ::Layouts::PageHeadingComponent.new(title, options: { class: 'gl-mb-3' })
- if @error
= render Pajamas::AlertComponent.new(title: @error[:message],
variant: :danger,
@ -10,24 +13,23 @@
dismissible: false) do |c|
- c.with_body do
= troubleshooting_link
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
= _('Register a one-time password authenticator')
%p
= _('Use a one-time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).')
.col-lg-8
= render ::Layouts::SettingsSectionComponent.new(_('Register a one-time password authenticator')) do |c|
- c.with_description do
= _('Use a one-time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).')
- if current_user.two_factor_otp_enabled?
= _("You've already enabled two-factor authentication using a one-time password authenticator. In order to register a different device, you must first delete this authenticator.")
- else
- register_2fa_token = _('We recommend using cloud-based authenticator applications that can restore access if you lose your hardware device.')
= register_2fa_token.html_safe
= link_to _('What are some examples?'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'enable-a-one-time-password-authenticator'), target: '_blank', rel: 'noopener noreferrer'
- c.with_body do
- if current_user.two_factor_otp_enabled?
%p
= _("You've already enabled two-factor authentication using a one-time password authenticator. In order to register a different device, you must first delete this authenticator.")
.gl-inline-block.gl-w-full.sm:gl-w-auto
.js-two-factor-action-confirm{ data: delete_otp_authenticator_data(current_password_required?) }
- else
%p
- register_2fa_token = _('We recommend using cloud-based authenticator applications that can restore access if you lose your hardware device.')
= register_2fa_token.html_safe
= link_to _('What are some examples?'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'enable-a-one-time-password-authenticator'), target: '_blank', rel: 'noopener noreferrer'
.gl-p-2.gl-mb-3{ style: 'background: #fff' }
.gl-inline-block.gl-p-3.gl-pb-2.gl-mb-5{ style: 'background: #fff' }
= raw @qr_code
.gl-mb-5
= render Pajamas::CardComponent.new do |c|
@ -36,9 +38,9 @@
= _("Can't scan the code?")
%p.gl-mt-0.gl-mb-3
= _("To add the entry manually, provide the following details to the application on your phone.")
%p.gl-mt-0.gl-mb-0
%p.gl-my-0
= _('Account: %{account}') % { account: @account_string }
%p.gl-mt-0.gl-mb-0{ data: { testid: 'otp-secret-content' } }
%p.gl-my-0{ data: { testid: 'otp-secret-content' } }
= _('Key:')
%code.two-factor-secret= current_user.otp_secret.scan(/.{4}/).join(' ')
%p.gl-mb-0.two-factor-new-manual-content
@ -63,58 +65,55 @@
.gl-mt-3
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { data: { testid: 'register-2fa-app-button' } }) do
= _('Register with two-factor app')
%hr
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
= _('Register a WebAuthn device')
%p
= _('Set up a hardware device to enable two-factor authentication (2FA).')
%p
= _("Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser.")
.col-lg-8
= render ::Layouts::SettingsSectionComponent.new(_('Register a WebAuthn device')) do |c|
- c.with_description do
= _('Set up a hardware device to enable two-factor authentication (2FA).')
= _("Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser.")
- c.with_body do
- if @webauthn_registration.errors.present?
= form_errors(@webauthn_registration)
#js-device-registration{ data: device_registration_data(current_password_required: current_password_required?, target_path: create_webauthn_profile_two_factor_auth_path, webauthn_error: @webauthn_error) }
%hr
%h5
= _('WebAuthn Devices (%{length})') % { length: @registrations.length }
- if @registrations.present?
.table-responsive
%table.table
%colgroup
%col{ width: "50%" }
%col{ width: "30%" }
%col{ width: "20%" }
%thead
%tr
%th= _('Name')
%th= s_('2FADevice|Registered On')
%th
%tbody
- @registrations.each do |registration|
%tr
%td
- if registration[:name].present?
= registration[:name]
- else
%span.gl-text-gray-500
= _("no name set")
%td= registration[:created_at].to_date.to_fs(:medium)
%td
.gl-float-right
.js-two-factor-action-confirm{ data: delete_webauthn_device_data(current_password_required?, registration[:delete_path]) }
- else
.settings-message.text-center
= _("You don't have any WebAuthn devices registered yet.")
%hr
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
= _('Disable two-factor authentication')
%p
= _('Use this section to disable your one-time password authenticator and WebAuthn devices. You can also generate new recovery codes.')
.col-lg-8
= render ::Layouts::CrudComponent.new(_('WebAuthn Devices'),
icon: 'lock',
count: @registrations.length) do |c|
- c.with_body do
- if @registrations.present?
.table-responsive
%table.table.gl-table
%colgroup
%col{ width: "50%" }
%col{ width: "30%" }
%col{ width: "20%" }
%thead
%tr
%th= _('Name')
%th= s_('2FADevice|Registered On')
%th
%tbody
- @registrations.each do |registration|
%tr
%td
- if registration[:name].present?
= registration[:name]
- else
%span.gl-text-subtle
= _("no name set")
%td= registration[:created_at].to_date.to_fs(:medium)
%td{ class: '!gl-py-3' }
.gl-float-right
.js-two-factor-action-confirm{ data: delete_webauthn_device_data(current_password_required?, registration[:delete_path]) }
- else
.gl-text-subtle
= _("You don't have any WebAuthn devices registered yet.")
= render ::Layouts::SettingsSectionComponent.new(_('Disable two-factor authentication')) do |c|
- c.with_description do
= _('Use this section to disable your one-time password authenticator and WebAuthn devices. You can also generate new recovery codes.')
- c.with_body do
- if current_user.two_factor_enabled?
%p
= _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.')

View File

@ -20,7 +20,7 @@
#js-fork-info{ data: vue_fork_divergence_data(project, ref) }
.info-well.gl-hidden.sm:gl-flex.project-last-commit.gl-flex-col.gl-mt-5
#js-last-commit.gl-m-auto{ data: {ref_type: @ref_type.to_s, history_link: project_commits_path(@project, @ref)} }
#js-last-commit.gl-m-auto{ data: {ref_type: @ref_type.to_s} }
= gl_loading_icon(size: 'md')
- if project.licensed_feature_available?(:code_owners)
#js-code-owners{ data: { branch: @ref, can_view_branch_rules: can_view_branch_rules?, branch_rules_path: branch_rules_path } }

View File

@ -13,7 +13,7 @@
.info-well.gl-hidden.sm:gl-block
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref, show_legacy_ci_icon: false, show_history_button: true
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref, show_legacy_ci_icon: false
- if project.licensed_feature_available?(:code_owners)
#js-code-owners{ data: { blob_path: blob.path, project_path: @project.full_path, branch: @ref, can_view_branch_rules: can_view_branch_rules?, branch_rules_path: branch_rules_path } }

View File

@ -26,8 +26,7 @@
- else
= link_button_to _('Blame'), project_blame_path(@project, @id, ref_type: @ref_type), data: { event_tracking: 'click_blame_control_on_blob_page' }, class: 'js-blob-blame-link' unless blob.empty?
.gl-block.sm:gl-hidden.gl-w-full
= link_button_to _('History'), project_commits_path(@project, @id, ref_type: @ref_type), data: { event_tracking: 'click_history_control_on_blob_page' }
= link_button_to _('History'), project_commits_path(@project, @id, ref_type: @ref_type), data: { event_tracking: 'click_history_control_on_blob_page' }
- permalink_description = _('Go to permalink')
- permalink_shortcut = 'y'

View File

@ -16,7 +16,6 @@
- commit = commit.present(current_user: current_user)
- commit_status = commit.detailed_status_for(ref)
- show_legacy_ci_icon = local_assigns.fetch(:show_legacy_ci_icon, true)
- show_history_button = local_assigns.fetch(:show_history_button, false)
- tags = commit.tags_for_display
- collapsible = local_assigns.fetch(:collapsible, true)
- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
@ -29,7 +28,7 @@
.gl-self-start.gl-hidden.sm:gl-block
= author_avatar(commit, size: 32, has_tooltip: false)
.commit-detail.flex-list.gl-flex.gl-justify-between.gl-items-start.gl-grow.gl-min-w-0
.commit-detail.flex-list.gl-flex.gl-justify-between.gl-items-center.gl-grow.gl-min-w-0
.commit-content{ data: { testid: 'commit-content' } }
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)], data: link_data_attrs
@ -59,8 +58,7 @@
%pre{ class: ["commit-row-description gl-mb-3 gl-whitespace-pre-wrap", (collapsible ? "js-toggle-content" : "!gl-block")] }
= preserve(markdown_field(commit, :description))
.commit-actions.gl-flex.gl-items-center
.commit-actions.flex-row
- if tags.present?
= gl_badge_tag(variant: :neutral, icon: 'tag', class: 'gl-font-monospace') do
- if tags.size > 1
@ -84,8 +82,3 @@
= commit.short_id
= clipboard_button(text: commit.id, category: :primary, size: :medium, title: _("Copy commit SHA"))
= link_to_browse_code(project, commit)
- if show_history_button
#js-commit-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
= render Pajamas::ButtonComponent.new(category: :tertiary, button_options: {class: 'gl-ml-4'}) do
= _('History')

View File

@ -10,8 +10,7 @@
= render_if_exists 'projects/tree/lock_link'
= render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
.gl-block.sm:gl-hidden.gl-w-full
#js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
#js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil, css_classes: 'gl-w-full sm:gl-w-auto'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module AuthorizedProjectUpdate
class UserRefreshOverUserRangeWorker # rubocop:disable Scalability/IdempotentWorker
class UserRefreshOverUserRangeWorker
# This worker checks if users requires an update to their project_authorizations records.
# This check is done via the data read from the database replica (and not from the primary).
# If this check returns true, a completely new Sidekiq job is enqueued for a specific user

View File

@ -8,5 +8,11 @@ module Search
feature_category :global_search
concurrency_limit -> { ::Search.default_concurrency_limit }
end
private
def logger
::Gitlab::Elasticsearch::Logger.build
end
end
end

View File

@ -813,6 +813,9 @@ Gitlab.ee do
Settings.cron_jobs['elastic_index_bulk_cron_worker'] ||= {}
Settings.cron_jobs['elastic_index_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_bulk_cron_worker']['job_class'] ||= 'ElasticIndexBulkCronWorker'
Settings.cron_jobs['elastic_indexing_control_worker'] ||= {}
Settings.cron_jobs['elastic_indexing_control_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_indexing_control_worker']['job_class'] ||= 'ElasticIndexingControlWorker'
Settings.cron_jobs['elastic_index_embedding_bulk_cron_worker'] ||= {}
Settings.cron_jobs['elastic_index_embedding_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_embedding_bulk_cron_worker']['job_class'] ||= 'Search::ElasticIndexEmbeddingBulkCronWorker'

View File

@ -12,7 +12,7 @@ Rails.application.configure do |config|
when :set_user
activity.user_authenticated!
activity.user_session_override!
when :fetch # rubocop:disable Lint/EmptyWhen
when :fetch
# We ignore session fetch events
else
activity.user_session_override!

View File

@ -321,8 +321,6 @@
- 1
- - elastic_full_index
- 1
- - elastic_indexing_control
- 1
- - elastic_namespace_indexer
- 1
- - elastic_namespace_rollout

View File

@ -0,0 +1,8 @@
---
migration_job_name: DeleteDuplicateIssuableResourceLinks
description: Remove the duplicates for the production DB.
feature_category: database
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172230
milestone: '17.6'
queued_migration_version: 20241111055711
finalized_by: # version of the migration that finalized this BBM

View File

@ -7,4 +7,4 @@ milestone: '17.4'
queued_migration_version: 20240910154923
# Replace with the approximate date you think it's best to ensure the completion of this BBM.
finalize_after: '2024-09-17'
finalized_by: # version of the migration that finalized this BBM
finalized_by: 20241115122644

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class UpdateSubscriptionsIndexOnNoteable < Gitlab::Database::Migration[2.2]
milestone '17.6'
INDEX_NAME = 'index_subscriptions_on_subscribable_type_subscribable_id_and_id'
COLUMN_NAMES = %i[subscribable_id subscribable_type id]
# Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/502840
#
# This is designed to improve iterating over related subscriptions records in batches:
def up
prepare_async_index :subscriptions, COLUMN_NAMES, name: INDEX_NAME
end
def down
unprepare_async_index :subscriptions, COLUMN_NAMES, name: INDEX_NAME
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# rubocop: disable Migration/PreventIndexCreation -- update an existing index
class UpdateSentNotificationsIndexOnNoteable < Gitlab::Database::Migration[2.2]
milestone '17.6'
INDEX_NAME = 'index_sent_notifications_on_noteable_type_noteable_id_and_id'
COLUMN_NAMES = %i[noteable_id id]
# Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/502841
#
# This is designed to replace existing index:
# "index_sent_notifications_on_noteable_type_noteable_id" btree (noteable_id) WHERE noteable_type = 'Issue'
# with
# "index_sent_notifications_on_noteable_type_noteable_id_id" btree (noteable_id, id) WHERE noteable_type = 'Issue'
# to improve iterating over issue related sent notification records in batches.
def up
prepare_async_index :sent_notifications, COLUMN_NAMES, where: "noteable_type = 'Issue'", name: INDEX_NAME
end
def down
unprepare_async_index :sent_notifications, COLUMN_NAMES, name: INDEX_NAME
end
end
# rubocop: enable Migration/PreventIndexCreation

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class QueueDeleteDuplicateIssuableResourceLinks < Gitlab::Database::Migration[2.2]
milestone '17.6'
restrict_gitlab_migration gitlab_schema: :gitlab_main
MIGRATION = "DeleteDuplicateIssuableResourceLinks"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 1000
SUB_BATCH_SIZE = 100
def up
queue_batched_background_migration(
MIGRATION,
:issuable_resource_links,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :issuable_resource_links, :id, [])
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class FinalizeDeleteOrphanedBuildRecords < Gitlab::Database::Migration[2.2]
milestone '17.7'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_ci
MIGRATION = 'DeleteOrphanedBuildRecords'
def up
ensure_batched_background_migration_is_finished(
job_class_name: MIGRATION,
table_name: :p_ci_builds,
column_name: :commit_id,
job_arguments: [],
finalize: true
)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
569a9ecb955d32ad1b10c50906dd3659ee7524b71be7a7b6e0c9c99525c2c4db

View File

@ -0,0 +1 @@
c5170b7d25515fe41978abf6bf370adb943e6d134738180116dbf9eb046065aa

View File

@ -0,0 +1 @@
390cf83a10bc461d45f0c47eada3f25f3dd611d7c6a124bfd8bf01099f793d60

View File

@ -0,0 +1 @@
374b6394e31f923d98c2fd5b5eedb3aaf46bdcef977f5bda6535569334835e82

View File

@ -114,32 +114,7 @@ module API
chat_notification_channels
].flatten,
'drone-ci' => ::Integrations::DroneCi.api_arguments,
'emails-on-push' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :disable_diffs,
type: ::Grape::API::Boolean,
desc: 'Disable code diffs'
},
{
required: false,
name: :send_from_committer_email,
type: ::Grape::API::Boolean,
desc: 'Send from committer'
},
{
required: false,
name: :branches_to_be_notified,
type: String,
desc: 'Branches for which notifications are to be sent'
}
],
'emails-on-push' => ::Integrations::EmailsOnPush.api_arguments,
'external-wiki' => ::Integrations::ExternalWiki.api_arguments,
'gitlab-slack-application' => [
::Integrations::GitlabSlackApplication.api_arguments,

View File

@ -14,7 +14,7 @@ module Atlassian
ALGORITHM = 'RS256'
DEFAULT_PUBLIC_KEY_CDN_URL = 'https://connect-install-keys.atlassian.com'
PROXY_PUBLIC_KEY_PATH = '/-/jira_connect/public_keys'
UUID4_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
KEY_ID_REGEX = %r{\A[A-Za-z0-9_\-\/]+\z}
def initialize(token, verification_claims)
@token = token
@ -61,7 +61,7 @@ module Atlassian
end
def retrieve_public_key(key_id)
raise KeyFetchError unless UUID4_REGEX.match?(key_id)
raise KeyFetchError unless KEY_ID_REGEX.match?(key_id)
public_key = Gitlab::HTTP.try_get("#{public_key_cdn_url}/#{key_id}").try(:body)

View File

@ -91,9 +91,7 @@ module Gitlab
def optionally_run_in_admin_mode(user)
raise NonSidekiqEnvironmentError unless Gitlab::Runtime.sidekiq?
unless Gitlab::CurrentSettings.admin_mode && user.admin? # rubocop:disable Cop/UserAdmin -- policy checks should be enforced further down the stack
return yield
end
return yield unless Gitlab::CurrentSettings.admin_mode && user.admin?
bypass_session!(user.id) do
with_current_admin(user) do

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class DeleteDuplicateIssuableResourceLinks < BatchedMigrationJob
operation_name :delete_duplicate_issuable_resource_links
scope_to ->(relation) { relation.where(is_unique: false) }
feature_category :database
def perform
each_sub_batch do |sub_batch|
sub_batch.delete_all
end
end
end
end
end

View File

@ -13,10 +13,7 @@ module Gitlab
def initialize(group_full_path:, cut_off_date: DEFAULT_TIME_PERIOD.ago.beginning_of_day, logger: nil)
@cut_off_date = cut_off_date
# rubocop: disable CodeReuse/ActiveRecord
@group = Group.find_by_full_path(group_full_path)
# rubocop: enable CodeReuse/ActiveRecord
raise "Group with full_path #{group_full_path} not found" unless @group
raise "Invalid time: #{@cut_off_date}" unless @cut_off_date <= MINIMUM_TIME_PERIOD.ago

View File

@ -74,7 +74,7 @@ module Gitlab
ActionController::HttpAuthentication::Basic.encode_credentials(actor_name, token)
end
private # rubocop:disable Lint/UselessAccessModifier
private
attr_reader :container

View File

@ -11,6 +11,7 @@ module Gitlab
click_house_migration: ::Gitlab::SidekiqMiddleware::PauseControl::Strategies::ClickHouseMigration,
zoekt: ::Gitlab::SidekiqMiddleware::PauseControl::Strategies::Zoekt,
none: ::Gitlab::SidekiqMiddleware::PauseControl::Strategies::None,
advanced_search: ::Gitlab::SidekiqMiddleware::PauseControl::Strategies::AdvancedSearch,
deprecated: ::Gitlab::SidekiqMiddleware::PauseControl::Strategies::Deprecated
}.freeze

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Gitlab
module SidekiqMiddleware
module PauseControl
module Strategies
class AdvancedSearch < Base
override :should_pause?
def should_pause?
Gitlab::CurrentSettings.elasticsearch_pause_indexing?
end
end
end
end
end
end

View File

@ -61808,7 +61808,7 @@ msgstr ""
msgid "Web terminal"
msgstr ""
msgid "WebAuthn Devices (%{length})"
msgid "WebAuthn Devices"
msgstr ""
msgid "WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details."
@ -64547,9 +64547,6 @@ msgstr ""
msgid "Your device is not compatible with GitLab. Please try another device"
msgstr ""
msgid "Your device needs to be set up. Plug it in (if needed) and click the button on the left."
msgstr ""
msgid "Your device was successfully set up! Give it a name and register it with the GitLab server."
msgstr ""

View File

@ -154,7 +154,7 @@
"deckar01-task_list": "^2.3.1",
"dexie": "^3.2.3",
"diff": "^3.4.0",
"dompurify": "^3.1.6",
"dompurify": "^3.2.0",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^10.0.0",

View File

@ -24,6 +24,7 @@ module RuboCop
Search EE::Search API::Search EE::API::Search API::Admin::Search RuboCop::Cop::Search Types Resolvers
API::Entities::Search::Zoekt API::Internal::Search::Zoekt
Keeps
Gitlab::SidekiqMiddleware::PauseControl::Strategies::AdvancedSearch
].map { |x| x.split('::') }.freeze
SEARCH_REGEXES = [

View File

@ -21,11 +21,11 @@ global.Option = window.Option;
DOMPurify.addHook = () => {};
DOMPurify.sanitize = (x) => x;
const defaultGlob = "doc/**/*.md";
const defaultGlob = 'doc/**/*.md';
const mermaidMatch = /```mermaid(.*?)```/gms;
const argv = process.argv.length > 2 ? process.argv.slice(2) : [defaultGlob];
const mdFiles = argv.flatMap((arg) => glob.sync(arg))
const mdFiles = argv.flatMap((arg) => glob.sync(arg));
console.log(`Checking ${mdFiles.length} markdown files...`);
@ -43,6 +43,10 @@ mermaid.initialize({
},
secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'htmlLabels'],
securityLevel: 'strict',
dompurifyConfig: {
ADD_TAGS: ['foreignObject'],
HTML_INTEGRATION_POINTS: { foreignobject: true },
},
});
let errors = 0;
@ -71,6 +75,8 @@ await Promise.all(
if (errors > 0) {
console.log(`Total errors: ${errors}`);
console.log(`To fix these errors, see https://docs.gitlab.com/ee/development/documentation/testing/#mermaid-chart-linting.`);
console.log(
`To fix these errors, see https://docs.gitlab.com/ee/development/documentation/testing/#mermaid-chart-linting.`,
);
process.exit(1);
}

View File

@ -80,7 +80,7 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
# Check last commit
expect(find('.commit-content').text).to include(message)
expect(find_by_testid('last-commit-id-label').text).to eq(short_newrev)
expect(find('.js-commit-sha-group').text).to eq(short_newrev)
end
end

View File

@ -9,7 +9,6 @@ import {
I18N_BUTTON_TRY_AGAIN,
I18N_ERROR_HTTP,
I18N_ERROR_UNSUPPORTED_BROWSER,
I18N_INFO_TEXT,
I18N_STATUS_SUCCESS,
I18N_STATUS_WAITING,
STATE_ERROR,
@ -89,11 +88,10 @@ describe('Registration', () => {
});
describe(`when ${STATE_READY} state`, () => {
it('shows button and explanation text', () => {
it('shows button', () => {
createComponent();
expect(findButton().text()).toBe(I18N_BUTTON_SETUP);
expect(wrapper.text()).toContain(I18N_INFO_TEXT);
});
});

View File

@ -1,13 +0,0 @@
describe('dompurify dependency', () => {
it('should not be updated unless mermaid is verified', () => {
// why: We use `require` so that we don't have to manually dig through node_modules
// eslint-disable-next-line global-require
const { version } = require('dompurify/package.json');
// NOTE: Bumping this to 3.1.7 breaks mermaid diagrams. When upgrading DOMPurify, you **must** manually verify that
// mermaid diagrams work visually before fixing this test. For more context:
// - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167644
// - https://github.com/cure53/DOMPurify/issues/1002#issuecomment-2381258197
expect(version).toEqual('3.1.6');
});
});

View File

@ -5,10 +5,10 @@ exports[`Repository last commit component renders commit widget 1`] = `
commit="[object Object]"
>
<div
class="commit-actions gl-flex gl-items-start"
class="commit-actions gl-flex gl-flex-align gl-flex-row gl-items-center"
>
<div
class="gl-flex gl-h-7 gl-items-center gl-ml-5"
class="gl-ml-5"
>
<ci-icon-stub
aria-label="Pipeline: failed"
@ -19,7 +19,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
/>
</div>
<gl-button-group-stub
class="gl-flex gl-items-center gl-ml-4 js-commit-sha-group"
class="gl-ml-4 js-commit-sha-group"
>
<gl-button-stub
buttontextclasses=""
@ -43,18 +43,6 @@ exports[`Repository last commit component renders commit widget 1`] = `
variant="default"
/>
</gl-button-group-stub>
<gl-button-stub
buttontextclasses=""
category="tertiary"
class="gl-ml-4"
data-testid="last-commit-history"
href="/history"
icon=""
size="medium"
variant="default"
>
History
</gl-button-stub>
</div>
</commit-info-stub>
`;

View File

@ -43,6 +43,7 @@ describe('HeaderArea', () => {
const findBreadcrumbs = () => wrapper.findComponent(Breadcrumbs);
const findRefSelector = () => wrapper.findComponent(RefSelector);
const findHistoryButton = () => wrapper.findByTestId('tree-history-control');
const findFindFileButton = () => wrapper.findByTestId('tree-find-file-control');
const findCompareButton = () => wrapper.findByTestId('tree-compare-control');
const { bindInternalEventDocument } = useMockInternalEventsTracking();
@ -94,6 +95,13 @@ describe('HeaderArea', () => {
wrapper = createComponent({}, 'treePathDecoded');
});
describe('History button', () => {
it('renders History button with correct href', () => {
expect(findHistoryButton().exists()).toBe(true);
expect(findHistoryButton().attributes('href')).toContain('/history');
});
});
describe('Find file button', () => {
it('renders Find file button', () => {
expect(findFindFileButton().exists()).toBe(true);
@ -143,9 +151,10 @@ describe('HeaderArea', () => {
wrapper = createComponent({}, 'treePathDecoded', { isReadmeView: true });
});
it('does not render RefSelector or Breadcrumbs', () => {
it('does not render RefSelector, Breadcrumbs and History button', () => {
expect(findRefSelector().exists()).toBe(false);
expect(findBreadcrumbs().exists()).toBe(false);
expect(findHistoryButton().exists()).toBe(false);
});
});
});

View File

@ -21,7 +21,6 @@ const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findStatusBox = () => wrapper.findComponent(SignatureBadge);
const findCommitInfo = () => wrapper.findComponent(CommitInfo);
const findHistoryButton = () => wrapper.findByTestId('last-commit-history');
const defaultPipelineEdges = [
{
@ -99,7 +98,7 @@ const createComponent = async (data = {}) => {
wrapper = shallowMountExtended(LastCommit, {
apolloProvider: createMockApollo([[pathLastCommitQuery, mockResolver]]),
propsData: { currentPath, historyUrl: '/history' },
propsData: { currentPath },
mixins: [{ data: () => ({ ref: refMock }) }],
stubs: {
SignatureBadge,
@ -145,11 +144,6 @@ describe('Repository last commit component', () => {
expect(findLastCommitLabel().text()).toBe('12345678');
});
it('renders History button with correct href', () => {
expect(findHistoryButton().exists()).toBe(true);
expect(findHistoryButton().attributes('href')).toContain('/history');
});
it('hides pipeline components when pipeline does not exist', async () => {
createComponent({ pipelineEdges: [] });
await waitForPromises();

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlIntersectionObserver } from '@gitlab/ui';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
import TokenAccessApp from '~/token_access/components/token_access_app.vue';
@ -10,38 +11,50 @@ describe('TokenAccessApp component', () => {
const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess);
const findOutboundTokenAccess = () => wrapper.findComponent(OutboundTokenAccess);
const findTokenPermissions = () => wrapper.findComponent(TokenPermissions);
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const createComponent = ({ allowPushRepositoryForJobToken = true } = {}) => {
wrapper = shallowMount(TokenAccessApp, {
provide: {
glFeatures: {
allowPushRepositoryForJobToken,
},
},
provide: { glFeatures: { allowPushRepositoryForJobToken } },
});
};
describe('default', () => {
const emitIntersectionObserverUpdate = (isIntersecting) => {
findIntersectionObserver().vm.$emit('update', { isIntersecting });
};
describe.each`
phrase | action | expected
${'on page load'} | ${() => {}} | ${false}
${'when section is expanded'} | ${() => emitIntersectionObserverUpdate(true)} | ${true}
${'when section is collapsed'} | ${() => emitIntersectionObserverUpdate(false)} | ${false}
`('$phrase', ({ action, expected }) => {
beforeEach(() => {
createComponent();
action();
});
it('renders the outbound token access component', () => {
expect(findOutboundTokenAccess().exists()).toBe(true);
it('renders intersection observer', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
it('renders the inbound token access component', () => {
expect(findInboundTokenAccess().exists()).toBe(true);
it('renders/does not render the outbound token access component', () => {
expect(findOutboundTokenAccess().exists()).toBe(expected);
});
it('renders the token permissions component', () => {
expect(findTokenPermissions().exists()).toBe(true);
it('renders/does not render the inbound token access component', () => {
expect(findInboundTokenAccess().exists()).toBe(expected);
});
it('renders/does not render the token permissions component', () => {
expect(findTokenPermissions().exists()).toBe(expected);
});
});
describe('when allowPushRepositoryForJobToken feature flag is disabled', () => {
beforeEach(() => {
createComponent({ allowPushRepositoryForJobToken: false });
findIntersectionObserver().vm.$emit('update', { isIntersecting: true });
});
it('does not render the token permissions component', () => {

View File

@ -44,8 +44,18 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric, feature_category: :integ
it { is_expected.not_to be_valid }
end
context 'JWT contains a key ID that is not a valid UUID4' do
let(:public_key_id) { '123' }
context 'When public key ID is a string' do
let(:public_key_id) { 'generic-keypair/cs-migrations/installation-key-pair--r0d6u65570m0lc8n' }
specify do
expect(asymmetric_jwt).to be_valid
expect(WebMock).to have_requested(:get, install_keys_url)
end
end
context 'when public key ID contains invalid characters' do
let(:public_key_id) { '$#%' }
it { is_expected.not_to be_valid }
end

View File

@ -0,0 +1,152 @@
# frozen_string_literal: true
# rubocop:disable RSpec/MultipleMemoizedHelpers -- Needed in specs
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DeleteDuplicateIssuableResourceLinks, feature_category: :database do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
let(:issues) { table(:issues) }
let(:issuable_resource_links) { table(:issuable_resource_links) }
let(:issue_base_type_enum_value) { 0 }
let(:issue_type) { table(:work_item_types).find_by!(base_type: issue_base_type_enum_value) }
let(:user) { create_user(email: "test1@example.com", username: "test1") }
let(:namespace) { namespaces.create!(name: "test-1", path: "test-1", owner_id: user.id) }
let!(:project) do
projects.create!(
id: 9999, namespace_id: namespace.id,
project_namespace_id: namespace.id,
creator_id: user.id
)
end
let!(:issue1) { create_issue }
let!(:issue2) { create_issue }
let!(:issuable_resource_link1) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link1", is_unique: false) }
let!(:issuable_resource_link1_alt1) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link1alt", is_unique: false) }
let!(:issuable_resource_link1_alt2) { issuable_resource_links.create!(issue_id: issue2.id, link: "https://gitlab.com/link1", is_unique: false) }
let!(:issuable_resource_link2) do
irl = issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link2")
irl.update!(is_unique: true)
irl
end
let!(:issuable_resource_link2_alt1) do
irl = issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link2alt")
irl.update!(is_unique: true)
irl
end
let!(:issuable_resource_link2_alt2) do
irl = issuable_resource_links.create!(issue_id: issue2.id, link: "https://gitlab.com/link2")
irl.update!(is_unique: true)
irl
end
let!(:issuable_resource_link3) do
irl = issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link3")
irl.update!(is_unique: nil)
irl
end
let!(:issuable_resource_link3_alt1) do
irl = issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link3alt")
irl.update!(is_unique: nil)
irl
end
let!(:issuable_resource_link3_alt2) do
irl = issuable_resource_links.create!(issue_id: issue2.id, link: "https://gitlab.com/link3")
irl.update!(is_unique: nil)
irl
end
let!(:issuable_resource_link4) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link4", is_unique: true) }
let!(:issuable_resource_link4_dup) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link4", is_unique: false) }
let!(:issuable_resource_link5) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link5", is_unique: false) }
let!(:issuable_resource_link5_dup) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link5", is_unique: false) }
let!(:issuable_resource_link6) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link6", is_unique: nil) }
let!(:issuable_resource_link6_dup) { issuable_resource_links.create!(issue_id: issue1.id, link: "https://gitlab.com/link6", is_unique: nil) }
let(:stating_id) { issuable_resource_links.pluck(:id).min }
let(:end_id) { issuable_resource_links.pluck(:id).max }
subject(:migration) do
described_class.new(
start_id: stating_id,
end_id: end_id,
batch_table: :issuable_resource_links,
batch_column: :id,
sub_batch_size: 100,
pause_ms: 0,
connection: ApplicationRecord.connection
)
end
describe 'the deletion of issuable_resource_links' do
using RSpec::Parameterized::TableSyntax
where(:issuable_resource_link, :expected_is_unique, :expected_to_get_deleted) do
ref(:issuable_resource_link1) | be(false) | true
ref(:issuable_resource_link1_alt1) | be(false) | true
ref(:issuable_resource_link1_alt2) | be(false) | true
ref(:issuable_resource_link2) | be(true) | false
ref(:issuable_resource_link2_alt1) | be(true) | false
ref(:issuable_resource_link2_alt2) | be(true) | false
ref(:issuable_resource_link3) | be_nil | false
ref(:issuable_resource_link3_alt1) | be_nil | false
ref(:issuable_resource_link3_alt2) | be_nil | false
ref(:issuable_resource_link4) | be(true) | false
ref(:issuable_resource_link4_dup) | be(false) | true
ref(:issuable_resource_link5) | be(false) | true
ref(:issuable_resource_link5_dup) | be(false) | true
ref(:issuable_resource_link6) | be_nil | false
ref(:issuable_resource_link6_dup) | be_nil | false
end
with_them do
it 'verifies the presence of the record' do
expect(issuable_resource_links.find(issuable_resource_link.id).is_unique).to expected_is_unique
expect { migration.perform }.to change { issuable_resource_links.count }.by(-6)
if expected_to_get_deleted
expect(issuable_resource_links.where(id: issuable_resource_link.id)).to be_empty
else
expect(issuable_resource_links.where(id: issuable_resource_link.id)).not_to be_empty
end
end
end
end
private
def create_issue
issues.create!(
title: 'title',
description: 'description',
project_id: project.id,
namespace_id: project.project_namespace_id,
work_item_type_id: issue_type.id
)
end
def create_user(overrides = {})
attrs = {
email: "test@example.com",
notification_email: "test@example.com",
name: "test",
username: "test",
state: "active",
projects_limit: 10
}.merge(overrides)
users.create!(attrs)
end
end
# rubocop:enable RSpec/MultipleMemoizedHelpers

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueDeleteDuplicateIssuableResourceLinks, migration: :gitlab_main, feature_category: :database do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_main,
table_name: :issuable_resource_links,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
}
end
end
end

View File

@ -2,8 +2,8 @@
require 'spec_helper'
RSpec.describe Integrations::EmailsOnPush do
let_it_be(:project) { create_default(:project).freeze }
RSpec.describe Integrations::EmailsOnPush, feature_category: :integrations do
let_it_be(:project) { create(:project, :repository) }
describe 'Validations' do
context 'when integration is active' do
@ -87,7 +87,6 @@ RSpec.describe Integrations::EmailsOnPush do
end
describe '#execute' do
let_it_be(:project) { create(:project, :repository) }
let(:push_data) { { object_kind: 'push' } }
let(:integration) { create(:emails_on_push_integration, project: project) }
let(:recipients) { 'test@gitlab.com' }

View File

@ -39,7 +39,7 @@ RSpec.describe WorkItems::DataSync::Widgets::AwardEmoji, feature_category: :team
context 'when cloning work item' do
let(:params) { { operation: :clone } }
it 'copies award_emoji from work_item to target_work_item' do
it 'does not copy award_emoji' do
expect(callback).not_to receive(:new_work_item_award_emoji)
expect(::AwardEmoji).not_to receive(:insert_all)

View File

@ -0,0 +1,149 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::DataSync::Widgets::Notifications, feature_category: :team_planning do
let_it_be(:current_user) { create(:user) }
let_it_be_with_reload(:work_item) { create(:work_item) }
let_it_be_with_reload(:subscription1) do
create(:subscription, subscribable: work_item, user: create(:user), subscribed: true)
end
let_it_be_with_reload(:subscription2) do
create(:subscription, subscribable: work_item, user: create(:user), subscribed: false)
end
let_it_be_with_reload(:sent_notification1) do
create(:sent_notification, project: work_item.project, noteable: work_item, recipient: create(:user))
end
let_it_be_with_reload(:sent_notification2) do
create(:sent_notification, project: work_item.project, noteable: work_item, recipient: create(:user))
end
let_it_be(:target_work_item) { create(:work_item) }
let(:params) { { operation: :move } }
subject(:callback) do
described_class.new(
work_item: work_item, target_work_item: target_work_item, current_user: current_user, params: params
)
end
describe '#after_save_commit' do
context 'when target work item has notifications widget' do
before do
allow(target_work_item).to receive(:get_widget).with(:notifications).and_return(true)
allow(work_item).to receive(:from_service_desk?).and_return(true)
end
context 'when moving work item' do
it 'copies notifications from work_item to target_work_item' do
expect(callback).to receive(:new_work_item_subscriptions).and_call_original
expect(callback).to receive(:new_work_item_sent_notifications).and_call_original
expect(::Subscription).to receive(:insert_all).and_call_original
expect(::SentNotification).to receive(:upsert_all).and_call_original
expected_subscriptions = work_item.subscriptions.pluck(:user_id)
expected_sent_notifications = work_item.sent_notifications.pluck(:recipient_id)
callback.after_save_commit
subscriptions = target_work_item.reload.subscriptions.pluck(:user_id)
sent_notifications = target_work_item.reload.sent_notifications.pluck(:recipient_id)
expect(subscriptions).to match_array(expected_subscriptions)
expect(sent_notifications).to match_array(expected_sent_notifications)
end
end
context 'when cloning work item' do
let(:params) { { operation: :clone } }
it 'does not copy subscriptions or notifications' do
expect(callback).not_to receive(:new_work_item_subscriptions)
expect(callback).not_to receive(:new_work_item_sent_notifications)
expect(::Subscription).not_to receive(:insert_all)
expect(::SentNotification).not_to receive(:upsert_all)
callback.after_save_commit
expect(target_work_item.reload.subscriptions).to be_empty
expect(target_work_item.reload.sent_notifications).to be_empty
end
end
context 'when work item is not a service desk work item' do
before do
allow(work_item).to receive(:from_service_desk?).and_return(false)
end
it 'copies subscriptions but does not copy notifications' do
expect(callback).to receive(:new_work_item_subscriptions).and_call_original
expect(callback).not_to receive(:new_work_item_sent_notifications)
expect(::Subscription).to receive(:insert_all).and_call_original
expect(::SentNotification).not_to receive(:upsert_all)
expected_subscriptions = work_item.subscriptions.pluck(:user_id)
callback.after_save_commit
subscriptions = target_work_item.reload.subscriptions.pluck(:user_id)
expect(subscriptions).to match_array(expected_subscriptions)
expect(target_work_item.reload.sent_notifications).to be_empty
end
end
end
context 'when target work item does not have notifications widget' do
before do
target_work_item.reload
allow(target_work_item).to receive(:get_widget).with(:notifications).and_return(false)
end
it 'does not copy subscriptions or notifications' do
expect(callback).not_to receive(:new_work_item_subscriptions)
expect(callback).not_to receive(:new_work_item_sent_notifications)
expect(::Subscription).not_to receive(:insert_all)
expect(::SentNotification).not_to receive(:upsert_all)
callback.after_save_commit
expect(target_work_item.reload.subscriptions).to be_empty
expect(target_work_item.reload.sent_notifications).to be_empty
end
end
end
describe '#post_move_cleanup' do
it 'removes original work item notifications' do
expect(work_item.subscriptions.count).to eq(2)
expect(work_item.sent_notifications.count).to eq(2)
expect { callback.post_move_cleanup }.not_to raise_error
expect(work_item.subscriptions).to be_empty
expect(work_item.sent_notifications).to be_empty
end
context 'when cleanup data in batches' do
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
end
it 'removes original work item notifications' do
create(:subscription, subscribable: work_item, user: create(:user), subscribed: true)
create(:subscription, subscribable: work_item, user: create(:user), subscribed: false)
create(:sent_notification, project: work_item.project, noteable: work_item, recipient: create(:user))
create(:sent_notification, project: work_item.project, noteable: work_item, recipient: create(:user))
expect(work_item.subscriptions.count).to eq(4)
expect(work_item.sent_notifications.count).to eq(4)
callback.post_move_cleanup
expect(work_item.subscriptions).to be_empty
expect(work_item.sent_notifications).to be_empty
end
end
end
end

View File

@ -66,15 +66,36 @@ RSpec.shared_examples 'cloneable and moveable widget data' do
work_item.reload.milestone&.title
end
def work_item_subscriptions(work_item)
work_item.reload.subscriptions.pluck(:user_id)
end
def work_item_sent_notifications(work_item)
work_item.reload.sent_notifications.pluck(:recipient_id)
end
let(:move) { WorkItems::DataSync::MoveService }
let(:clone) { WorkItems::DataSync::CloneService }
let_it_be(:users) { create_list(:user, 3) }
let_it_be(:thumbs_ups) { create_list(:award_emoji, 2, name: 'thumbsup', awardable: original_work_item) }
let_it_be(:thumbs_downs) { create_list(:award_emoji, 2, name: 'thumbsdown', awardable: original_work_item) }
let_it_be(:participant2) { create(:issue_email_participant, issue: original_work_item, email: 'user2@example.com') }
let_it_be(:award_emojis) { original_work_item.reload.award_emoji.pluck(:user_id, :name) }
let_it_be(:subscriptions) do
create_list(:subscription, 2, subscribable: original_work_item)
# create subscriptions for original work item and return subscribers as `expected_data` for later comparison.
original_work_item.reload.subscriptions.pluck(:user_id)
end
let_it_be(:notifications) do
create_list(:sent_notification, 2, noteable: original_work_item,
project: original_work_item.project, recipient: create(:user)
)
# create sent notification for original work item and return recipients as `expected_data` for later comparison.
original_work_item.reload.sent_notifications.pluck(:recipient_id)
end
let_it_be(:emails) do
create_list(:issue_email_participant, 2, issue: original_work_item)
# create email participants on original work item and return emails as `expected_data` for later comparison.
@ -109,14 +130,20 @@ RSpec.shared_examples 'cloneable and moveable widget data' do
end
where(:widget_name, :eval_value, :expected_data, :operations) do
:assignees | :work_item_assignees | ref(:assignees) | [ref(:move), ref(:clone)]
:award_emoji | :work_item_award_emoji | ref(:award_emojis) | [ref(:move)]
:email_participants | :work_item_emails | ref(:emails) | [ref(:move)]
:milestone | :work_item_milestone | ref(:milestone) | [ref(:move), ref(:clone)]
:assignees | :work_item_assignees | ref(:assignees) | [ref(:move), ref(:clone)]
:award_emoji | :work_item_award_emoji | ref(:award_emojis) | [ref(:move)]
:email_participants | :work_item_emails | ref(:emails) | [ref(:move)]
:milestone | :work_item_milestone | ref(:milestone) | [ref(:move), ref(:clone)]
:subscriptions | :work_item_subscriptions | ref(:subscriptions) | [ref(:move)]
:sent_notifications | :work_item_sent_notifications | ref(:notifications) | [ref(:move)]
end
with_them do
context "with widget" do
before do
allow(original_work_item).to receive(:from_service_desk?).and_return(true)
end
it 'clones and moves widget data' do
new_work_item = service.execute[:work_item]
widget_value = send(eval_value, new_work_item)

View File

@ -108,45 +108,4 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
end
end
end
context 'with history button' do
let(:ref) { 'master' }
it 'does not render the history button when show_history_button is not provided' do
render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit
}
expect(rendered).not_to have_css('#js-commit-history-link')
expect(rendered).not_to have_content('History')
end
it 'renders the history button when show_history_button is true' do
allow(view).to receive(:project_commits_path).and_return('/commits/123')
render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit,
show_history_button: true
}
expect(rendered).to have_css('#js-commit-history-link')
expect(rendered).to have_content('History')
end
it 'does not render the history button when show_history_button is false' do
render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit,
show_history_button: false
}
expect(rendered).not_to have_css('#js-commit-history-link')
expect(rendered).not_to have_content('History')
end
end
end

View File

@ -11,6 +11,10 @@ RSpec.describe Search::Worker, feature_category: :global_search do
include ApplicationWorker
include ::Search::Worker
def perform
logger.info 'Worker start'
end
end
end
@ -26,4 +30,12 @@ RSpec.describe Search::Worker, feature_category: :global_search do
expect(Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap.limit_for(worker: worker_class)).to eq(limit)
end
it 'uses a logger built with Gitlab::Elasticsearch::Logger' do
logger = instance_double(Gitlab::Elasticsearch::Logger)
expect(Gitlab::Elasticsearch::Logger).to receive(:build).and_return(logger)
expect(logger).to receive(:info).with('Worker start')
worker.perform
end
end

View File

@ -6740,10 +6740,10 @@ domexception@^4.0.0:
dependencies:
webidl-conversions "^7.0.0"
dompurify@^3.0.5, dompurify@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
dompurify@^3.0.5, dompurify@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.0.tgz#53c414317c51503183696fcdef6dd3f916c607ed"
integrity sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==
dropzone@^4.2.0:
version "4.2.0"