Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-21 21:14:46 +00:00
parent d5ff067431
commit a3e6d34643
69 changed files with 861 additions and 410 deletions

3
.gitignore vendored
View File

@ -107,6 +107,9 @@ tags.lock
tags.temp
.stylelintcache
.solargraph.yml
jest-snapshot-test-match.json
jest-test-report.json
jest-snapshot-test-report.json
# Vite Ruby
/public/vite*

View File

@ -306,6 +306,42 @@ jest-integration:
- run_timed_command "yarn jest:integration --ci"
needs: ["rspec-all frontend_fixture", "graphql-schema-dump"]
jest-snapshot-vue3:
extends:
- .jest-base
- .frontend:rules:jest-snapshot
needs: ["rspec-all frontend_fixture"]
variables:
VUE_VERSION: 3
JEST_REPORT: jest-test-report.json
SNAPSHOT_TEST_REPORT: jest-snapshot-test-report.json
script:
- |
yarn jest:snapshots --ci --json --outputFile="${JEST_REPORT}" || echo 'Proceed to parsing test report...'
echo $(ruby -rjson -e 'puts JSON.generate(JSON.parse(File.read(ENV["JEST_REPORT"])).dig("snapshot"))') > "${SNAPSHOT_TEST_REPORT}"
echo " ============= snapshot test report start =============="
cat "${SNAPSHOT_TEST_REPORT}"
echo " ============= snapshot test report end ================"
snapshot_test_failed=$(ruby -rjson -e 'puts JSON.parse(File.read(ENV["SNAPSHOT_TEST_REPORT"])).dig("failure")')
if [[ "${snapshot_test_failed}" == "true" ]]
then
echo "You have failed snapshot tests! Exiting 1..."
exit 1
else
echo 'All snapshot tests passed! Exiting 0...'
exit 0
fi
artifacts:
name: snapshot_tests
expire_in: 31d
when: always
paths:
- jest-snapshot-test-match.json
- jest-snapshot-test-report.json
coverage-frontend:
extends:
- .default-retry

View File

@ -1390,6 +1390,26 @@
changes: *frontend-build-patterns
allow_failure: true
.frontend:rules:jest-snapshot:
rules:
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-fork-merge-request
when: never
- <<: *if-merge-request-labels-run-all-jest
when: manual
allow_failure: true
- <<: *if-merge-request-labels-frontend-and-feature-flag
when: manual
allow_failure: true
- <<: *if-merge-request
changes: *frontend-dependency-patterns
when: manual
allow_failure: true
- <<: *if-merge-request
changes: [".gitlab/ci/rules.gitlab-ci.yml", ".gitlab/ci/frontend.gitlab-ci.yml"]
allow_failure: true
################
# Memory rules #
################

View File

@ -64,7 +64,6 @@ export default {
},
computed: {
getDrawerHeaderHeight() {
if (!this.showActionsDrawer || gon.use_new_navigation) return '0';
return getContentWrapperHeight();
},
isFormValid() {

View File

@ -25,11 +25,10 @@ export default {
resolvedStatusMessage() {
let message;
const discussionResolved = this.isDiscussionResolved(
this.draft ? this.draft.discussion_id : this.discussionId,
'draft' in this ? this.draft.discussion_id : this.discussionId,
);
const discussionToBeResolved = this.draft
? this.draft.resolve_discussion
: this.resolveDiscussion;
const discussionToBeResolved =
'draft' in this ? this.draft.resolve_discussion : this.resolveDiscussion;
if (discussionToBeResolved && discussionResolved && !this.$options.showStaysResolved) {
return undefined;

View File

@ -538,13 +538,10 @@ const GLOBAL_SHORTCUTS_GROUP = {
GO_TO_YOUR_TODO_LIST,
TOGGLE_PERFORMANCE_BAR,
HIDE_APPEARING_CONTENT,
TOGGLE_SUPER_SIDEBAR,
],
};
if (gon.use_new_navigation) {
GLOBAL_SHORTCUTS_GROUP.keybindings.push(TOGGLE_SUPER_SIDEBAR);
}
export const EDITING_SHORTCUTS_GROUP = {
id: 'editing',
name: __('Editing'),

View File

@ -198,11 +198,7 @@ export default class Shortcuts {
}
static focusSearch(e) {
if (gon.use_new_navigation) {
document.querySelector('#super-sidebar-search')?.click();
} else {
document.querySelector('#search')?.focus();
}
document.querySelector('#super-sidebar-search')?.click();
if (e.preventDefault) {
e.preventDefault();

View File

@ -67,7 +67,7 @@ export default {
return featureFlag.iid ? `^${featureFlag.iid}` : '';
},
canDeleteFlag(flag) {
return !this.permissions || (flag.scopes || []).every((scope) => scope.can_update);
return (flag.scopes || []).every((scope) => scope.can_update);
},
setDeleteModalData(featureFlag) {
this.deleteFeatureFlagUrl = featureFlag.destroy_path;

View File

@ -1,10 +1,8 @@
// TODO: Remove this with the removal of the old navigation.
// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
import Vue from 'vue';
import { highCountTrim } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
import Translate from '~/vue_shared/translate';
/**
* Updates todo counter when todos are toggled.
@ -29,76 +27,6 @@ export default function initTodoToggle() {
});
}
export function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
if (setStatusModalTriggerEl) {
setStatusModalTriggerEl.addEventListener('click', () => {
const topNavbar = document.querySelector('.navbar-gitlab');
const buttonWithinTopNav = topNavbar && topNavbar.contains(setStatusModalTriggerEl);
Tracking.event(undefined, 'click_button', {
label: 'user_edit_status',
property: buttonWithinTopNav ? 'navigation_top' : 'nav_user_menu',
});
import(
/* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue'
)
.then(({ default: SetStatusModalWrapper }) => {
const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
const statusModalElement = document.createElement('div');
setStatusModalWrapperEl.appendChild(statusModalElement);
Vue.use(Translate);
// eslint-disable-next-line no-new
new Vue({
el: statusModalElement,
data() {
const {
currentEmoji,
defaultEmoji,
currentMessage,
currentAvailability,
currentClearStatusAfter,
} = setStatusModalWrapperEl.dataset;
return {
currentEmoji,
defaultEmoji,
currentMessage,
currentAvailability,
currentClearStatusAfter,
};
},
render(createElement) {
const {
currentEmoji,
defaultEmoji,
currentMessage,
currentAvailability,
currentClearStatusAfter,
} = this;
return createElement(SetStatusModalWrapper, {
props: {
currentEmoji,
defaultEmoji,
currentMessage,
currentAvailability,
currentClearStatusAfter,
},
});
},
});
})
.catch(() => {});
});
setStatusModalTriggerEl.classList.add('ready');
}
}
function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
const { trackLabel, trackProperty } = elToTrack.dataset;
@ -119,7 +47,4 @@ export function initNavUserDropdownTracking() {
}
}
if (!gon?.use_new_navigation) {
requestIdleCallback(initStatusTriggers);
}
requestIdleCallback(initNavUserDropdownTracking);

View File

@ -187,6 +187,11 @@ export default {
return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0;
},
},
watch: {
title() {
this.updateCommitMessage();
},
},
mounted() {
if (!this.commitMessage) this.updateCommitMessage();
@ -321,7 +326,6 @@ export default {
:required="true"
:autofocus="!pageInfo.persisted"
:placeholder="$options.i18n.title.placeholder"
@input="updateCommitMessage"
/>
</gl-form-group>
</div>
@ -361,8 +365,8 @@ export default {
:drawio-enabled="drawioEnabled"
@contentEditor="notifyContentEditorActive"
@markdownField="notifyContentEditorInactive"
@keydown.ctrl.enter="submitFormShortcut"
@keydown.meta.enter="submitFormShortcut"
@keydown.ctrl.enter="submitFormWithShortcut"
@keydown.meta.enter="submitFormWithShortcut"
/>
<div class="form-text gl-text-gray-600">
<gl-sprintf

View File

@ -12,3 +12,5 @@ export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
export const SET_STATUS_MODAL_ID = 'set-user-status-modal';

View File

@ -2,17 +2,18 @@
import { GlToast, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import { createAlert } from '~/alert';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isUserBusy, computedClearStatusAfterValue } from './utils';
import { AVAILABILITY_STATUS } from './constants';
import { AVAILABILITY_STATUS, SET_STATUS_MODAL_ID } from './constants';
import SetStatusForm from './set_status_form.vue';
Vue.use(GlToast);
export default {
SET_STATUS_MODAL_ID,
components: {
GlModal,
SetStatusForm,
@ -29,11 +30,13 @@ export default {
},
currentEmoji: {
type: String,
required: true,
required: false,
default: '',
},
currentMessage: {
type: String,
required: true,
required: false,
default: '',
},
currentAvailability: {
type: String,
@ -51,7 +54,6 @@ export default {
defaultEmojiTag: '',
emoji: this.currentEmoji,
message: this.currentMessage,
modalId: 'set-user-status-modal',
availability: isUserBusy(this.currentAvailability),
clearStatusAfter: null,
};
@ -65,11 +67,11 @@ export default {
},
},
mounted() {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
this.$emit('mounted');
},
methods: {
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
this.$root.$emit(BV_HIDE_MODAL, SET_STATUS_MODAL_ID);
},
removeStatus() {
this.availability = false;
@ -132,7 +134,7 @@ export default {
<template>
<gl-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:modal-id="$options.SET_STATUS_MODAL_ID"
:action-primary="$options.actionPrimary"
:action-secondary="$options.actionSecondary"
modal-class="set-user-status-modal"

View File

@ -0,0 +1,72 @@
<script>
export default {
name: 'ScrollScrim',
data() {
return {
topBoundaryVisible: true,
bottomBoundaryVisible: true,
};
},
computed: {
scrimClasses() {
return {
'top-scrim-visible': !this.topBoundaryVisible,
'bottom-scrim-visible gl-border-b': !this.bottomBoundaryVisible,
};
},
},
mounted() {
this.observeScroll();
},
beforeDestroy() {
this.scrollObserver?.disconnect();
},
methods: {
observeScroll() {
const root = this.$el;
const options = {
rootMargin: '8px',
root,
threshold: 1.0,
};
this.scrollObserver?.disconnect();
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
this[entry.target?.$__visibilityProp] = entry.isIntersecting;
});
}, options);
const topBoundary = this.$refs['top-boundary'];
const bottomBoundary = this.$refs['bottom-boundary'];
topBoundary.$__visibilityProp = 'topBoundaryVisible';
observer.observe(topBoundary);
bottomBoundary.$__visibilityProp = 'bottomBoundaryVisible';
observer.observe(bottomBoundary);
this.scrollObserver = observer;
},
},
};
</script>
<template>
<div class="gl-scroll-scrim gl-overflow-auto" :class="scrimClasses">
<div class="top-scrim-wrapper">
<div class="top-scrim"></div>
</div>
<div ref="top-boundary"></div>
<slot></slot>
<div ref="bottom-boundary"></div>
<div class="bottom-scrim-wrapper">
<div class="bottom-scrim"></div>
</div>
</div>
</template>

View File

@ -194,7 +194,7 @@ export default {
/>
<ul
aria-labelledby="super-sidebar-context-header"
class="gl-p-0 gl-list-style-none"
class="gl-p-0 gl-mb-0 gl-list-style-none"
data-testid="non-static-items-section"
>
<template v-for="item in nonStaticItems">

View File

@ -20,6 +20,7 @@ import HelpCenter from './help_center.vue';
import SidebarMenu from './sidebar_menu.vue';
import SidebarPeekBehavior from './sidebar_peek_behavior.vue';
import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue';
import ScrollScrim from './scroll_scrim.vue';
export default {
components: {
@ -30,6 +31,7 @@ export default {
SidebarPeekBehavior,
SidebarHoverPeekBehavior,
SidebarPortalTarget,
ScrollScrim,
TrialStatusWidget: () =>
import('ee_component/contextual_sidebar/components/trial_status_widget.vue'),
TrialStatusPopover: () =>
@ -202,7 +204,7 @@ export default {
<div
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"
>
<div class="gl-flex-grow-1 gl-overflow-auto" data-testid="nav-container">
<scroll-scrim class="gl-flex-grow-1" data-testid="nav-container">
<div
id="super-sidebar-context-header"
class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-weight-bold gl-font-sm super-sidebar-context-header"
@ -218,8 +220,8 @@ export default {
:update-pins-url="sidebarData.update_pins_url"
/>
<sidebar-portal-target />
</div>
<div class="gl-p-3">
</scroll-scrim>
<div class="gl-p-2">
<help-center ref="helpCenter" :sidebar-data="sidebarData" />
<gl-button
v-if="sidebarData.is_admin"

View File

@ -5,11 +5,13 @@ import {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlButton,
GlModalDirective,
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
import { SET_STATUS_MODAL_ID } from '~/set_status_modal/constants';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
import UserMenuProfileItem from './user_menu_profile_item.vue';
@ -18,6 +20,7 @@ const DROPDOWN_X_OFFSET_BASE = -211;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
SET_STATUS_MODAL_ID,
i18n: {
setStatus: s__('SetStatusModal|Set status'),
editStatus: s__('SetStatusModal|Edit status'),
@ -36,9 +39,14 @@ export default {
GlDisclosureDropdownItem,
GlButton,
UserMenuProfileItem,
SetStatusModal: () =>
import(
/* webpackChunkName: 'statusModalBundle' */ '~/set_status_modal/set_status_modal_wrapper.vue'
),
},
directives: {
SafeHtml,
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
inject: ['isImpersonating'],
@ -48,6 +56,11 @@ export default {
type: Object,
},
},
data() {
return {
setStatusModalReady: false,
};
},
computed: {
toggleText() {
return sprintf(__('%{user} users menu'), { user: this.data.name });
@ -61,7 +74,8 @@ export default {
return {
text: statusLabel,
extraAttrs: {
class: 'js-set-status-modal-trigger',
...USER_MENU_TRACKING_DEFAULTS,
'data-track-label': 'user_edit_status',
},
};
},
@ -140,24 +154,22 @@ export default {
};
},
statusModalData() {
const defaultData = {
'data-current-emoji': '',
'data-current-message': '',
'data-default-emoji': 'speech_balloon',
};
if (!this.data?.status?.can_update) {
return null;
}
const { busy, customized } = this.data.status;
if (!busy && !customized) {
return defaultData;
return {};
}
const { emoji, message, availability, clear_after: clearAfter } = this.data.status;
return {
...defaultData,
'data-current-emoji': this.data.status.emoji,
'data-current-message': this.data.status.message,
'data-current-availability': this.data.status.availability,
'data-current-clear-status-after': this.data.status.clear_after,
'current-emoji': emoji || '',
'current-message': message || '',
'current-availability': availability || '',
'current-clear-status-after': clearAfter || '',
};
},
buyPipelineMinutesCalloutData() {
@ -248,7 +260,8 @@ export default {
<gl-disclosure-dropdown-group bordered>
<gl-disclosure-dropdown-item
v-if="data.status.can_update"
v-if="setStatusModalReady && statusModalData"
v-gl-modal="$options.SET_STATUS_MODAL_ID"
:item="statusItem"
data-testid="status-item"
@action="closeDropdown"
@ -304,11 +317,11 @@ export default {
@action="trackSignOut"
/>
</gl-disclosure-dropdown>
<div
v-if="data.status.can_update"
class="js-set-status-modal-wrapper"
<set-status-modal
v-if="statusModalData"
default-emoji="speech_balloon"
v-bind="statusModalData"
></div>
@mounted="setStatusModalReady = true"
/>
</div>
</template>

View File

@ -3,7 +3,6 @@ import { GlToast } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { initStatusTriggers } from '../header';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
import createStore from './components/global_search/store';
import {
@ -153,5 +152,3 @@ export const initSuperSidebarToggle = () => {
},
});
};
requestIdleCallback(initStatusTriggers);

View File

@ -86,11 +86,7 @@ export default {
},
showSuperSidebarToggle() {
return gon.use_new_navigation && sidebarState.isCollapsed;
},
topBarClasses() {
return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
return sidebarState.isCollapsed;
},
},
@ -124,7 +120,7 @@ export default {
<template>
<div>
<div :class="topBarClasses" data-testid="top-bar">
<div class="top-bar-fixed container-fluid" data-testid="top-bar">
<div
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>

View File

@ -26,7 +26,6 @@
@import 'framework/highlight';
@import 'framework/lists';
@import 'framework/logo';
@import 'framework/job_log';
@import 'framework/markdown_area';
@import 'framework/media_object';
@import 'framework/modal';

View File

@ -1,47 +0,0 @@
.job-log {
font-family: $monospace-font;
padding: $gl-padding-8 $input-horizontal-padding;
margin: 0 0 $gl-padding-8;
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
color: color-yiq($builds-log-bg);
border-radius: 0 0 $border-radius-default $border-radius-default;
min-height: 42px;
background-color: $builds-log-bg;
}
.log-line {
padding: 1px $gl-padding-8 1px $job-log-line-padding;
min-height: $gl-line-height-20;
}
.line-number {
color: $gray-500;
padding: 0 $gl-padding-8;
min-width: $job-line-number-width;
margin-left: -$job-line-number-margin;
padding-right: 1em;
user-select: none;
&:hover,
&:active,
&:visited {
text-decoration: underline;
color: $gray-500;
}
}
.collapsible-line {
&:hover {
background-color: rgba($white, 0.2);
}
.arrow {
margin-left: -$job-arrow-margin;
}
}
.loader-animation {
@include build-loader-animation;
}

View File

@ -404,3 +404,57 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
}
// Styles for the ScrollScrim component.
// Should eventually be moved to gitlab-ui.
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1869
$scroll-scrim-height: 2.25rem;
.gl-scroll-scrim {
.top-scrim-wrapper,
.bottom-scrim-wrapper {
height: $scroll-scrim-height;
opacity: 0;
position: sticky;
z-index: 1;
display: block;
left: 0;
right: 0;
pointer-events: none;
transition: opacity 0.1s;
}
.top-scrim-wrapper {
top: 0;
margin-bottom: -$scroll-scrim-height;
.top-scrim {
background: linear-gradient(180deg, var(--sidebar-background, $gray-10) 0%, $transparent-rgba 100%);
}
}
.bottom-scrim-wrapper {
bottom: 0;
margin-top: -$scroll-scrim-height;
.bottom-scrim {
background: linear-gradient(180deg, $transparent-rgba 0%, var(--sidebar-background, $gray-10));
}
}
.top-scrim,
.bottom-scrim {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&.top-scrim-visible .top-scrim-wrapper,
&.bottom-scrim-visible .bottom-scrim-wrapper {
opacity: 1;
}
}

View File

@ -166,3 +166,51 @@
margin-bottom: 0;
}
}
.job-log {
font-family: $monospace-font;
padding: $gl-padding-8 $input-horizontal-padding;
margin: 0 0 $gl-padding-8;
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
color: color-yiq($builds-log-bg);
border-radius: 0 0 $border-radius-default $border-radius-default;
min-height: 42px;
background-color: $builds-log-bg;
}
.log-line {
padding: 1px $gl-padding-8 1px $job-log-line-padding;
min-height: $gl-line-height-20;
}
.line-number {
color: $gray-500;
padding: 0 $gl-padding-8;
min-width: $job-line-number-width;
margin-left: -$job-line-number-margin;
padding-right: 1em;
user-select: none;
&:hover,
&:active,
&:visited {
text-decoration: underline;
color: $gray-500;
}
}
.collapsible-line {
&:hover {
background-color: rgba($white, 0.2);
}
.arrow {
margin-left: -$job-arrow-margin;
}
}
.loader-animation {
@include build-loader-animation;
}

View File

@ -1722,6 +1722,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED', value: ProtectedBranch.protected?(target_project, target_branch).to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION', value: description)
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?

View File

@ -171,6 +171,8 @@ module Projects
project.import_url,
schemes: Project::VALID_IMPORT_PROTOCOLS,
ports: Project::VALID_IMPORT_PORTS,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
dns_rebind_protection: dns_rebind_protection?)
.then do |(import_url, resolved_host)|
next '' if resolved_host.nil? || !import_url.scheme.in?(%w[http https])
@ -179,6 +181,11 @@ module Projects
end
end
def allow_local_requests?
Rails.env.development? && # There is no known usecase for this in non-development environments
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
def dns_rebind_protection?
return false if Gitlab.http_proxy_env?

View File

@ -613,8 +613,8 @@ module Gitlab
# https://github.com/rails/rails/blob/fdf840f69a2e33d78a9d40b91d9b7fddb76711e9/activerecord/lib/active_record/railtie.rb#L308
initializer :clear_active_connections_again, after: :set_routes_reloader_hook do
# rubocop:disable Database/MultipleDatabases
ActiveRecord::Base.clear_active_connections!
ActiveRecord::Base.flush_idle_connections!
ActiveRecord::Base.connection_handler.clear_active_connections!(ActiveRecord::Base.current_role)
ActiveRecord::Base.connection_handler.flush_idle_connections!(ActiveRecord::Base.current_role)
# rubocop:enable Database/MultipleDatabases
end

View File

@ -1,8 +0,0 @@
---
name: custom_roles_ui_saas
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130089
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423077
milestone: '16.4'
type: development
group: group::authorization
default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '16.5'
type: ops
group: group::global search
default_enabled: false
default_enabled: true

View File

@ -13,7 +13,11 @@ SUGGEST_COMMENT
def check_yaml(saas_feature)
mr_group_label = helper.group_label
message_for_missing_group!(saas_feature: saas_feature, mr_group_label: mr_group_label) if saas_feature.group.nil?
if saas_feature.group.nil?
message_for_missing_group!(saas_feature: saas_feature, mr_group_label: mr_group_label)
else
message_for_group!(saas_feature: saas_feature, mr_group_label: mr_group_label)
end
rescue Psych::Exception
# YAML could not be parsed, fail the build.
fail "#{helper.html_link(saas_feature.path)} isn't valid YAML! #{SEE_DOC.capitalize}"

View File

@ -1,7 +1,9 @@
---
migration_job_name: BackfillHasRemediationsOfVulnerabilityReads
description: Backfills has_remediations column for vulnerability_reads table.
Originally introduced via https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133714
RE-ran because there was a error in remediation ingestion logic.
feature_category: database
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133714
milestone: 16.5
milestone: 16.7
queued_migration_version: 20231011142714

View File

@ -9,19 +9,15 @@ class QueueBackfillHasRemediationsOfVulnerabilityReads < Gitlab::Database::Migra
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
def up
queue_batched_background_migration(
MIGRATION,
:vulnerability_reads,
:vulnerability_id,
job_interval: DELAY_INTERVAL,
queued_migration_version: '20231011142714',
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
# per: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#requeuing-batched-background-migrations
# > When you requeue the batched background migration, turn the original queuing
# > into a no-op by clearing up the #up and #down methods of the migration
# > performing the requeuing. Otherwise, the batched background migration is
# > queued multiple times on systems that are upgrading multiple patch releases
# > at once.
#
# being re-run via https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135747
def up; end
def down
delete_batched_background_migration(MIGRATION, :vulnerability_reads, :vulnerability_id, [])
end
def down; end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
# rubocop: disable BackgroundMigration/DictionaryFile -- queued/introduced before the rule is introduced
class RequeueBackfillHasRemediationsOfVulnerabilityReads < Gitlab::Database::Migration[2.2]
milestone '16.7'
MIGRATION = "BackfillHasRemediationsOfVulnerabilityReads"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 10_000
SUB_BATCH_SIZE = 50
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
def up
queue_batched_background_migration(
MIGRATION,
:vulnerability_reads,
:vulnerability_id,
job_interval: DELAY_INTERVAL,
queued_migration_version: '20231031204841',
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :vulnerability_reads, :vulnerability_id, [])
end
end
# rubocop: enable BackgroundMigration/DictionaryFile

View File

@ -0,0 +1 @@
a1bbcd9430acc48bc271dd041c2999932d24d15bfa2ef8766d7bf9920d2d3539

View File

@ -162,13 +162,15 @@ When you remove a merge request from a merge train:
## Skip the merge train and merge immediately
If you have a high-priority merge request, like a critical patch that must
be merged urgently, select **Merge Immediately**.
be merged urgently, you can select **Merge Immediately**.
When you merge a merge request immediately:
- The current merge train is recreated.
- All pipelines restart.
- Redundant pipelines [are cancelled](#automatic-pipeline-cancellation).
- The commits from the merge request are merged, ignoring the status of the merge train.
- The merge train pipelines for all other merge requests on the train [are cancelled](#automatic-pipeline-cancellation).
- A new merge train starts and all the merge requests from the original merge train are added to this new merge train,
with a new merge train pipeline for each. These new merge train pipelines now contain
the commits added by the merge request that was merged immediately.
WARNING:
Merging immediately can use a lot of CI/CD resources. Use this option

View File

@ -158,6 +158,7 @@ These variables are available when:
| `CI_MERGE_REQUEST_APPROVED` | 14.1 | all | Approval status of the merge request. `true` when [merge request approvals](../../user/project/merge_requests/approvals/index.md) is available and the merge request has been approved. |
| `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of usernames of assignees for the merge request. |
| `CI_MERGE_REQUEST_ID` | 11.6 | all | The instance-level ID of the merge request. This is a unique ID across all projects on the GitLab instance. |
| `CI_MERGE_REQUEST_DESCRIPTION` | 16.7 | all | The description of the merge request. |
| `CI_MERGE_REQUEST_IID` | 11.6 | all | The project-level IID (internal ID) of the merge request. This ID is unique for the current project, and is the number used in the merge request URL, page title, and other visible locations. |
| `CI_MERGE_REQUEST_LABELS` | 11.9 | all | Comma-separated label names of the merge request. |
| `CI_MERGE_REQUEST_MILESTONE` | 11.9 | all | The milestone title of the merge request. |

View File

@ -4,4 +4,6 @@ group: ModelOps
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# Data Science
- [Model Registry](model_registry/index.md)

View File

@ -1495,11 +1495,43 @@ Logging helps track events for debugging. Logging also allows the application to
- An audit trail for log edits must be available.
- To avoid data loss, logs must be saved on different storage.
### Who to contact if you have questions
## URL Spoofing
We want to protect our users from bad actors who might try to use GitLab
features to redirect other users to malicious sites.
Many features in GitLab allow users to post links to external websites. It is
important that the destination of any user-specified link is made very clear
to the user.
### `external_redirect_path`
When presenting links provided by users, if the actual URL is hidden, use the `external_redirect_path`
helper method to redirect the user to a warning page first. For example:
```ruby
# Bad :(
# This URL comes from User-Land and may not be safe...
# We need the user to *see* where they are going.
link_to foo_social_url(@user), title: "Foo Social" do
sprite_icon('question-o')
end
# Good :)
# The external_redirect "leaving GitLab" page will show the URL to the user
# before they leave.
link_to external_redirect_path(url: foo_social_url(@user)), title: "Foo" do
sprite_icon('question-o')
end
```
Also see this [real-life usage](https://gitlab.com/gitlab-org/gitlab/-/blob/bdba5446903ff634fb12ba695b2de99b6d6881b5/app/helpers/application_helper.rb#L378) as an example.
## Who to contact if you have questions
For general guidance, contact the [Application Security](https://about.gitlab.com/handbook/security/security-engineering/application-security/) team.
### Related topics
## Related topics
- [Log system in GitLab](../administration/logs/index.md)
- [Audit event development guidelines](../development/audit_event_guide/index.md))

46
jest.config.snapshots.js Normal file
View File

@ -0,0 +1,46 @@
const fs = require('fs');
const path = require('path');
const baseConfig = require('./jest.config.base');
function findSnapshotTestsFromDir(dir, results = []) {
fs.readdirSync(dir).forEach((file) => {
const fullPath = path.join(dir, file);
if (fs.lstatSync(fullPath).isDirectory()) {
findSnapshotTestsFromDir(fullPath, results);
} else {
const fileContent = fs.readFileSync(fullPath, 'utf8');
if (/toMatchSnapshot|toMatchInlineSnapshot/.test(fileContent)) {
results.push(`<rootDir>/${fullPath}`);
}
}
});
return results;
}
function saveArrayToFile(array, fileName) {
fs.writeFile(fileName, JSON.stringify(array, null, 2), (err) => {
if (err) {
console.error(`Error writing Array data to ${fileName}:`, err);
}
});
}
module.exports = () => {
const testMatch = [
...findSnapshotTestsFromDir('spec/frontend'),
...findSnapshotTestsFromDir('ee/spec/frontend'),
];
const { CI, SNAPSHOT_TEST_MATCH_FILE } = process.env;
if (CI && SNAPSHOT_TEST_MATCH_FILE) {
saveArrayToFile(testMatch, SNAPSHOT_TEST_MATCH_FILE);
}
return {
...baseConfig('spec/frontend'),
roots: ['<rootDir>/spec/frontend'],
rootsEE: ['<rootDir>/ee/spec/frontend'],
rootsJH: ['<rootDir>/jh/spec/frontend'],
testMatch,
};
};

View File

@ -20,6 +20,8 @@ module Gitlab
def perform
each_sub_batch do |sub_batch|
reset_has_remediations_attribute(sub_batch)
update_query = update_query_for(sub_batch)
connection.execute(update_query)
@ -28,6 +30,10 @@ module Gitlab
private
def reset_has_remediations_attribute(sub_batch)
sub_batch.update_all(has_remediations: false)
end
def update_query_for(sub_batch)
subquery = sub_batch.joins("
INNER JOIN vulnerability_occurrences ON

View File

@ -10,9 +10,8 @@ module Gitlab
class BaseInput
ArgumentNotValidError = Class.new(StandardError)
# Checks whether the class matches the type in the specification
def self.matches?(spec)
raise NotImplementedError
spec.is_a?(Hash) && spec[:type] == type_name
end
# Human readable type used in error messages

View File

@ -8,10 +8,6 @@ module Gitlab
class BooleanInput < BaseInput
extend ::Gitlab::Utils::Override
def self.matches?(spec)
spec.is_a?(Hash) && spec[:type] == type_name
end
def self.type_name
'boolean'
end

View File

@ -8,10 +8,6 @@ module Gitlab
class NumberInput < BaseInput
extend ::Gitlab::Utils::Override
def self.matches?(spec)
spec.is_a?(Hash) && spec[:type] == type_name
end
def self.type_name
'number'
end

View File

@ -17,7 +17,7 @@ module Gitlab
# inputs:
# foo:
# ```
spec.nil? || (spec.is_a?(Hash) && [nil, type_name].include?(spec[:type]))
spec.nil? || super || (spec.is_a?(Hash) && !spec.key?(:type))
end
def self.type_name

View File

@ -548,43 +548,10 @@ module Gitlab
end
end
def self.storage_metadata_file_path(storage)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
File.join(
Gitlab.config.repositories.storages[storage].legacy_disk_path, GITALY_METADATA_FILENAME
)
end
end
def self.can_use_disk?(storage)
cached_value = MUTEX.synchronize do
@can_use_disk ||= {}
@can_use_disk[storage]
end
return cached_value unless cached_value.nil?
gitaly_filesystem_id = filesystem_id(storage)
direct_filesystem_id = filesystem_id_from_disk(storage)
MUTEX.synchronize do
@can_use_disk[storage] = gitaly_filesystem_id.present? &&
gitaly_filesystem_id == direct_filesystem_id
end
end
def self.filesystem_id(storage)
Gitlab::GitalyClient::ServerService.new(storage).storage_info&.filesystem_id
end
def self.filesystem_id_from_disk(storage)
metadata_file = File.read(storage_metadata_file_path(storage))
metadata_hash = Gitlab::Json.parse(metadata_file)
metadata_hash['gitaly_filesystem_id']
rescue Errno::ENOENT, Errno::EACCES, JSON::ParserError
nil
end
def self.filesystem_disk_available(storage)
Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.available
end

View File

@ -11,15 +11,17 @@ module InitializerConnections
def self.raise_if_new_database_connection
return yield if Gitlab::Utils.to_boolean(ENV['SKIP_RAISE_ON_INITIALIZE_CONNECTIONS'])
previous_connection_counts = ActiveRecord::Base.connection_handler.connection_pool_list.to_h do |pool|
[pool.db_config.name, pool.connections.size]
end
previous_connection_counts =
ActiveRecord::Base.connection_handler.connection_pool_list(ApplicationRecord.current_role).to_h do |pool|
[pool.db_config.name, pool.connections.size]
end
yield
new_connection_counts = ActiveRecord::Base.connection_handler.connection_pool_list.to_h do |pool|
[pool.db_config.name, pool.connections.size]
end
new_connection_counts =
ActiveRecord::Base.connection_handler.connection_pool_list(ApplicationRecord.current_role).to_h do |pool|
[pool.db_config.name, pool.connections.size]
end
raise_database_connection_made_error unless previous_connection_counts == new_connection_counts
end

View File

@ -87,7 +87,11 @@ namespace :gitlab do
# Skip if databases are yet to be provisioned
next unless connection[:identifier] && shared_connection[:identifier]
unless connection[:identifier] == shared_connection[:identifier]
connection_identifier, shared_connection_identifier = [
connection[:identifier], shared_connection[:identifier]
].map { |identifier| identifier.slice("system_identifier", "current_database") }
unless connection_identifier == shared_connection_identifier
warnings << "- The '#{connection[:name]}' since it is using 'database_tasks: false' " \
"should share database with '#{share_with}:'."
end

View File

@ -23069,6 +23069,9 @@ msgstr ""
msgid "GroupSAML|Could not create SAML group link: %{errors}."
msgstr ""
msgid "GroupSAML|Custom roles"
msgstr ""
msgid "GroupSAML|Default membership role"
msgstr ""
@ -23168,6 +23171,9 @@ msgstr ""
msgid "GroupSAML|Some to-do items may be hidden because your SAML session has expired. Select the groups path to reauthenticate and view the hidden to-do items."
msgstr ""
msgid "GroupSAML|Standard roles"
msgstr ""
msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to %{linkStart}reset it%{linkEnd}."
msgstr ""
@ -56719,6 +56725,9 @@ msgstr ""
msgid "ciReport|Automatically apply the patch in a new branch"
msgstr ""
msgid "ciReport|Automatically opens a merge request with a solution generated by AI"
msgstr ""
msgid "ciReport|Base pipeline codequality artifact not found"
msgstr ""
@ -56901,6 +56910,9 @@ msgstr ""
msgid "ciReport|RPS"
msgstr ""
msgid "ciReport|Resolve with AI"
msgstr ""
msgid "ciReport|Resolve with merge request"
msgstr ""

View File

@ -18,6 +18,7 @@
"jest:integration": "jest --config jest.config.integration.js",
"jest:scripts": "jest --config jest.config.scripts.js",
"jest:quarantine": "grep -r 'quarantine:' spec/frontend ee/spec/frontend",
"jest:snapshots": "jest --config jest.config.snapshots.js",
"lint:eslint": "node scripts/frontend/eslint.js",
"lint:eslint:fix": "node scripts/frontend/eslint.js --fix",
"lint:eslint:all": "node scripts/frontend/eslint.js .",

View File

@ -161,6 +161,7 @@ gitlab-runner:
memory: 150Mi
nodeSelector:
preemptible: "true"
terminationGracePeriodSeconds: 60 # Wait for 1min before killing gitlab-runner
podAnnotations:
<<: *safe-to-evict

View File

@ -25,6 +25,7 @@ module Trigger
class Base
# Can be overridden
STABLE_BRANCH_REGEX = /^[\d-]+-stable(-ee|-jh)?$/
def self.access_token
ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE']
end
@ -113,21 +114,33 @@ module Trigger
end
def stable_branch?
ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee|-jh)?$/
ENV['CI_COMMIT_REF_NAME'] =~ STABLE_BRANCH_REGEX
end
def mr_target_stable_branch?
ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] =~ STABLE_BRANCH_REGEX
end
def fallback_ref
if trigger_stable_branch_if_detected? && stable_branch?
if ENV['CI_PROJECT_NAMESPACE'] == 'gitlab-cn'
ENV['CI_COMMIT_REF_NAME'].delete_suffix('-jh')
elsif ENV['CI_PROJECT_NAMESPACE'] == 'gitlab-org'
ENV['CI_COMMIT_REF_NAME'].delete_suffix('-ee')
end
return primary_ref unless trigger_stable_branch_if_detected?
if stable_branch?
normalize_stable_branch_name(ENV['CI_COMMIT_REF_NAME'])
elsif mr_target_stable_branch?
normalize_stable_branch_name(ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'])
else
primary_ref
end
end
def normalize_stable_branch_name(branch_name)
if ENV['CI_PROJECT_NAMESPACE'] == 'gitlab-cn'
branch_name.delete_suffix('-jh')
elsif ENV['CI_PROJECT_NAMESPACE'] == 'gitlab-org'
branch_name.delete_suffix('-ee')
end
end
def ref
ENV.fetch(ref_param_name, fallback_ref)
end

View File

@ -188,7 +188,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
context 'user status', :js do
def select_emoji(emoji_name, is_modal = false)
def select_emoji(emoji_name)
toggle_button = find('.emoji-menu-toggle-button')
toggle_button.click
emoji_button = find("gl-emoji[data-name=\"#{emoji_name}\"]")
@ -330,10 +330,12 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
find_by_testid('user-dropdown').click
within_testid('user-dropdown') do
find('.js-set-status-modal-trigger.ready')
expect(page).to have_button(text: button_text, visible: :visible)
click_button button_text
end
expect(page.find('#set-user-status-modal')).to be_visible
end
def open_user_status_modal
@ -386,7 +388,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
it 'adds emoji to user status' do
emoji = 'grinning'
open_user_status_modal
select_emoji(emoji, true)
select_emoji(emoji)
set_user_status_in_modal
visit_user
@ -415,7 +417,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
it 'opens the emoji modal again after closing it' do
open_user_status_modal
select_emoji('grinning', true)
select_emoji('grinning')
find('.emoji-menu-toggle-button').click
@ -428,7 +430,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
emoji = 'grinning'
open_user_status_modal
select_emoji(emoji, true)
select_emoji(emoji)
expect(page.all('.award-control .js-counter')).to all(have_content('0'))
end
@ -451,7 +453,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
emoji = 'grinning'
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
select_emoji(emoji)
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal

View File

@ -23,6 +23,9 @@ describe('IssueBoardFilter', () => {
fullPath: 'gitlab-org',
isGroupBoard: true,
},
mocks: {
$apollo: {},
},
});
};

View File

@ -1,7 +1,6 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import { createAlert } from '~/alert';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -24,9 +23,8 @@ import {
mockJobRetryMutationData,
} from '../mock_data';
const localVue = createLocalVue();
jest.mock('~/alert');
localVue.use(VueApollo);
Vue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@ -62,7 +60,6 @@ describe('Manual Variables Form', () => {
]);
const options = {
localVue,
apolloProvider: mockApollo,
};

View File

@ -1,5 +1,5 @@
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@ -43,11 +43,10 @@ describe('noteActions', () => {
store.state.isPromoteCommentToTimelineEventInProgress = isPromotionInProgress;
};
const mountNoteActions = (propsData, computed) => {
return mount(noteActions, {
const mountNoteActions = (propsData) => {
return shallowMount(noteActions, {
store,
propsData,
computed,
stubs: {
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
methods: {
@ -190,15 +189,14 @@ describe('noteActions', () => {
};
beforeEach(() => {
wrapper = mountNoteActions(props, {
targetType: () => 'issue',
});
wrapper = mountNoteActions(props);
store.state.noteableData = {
current_user: {
can_set_issue_metadata: true,
},
};
store.state.userData = userDataMock;
store.state.noteableData.targetType = 'issue';
});
afterEach(() => {

View File

@ -106,7 +106,6 @@ describe('Shortcuts', () => {
let event;
beforeEach(() => {
window.gon.use_new_navigation = true;
event = new KeyboardEvent('keydown', { cancelable: true });
Shortcuts.focusSearch(event);
});

View File

@ -0,0 +1,60 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
describe('ScrollScrim', () => {
let wrapper;
const { trigger: triggerIntersection } = useMockIntersectionObserver();
const createWrapper = () => {
wrapper = shallowMountExtended(ScrollScrim, {});
};
beforeEach(() => {
createWrapper();
});
const findTopBoundary = () => wrapper.vm.$refs['top-boundary'];
const findBottomBoundary = () => wrapper.vm.$refs['bottom-boundary'];
describe('top scrim', () => {
describe('when top boundary is visible', () => {
it('does not show', async () => {
triggerIntersection(findTopBoundary(), { entry: { isIntersecting: true } });
await nextTick();
expect(wrapper.classes()).not.toContain('top-scrim-visible');
});
});
describe('when top boundary is not visible', () => {
it('does show', async () => {
triggerIntersection(findTopBoundary(), { entry: { isIntersecting: false } });
await nextTick();
expect(wrapper.classes()).toContain('top-scrim-visible');
});
});
});
describe('bottom scrim', () => {
describe('when bottom boundary is visible', () => {
it('does not show', async () => {
triggerIntersection(findBottomBoundary(), { entry: { isIntersecting: true } });
await nextTick();
expect(wrapper.classes()).not.toContain('bottom-scrim-visible');
});
});
describe('when bottom boundary is not visible', () => {
it('does show', async () => {
triggerIntersection(findBottomBoundary(), { entry: { isIntersecting: false } });
await nextTick();
expect(wrapper.classes()).toContain('bottom-scrim-visible');
});
});
});
});

View File

@ -302,8 +302,8 @@ describe('SuperSidebar component', () => {
createWrapper();
});
it('allows overflow', () => {
expect(findNavContainer().classes()).toContain('gl-overflow-auto');
it('allows overflow with scroll scrim', () => {
expect(findNavContainer().element.tagName).toContain('SCROLL-SCRIM');
});
});

View File

@ -1,8 +1,10 @@
import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue';
import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
import SetStatusModal from '~/set_status_modal/set_status_modal_wrapper.vue';
import { mockTracking } from 'helpers/tracking_helper';
import PersistentUserCallout from '~/persistent_user_callout';
import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
@ -13,6 +15,7 @@ describe('UserMenu component', () => {
const GlEmoji = { template: '<img/>' };
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findSetStatusModal = () => wrapper.findComponent(SetStatusModal);
const showDropdown = () => findDropdown().vm.$emit('shown');
const closeDropdownSpy = jest.fn();
@ -28,6 +31,7 @@ describe('UserMenu component', () => {
stubs: {
GlEmoji,
GlAvatar: true,
SetStatusModal: stubComponent(SetStatusModal),
...stubs,
},
provide: {
@ -91,31 +95,46 @@ describe('UserMenu component', () => {
describe('User status item', () => {
let item;
const setItem = ({ can_update, busy, customized, stubs } = {}) => {
createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } }, stubs);
const setItem = async ({
can_update: canUpdate = false,
busy = false,
customized = false,
stubs,
} = {}) => {
createWrapper(
{ status: { ...userMenuMockStatus, can_update: canUpdate, busy, customized } },
stubs,
);
// Mock mounting the modal if we can update
if (canUpdate) {
expect(wrapper.vm.setStatusModalReady).toEqual(false);
findSetStatusModal().vm.$emit('mounted');
await nextTick();
expect(wrapper.vm.setStatusModalReady).toEqual(true);
}
item = wrapper.findByTestId('status-item');
};
describe('When user cannot update the status', () => {
it('does not render the status menu item', () => {
setItem();
it('does not render the status menu item', async () => {
await setItem();
expect(item.exists()).toBe(false);
});
});
describe('When user can update the status', () => {
it('renders the status menu item', () => {
setItem({ can_update: true });
it('renders the status menu item', async () => {
await setItem({ can_update: true });
expect(item.exists()).toBe(true);
expect(item.find('button').attributes()).toMatchObject({
'data-track-property': 'nav_user_menu',
'data-track-action': 'click_link',
'data-track-label': 'user_edit_status',
});
});
it('should set the CSS class for triggering status update modal', () => {
setItem({ can_update: true });
expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
});
it('should close the dropdown when status modal opened', () => {
setItem({
it('should close the dropdown when status modal opened', async () => {
await setItem({
can_update: true,
stubs: {
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
@ -139,57 +158,75 @@ describe('UserMenu component', () => {
${true} | ${true} | ${'Edit status'}
`(
'when busy is "$busy" and customized is "$customized" the label is "$label"',
({ busy, customized, label }) => {
setItem({ can_update: true, busy, customized });
async ({ busy, customized, label }) => {
await setItem({ can_update: true, busy, customized });
expect(item.text()).toBe(label);
},
);
});
});
});
describe('Status update modal wrapper', () => {
const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper');
it('renders the modal wrapper', () => {
setItem({ can_update: true });
expect(findModalWrapper().exists()).toBe(true);
describe('set status modal', () => {
describe('when the user cannot update the status', () => {
it('should not render the modal', () => {
createWrapper({
status: { ...userMenuMockStatus, can_update: false },
});
describe('when user cannot update status', () => {
it('sets default data attributes', () => {
setItem({ can_update: true });
expect(findModalWrapper().attributes()).toMatchObject({
'data-current-emoji': '',
'data-current-message': '',
'data-default-emoji': 'speech_balloon',
});
expect(findSetStatusModal().exists()).toBe(false);
});
});
describe('when the user can update the status', () => {
describe.each`
busy | customized
${true} | ${true}
${true} | ${false}
${false} | ${true}
`('and the status is busy or customized', ({ busy, customized }) => {
it('should pass the current status to the modal', () => {
createWrapper({
status: { ...userMenuMockStatus, can_update: true, busy, customized },
});
expect(findSetStatusModal().exists()).toBe(true);
expect(findSetStatusModal().props()).toMatchObject({
defaultEmoji: 'speech_balloon',
currentEmoji: userMenuMockStatus.emoji,
currentMessage: userMenuMockStatus.message,
currentAvailability: userMenuMockStatus.availability,
currentClearStatusAfter: userMenuMockStatus.clear_after,
});
});
describe.each`
busy | customized
${true} | ${true}
${true} | ${false}
${false} | ${true}
${false} | ${false}
`(`when user can update status`, ({ busy, customized }) => {
it(`and ${busy ? 'is busy' : 'is not busy'} and status ${
customized ? 'is' : 'is not'
} customized sets user status data attributes`, () => {
setItem({ can_update: true, busy, customized });
if (busy || customized) {
expect(findModalWrapper().attributes()).toMatchObject({
'data-current-emoji': userMenuMockStatus.emoji,
'data-current-message': userMenuMockStatus.message,
'data-current-availability': userMenuMockStatus.availability,
'data-current-clear-status-after': userMenuMockStatus.clear_after,
});
} else {
expect(findModalWrapper().attributes()).toMatchObject({
'data-current-emoji': '',
'data-current-message': '',
'data-default-emoji': 'speech_balloon',
});
}
it('casts falsey values to empty strings', () => {
createWrapper({
status: { can_update: true, busy, customized },
});
expect(findSetStatusModal().exists()).toBe(true);
expect(findSetStatusModal().props()).toMatchObject({
defaultEmoji: 'speech_balloon',
currentEmoji: '',
currentMessage: '',
currentAvailability: '',
currentClearStatusAfter: '',
});
});
});
describe('and the status is neither busy nor customized', () => {
it('should pass an empty status to the modal', () => {
createWrapper({
status: { ...userMenuMockStatus, can_update: true, busy: false, customized: false },
});
expect(findSetStatusModal().exists()).toBe(true);
expect(findSetStatusModal().props()).toMatchObject({
defaultEmoji: 'speech_balloon',
currentEmoji: '',
currentMessage: '',
});
});
});

View File

@ -116,21 +116,23 @@ describe('Experimental new namespace creation app', () => {
expect(findLegacyContainer().exists()).toBe(true);
});
describe.each`
featureFlag | isSuperSidebarCollapsed | isToggleVisible
${true} | ${true} | ${true}
${true} | ${false} | ${false}
${false} | ${true} | ${false}
${false} | ${false} | ${false}
`('Super sidebar toggle', ({ featureFlag, isSuperSidebarCollapsed, isToggleVisible }) => {
beforeEach(() => {
sidebarState.isCollapsed = isSuperSidebarCollapsed;
gon.use_new_navigation = featureFlag;
createComponent();
describe('SuperSidebarToggle', () => {
describe('when collapsed', () => {
it('shows sidebar toggle', () => {
sidebarState.isCollapsed = true;
createComponent();
expect(findSuperSidebarToggle().exists()).toBe(true);
});
});
it(`${isToggleVisible ? 'is visible' : 'is not visible'}`, () => {
expect(findSuperSidebarToggle().exists()).toBe(isToggleVisible);
describe('when not collapsed', () => {
it('does not show sidebar toggle', () => {
sidebarState.isCollapsed = false;
createComponent();
expect(findSuperSidebarToggle().exists()).toBe(false);
});
});
});
@ -170,17 +172,10 @@ describe('Experimental new namespace creation app', () => {
});
describe('top bar', () => {
it('adds "top-bar-fixed" and "container-fluid" classes when new navigation enabled', () => {
gon.use_new_navigation = true;
it('has "top-bar-fixed" and "container-fluid" classes', () => {
createComponent();
expect(findTopBar().classes()).toEqual(['top-bar-fixed', 'container-fluid']);
});
it('does not add classes when new navigation is not enabled', () => {
createComponent();
expect(findTopBar().classes()).toEqual([]);
});
});
});

View File

@ -4,8 +4,34 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do
describe '.matches?' do
it 'is not implemented' do
expect { described_class.matches?(double) }.to raise_error(NotImplementedError)
context 'when given is a hash' do
before do
stub_const('TestInput', Class.new(described_class))
TestInput.class_eval do
def self.type_name
'test'
end
end
end
context 'when the spec type matches the input type' do
it 'returns true' do
expect(TestInput.matches?({ type: 'test' })).to be_truthy
end
end
context 'when the spec type does not match the input type' do
it 'returns false' do
expect(TestInput.matches?({ type: 'string' })).to be_falsey
end
end
end
context 'when not given a hash' do
it 'returns false' do
expect(described_class.matches?([])).to be_falsey
end
end
end

View File

@ -113,6 +113,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr
merge_request.source_branch
).to_s,
'CI_MERGE_REQUEST_TITLE' => merge_request.title,
'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description,
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
@ -214,6 +215,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha,
'CI_MERGE_REQUEST_TITLE' => merge_request.title,
'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description,
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),

View File

@ -15,7 +15,9 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::ForceDisconnectableMixin, :d
end
let(:config) { ActiveRecord::Base.configurations.find_db_config(Rails.env).configuration_hash.merge(pool: 1) }
let(:pool) { model.establish_connection(config) }
let(:pool) do
model.establish_connection(ActiveRecord::DatabaseConfigurations::HashConfig.new(Rails.env, 'main', config))
end
it 'calls the force disconnect callback on checkin' do
connection = pool.connection

View File

@ -25,12 +25,15 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do
context 'multiple databases', :reestablished_active_record_base do
before do
connection_class.establish_connection(
db_config =
ActiveRecord::Base
.connection_pool
.db_config
.configuration_hash
.merge(configuration_overrides)
connection_class.establish_connection(
ActiveRecord::DatabaseConfigurations::HashConfig.new(Rails.env, 'main', db_config)
)
end

View File

@ -21,6 +21,8 @@ RSpec.describe Gitlab::Database::Transaction::Observer, feature_category: :datab
it 'tracks transaction data', :aggregate_failures do
ActiveRecord::Base.transaction do
User.first
ActiveRecord::Base.transaction(requires_new: true) do
User.first

View File

@ -244,9 +244,9 @@ RSpec.describe Gitlab::Database::WithLockRetries, feature_category: :database do
it 'executes `SET LOCAL lock_timeout` using the configured timeout value in milliseconds' do
expect(connection).to receive(:execute).with("RESET idle_in_transaction_session_timeout; RESET lock_timeout").and_call_original
expect(connection).to receive(:execute).with("SAVEPOINT active_record_1", "TRANSACTION").and_call_original
expect(connection).to receive(:create_savepoint).with('active_record_1')
expect(connection).to receive(:execute).with("SET LOCAL lock_timeout TO '15ms'").and_call_original
expect(connection).to receive(:execute).with("RELEASE SAVEPOINT active_record_1", "TRANSACTION").and_call_original
expect(connection).to receive(:release_savepoint).with('active_record_1')
subject.run {}
end

View File

@ -40,16 +40,6 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
end
end
describe '.filesystem_id_from_disk' do
it 'catches errors' do
[Errno::ENOENT, Errno::EACCES, JSON::ParserError].each do |error|
stub_file_read(described_class.storage_metadata_file_path('default'), error: error)
expect(described_class.filesystem_id_from_disk('default')).to be_nil
end
end
end
describe '.filesystem_id' do
it 'returns an empty string when the relevant storage status is not found in the response' do
response = double("response")
@ -361,19 +351,6 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
end
end
describe '.can_use_disk?' do
it 'properly caches a false result' do
# spec_helper stubs this globally
allow(described_class).to receive(:can_use_disk?).and_call_original
expect(described_class).to receive(:filesystem_id).once
expect(described_class).to receive(:filesystem_id_from_disk).once
2.times do
described_class.can_use_disk?('unknown')
end
end
end
describe '.connection_data' do
it 'returns connection data' do
address = 'tcp://localhost:9876'

View File

@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillHasRemediationsOfVulnerabilityReads, feature_category: :database do
RSpec.describe RequeueBackfillHasRemediationsOfVulnerabilityReads, feature_category: :database do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do

View File

@ -236,14 +236,62 @@ RSpec.describe Trigger, feature_category: :tooling do
describe "TRIGGER_BRANCH" do
context 'when CNG_BRANCH is not set' do
it 'sets TRIGGER_BRANCH to master' do
stub_env('CI_PROJECT_NAMESPACE', 'gitlab-org')
expect(subject.variables['TRIGGER_BRANCH']).to eq('master')
context 'with gitlab-org' do
before do
stub_env('CI_PROJECT_NAMESPACE', 'gitlab-org')
end
it 'sets TRIGGER_BRANCH to master if the commit ref is master' do
stub_env('CI_COMMIT_REF_NAME', 'master')
stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', nil)
expect(subject.variables['TRIGGER_BRANCH']).to eq('master')
end
it 'sets the TRIGGER_BRANCH to master if the commit is part of an MR targeting master' do
stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', 'master')
expect(subject.variables['TRIGGER_BRANCH']).to eq('master')
end
it 'sets TRIGGER_BRANCH to stable branch if the commit ref is a stable branch' do
stub_env('CI_COMMIT_REF_NAME', '16-6-stable-ee')
expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
end
it 'sets the TRIGGER_BRANCH to stable branch if the commit is part of an MR targeting stable branch' do
stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', '16-6-stable-ee')
expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
end
end
it 'sets TRIGGER_BRANCH to main-jh on JH side' do
stub_env('CI_PROJECT_NAMESPACE', 'gitlab-cn')
expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh')
context 'with gitlab-cn' do
before do
stub_env('CI_PROJECT_NAMESPACE', 'gitlab-cn')
end
it 'sets TRIGGER_BRANCH to main-jh if commit ref is main-jh' do
stub_env('CI_COMMIT_REF_NAME', 'main-jh')
stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', nil)
expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh')
end
it 'sets the TRIGGER_BRANCH to main-jh if the commit is part of an MR targeting main-jh' do
stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', 'main-jh')
expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh')
end
it 'sets TRIGGER_BRANCH to 16-6-stable if commit ref is a stable branch' do
stub_env('CI_COMMIT_REF_NAME', '16-6-stable-jh')
expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
end
it 'sets the TRIGGER_BRANCH to 16-6-stable if the commit is part of an MR targeting 16-6-stable-jh' do
stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', '16-6-stable-jh')
expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
end
end
end

View File

@ -352,13 +352,53 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
end
end
context 'when import is a local request' do
before do
project.import_url = "http://127.0.0.1/group/project"
end
context 'when local network requests are enabled' do
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
end
it 'returns an error' do
expect(project.repository).not_to receive(:import_repository)
expect(subject.execute).to include(
status: :error,
message: end_with('Requests to localhost are not allowed')
)
end
context 'when environment is development' do
before do
stub_rails_env('development')
end
it 'imports successfully' do
expect(project.repository)
.to receive(:import_repository)
.and_return(true)
expect(subject.execute[:status]).to eq(:success)
end
end
end
end
context 'when DNS rebind protection is disabled' do
before do
allow(Gitlab::CurrentSettings).to receive(:dns_rebinding_protection_enabled?).and_return(false)
project.import_url = "https://example.com/group/project"
allow(Gitlab::UrlBlocker).to receive(:validate!)
.with(project.import_url, ports: Project::VALID_IMPORT_PORTS, schemes: Project::VALID_IMPORT_PROTOCOLS, dns_rebind_protection: false)
.with(
project.import_url,
ports: Project::VALID_IMPORT_PORTS,
schemes: Project::VALID_IMPORT_PROTOCOLS,
allow_local_network: false,
allow_localhost: false,
dns_rebind_protection: false
)
.and_return([Addressable::URI.parse("https://example.com/group/project"), nil])
end
@ -386,7 +426,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = "https://example.com/group/project"
allow(Gitlab::UrlBlocker).to receive(:validate!)
.with(project.import_url, ports: Project::VALID_IMPORT_PORTS, schemes: Project::VALID_IMPORT_PROTOCOLS, dns_rebind_protection: true)
.with(
project.import_url,
ports: Project::VALID_IMPORT_PORTS,
schemes: Project::VALID_IMPORT_PROTOCOLS,
allow_local_network: false,
allow_localhost: false,
dns_rebind_protection: true
)
.and_return([Addressable::URI.parse("https://172.16.123.1/group/project"), 'example.com'])
end
@ -407,7 +454,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = 'https://gitlab.com/gitlab-org/gitlab-development-kit'
allow(Gitlab::UrlBlocker).to receive(:validate!)
.with(project.import_url, ports: Project::VALID_IMPORT_PORTS, schemes: Project::VALID_IMPORT_PROTOCOLS, dns_rebind_protection: true)
.with(
project.import_url,
ports: Project::VALID_IMPORT_PORTS,
schemes: Project::VALID_IMPORT_PROTOCOLS,
allow_local_network: false,
allow_localhost: false,
dns_rebind_protection: true
)
.and_return([Addressable::URI.parse('https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]/gitlab-org/gitlab-development-kit'), 'gitlab.com'])
end
@ -430,7 +484,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = "http://example.com/group/project"
allow(Gitlab::UrlBlocker).to receive(:validate!)
.with(project.import_url, ports: Project::VALID_IMPORT_PORTS, schemes: Project::VALID_IMPORT_PROTOCOLS, dns_rebind_protection: true)
.with(
project.import_url,
ports: Project::VALID_IMPORT_PORTS,
schemes: Project::VALID_IMPORT_PROTOCOLS,
allow_local_network: false,
allow_localhost: false,
dns_rebind_protection: true
)
.and_return([Addressable::URI.parse("http://172.16.123.1/group/project"), 'example.com'])
end
@ -452,7 +513,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = "git://example.com/group/project.git"
allow(Gitlab::UrlBlocker).to receive(:validate!)
.with(project.import_url, ports: Project::VALID_IMPORT_PORTS, schemes: Project::VALID_IMPORT_PROTOCOLS, dns_rebind_protection: true)
.with(
project.import_url,
ports: Project::VALID_IMPORT_PORTS,
schemes: Project::VALID_IMPORT_PROTOCOLS,
allow_local_network: false,
allow_localhost: false,
dns_rebind_protection: true
)
.and_return([Addressable::URI.parse("git://172.16.123.1/group/project"), 'example.com'])
end

View File

@ -550,7 +550,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
with_them do
it 'outputs changed message for automation after operations happen' do
allow(ActiveRecord::Base.connection.schema_migration).to receive(:table_exists?).and_return(schema_migration_table_exists)
allow(ActiveRecord::Base.connection).to receive_message_chain(:schema_migration, :table_exists?).and_return(schema_migration_table_exists)
allow_any_instance_of(ActiveRecord::MigrationContext).to receive(:needs_migration?).and_return(needs_migrations)
expect { run_rake_task('gitlab:db:unattended') }.to output(/^#{rake_output}$/).to_stdout
end