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 tags.temp
.stylelintcache .stylelintcache
.solargraph.yml .solargraph.yml
jest-snapshot-test-match.json
jest-test-report.json
jest-snapshot-test-report.json
# Vite Ruby # Vite Ruby
/public/vite* /public/vite*

View File

@ -306,6 +306,42 @@ jest-integration:
- run_timed_command "yarn jest:integration --ci" - run_timed_command "yarn jest:integration --ci"
needs: ["rspec-all frontend_fixture", "graphql-schema-dump"] 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: coverage-frontend:
extends: extends:
- .default-retry - .default-retry

View File

@ -1390,6 +1390,26 @@
changes: *frontend-build-patterns changes: *frontend-build-patterns
allow_failure: true 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 # # Memory rules #
################ ################

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
// TODO: Remove this with the removal of the old navigation. // TODO: Remove this with the removal of the old navigation.
// See https://gitlab.com/groups/gitlab-org/-/epics/11875. // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
import Vue from 'vue';
import { highCountTrim } from '~/lib/utils/text_utility'; import { highCountTrim } from '~/lib/utils/text_utility';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import Translate from '~/vue_shared/translate';
/** /**
* Updates todo counter when todos are toggled. * 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) { function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
const { trackLabel, trackProperty } = elToTrack.dataset; const { trackLabel, trackProperty } = elToTrack.dataset;
@ -119,7 +47,4 @@ export function initNavUserDropdownTracking() {
} }
} }
if (!gon?.use_new_navigation) {
requestIdleCallback(initStatusTriggers);
}
requestIdleCallback(initNavUserDropdownTracking); requestIdleCallback(initNavUserDropdownTracking);

View File

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

View File

@ -12,3 +12,5 @@ export const AVAILABILITY_STATUS = {
BUSY: 'busy', BUSY: 'busy',
NOT_SET: 'not_set', 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 { GlToast, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import { createAlert } from '~/alert'; 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 { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api'; import { updateUserStatus } from '~/rest_api';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isUserBusy, computedClearStatusAfterValue } from './utils'; 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'; import SetStatusForm from './set_status_form.vue';
Vue.use(GlToast); Vue.use(GlToast);
export default { export default {
SET_STATUS_MODAL_ID,
components: { components: {
GlModal, GlModal,
SetStatusForm, SetStatusForm,
@ -29,11 +30,13 @@ export default {
}, },
currentEmoji: { currentEmoji: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
currentMessage: { currentMessage: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
currentAvailability: { currentAvailability: {
type: String, type: String,
@ -51,7 +54,6 @@ export default {
defaultEmojiTag: '', defaultEmojiTag: '',
emoji: this.currentEmoji, emoji: this.currentEmoji,
message: this.currentMessage, message: this.currentMessage,
modalId: 'set-user-status-modal',
availability: isUserBusy(this.currentAvailability), availability: isUserBusy(this.currentAvailability),
clearStatusAfter: null, clearStatusAfter: null,
}; };
@ -65,11 +67,11 @@ export default {
}, },
}, },
mounted() { mounted() {
this.$root.$emit(BV_SHOW_MODAL, this.modalId); this.$emit('mounted');
}, },
methods: { methods: {
closeModal() { closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId); this.$root.$emit(BV_HIDE_MODAL, SET_STATUS_MODAL_ID);
}, },
removeStatus() { removeStatus() {
this.availability = false; this.availability = false;
@ -132,7 +134,7 @@ export default {
<template> <template>
<gl-modal <gl-modal
:title="s__('SetStatusModal|Set a status')" :title="s__('SetStatusModal|Set a status')"
:modal-id="modalId" :modal-id="$options.SET_STATUS_MODAL_ID"
:action-primary="$options.actionPrimary" :action-primary="$options.actionPrimary"
:action-secondary="$options.actionSecondary" :action-secondary="$options.actionSecondary"
modal-class="set-user-status-modal" 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 <ul
aria-labelledby="super-sidebar-context-header" 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" data-testid="non-static-items-section"
> >
<template v-for="item in nonStaticItems"> <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 SidebarMenu from './sidebar_menu.vue';
import SidebarPeekBehavior from './sidebar_peek_behavior.vue'; import SidebarPeekBehavior from './sidebar_peek_behavior.vue';
import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue'; import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue';
import ScrollScrim from './scroll_scrim.vue';
export default { export default {
components: { components: {
@ -30,6 +31,7 @@ export default {
SidebarPeekBehavior, SidebarPeekBehavior,
SidebarHoverPeekBehavior, SidebarHoverPeekBehavior,
SidebarPortalTarget, SidebarPortalTarget,
ScrollScrim,
TrialStatusWidget: () => TrialStatusWidget: () =>
import('ee_component/contextual_sidebar/components/trial_status_widget.vue'), import('ee_component/contextual_sidebar/components/trial_status_widget.vue'),
TrialStatusPopover: () => TrialStatusPopover: () =>
@ -202,7 +204,7 @@ export default {
<div <div
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden" 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 <div
id="super-sidebar-context-header" 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" 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" :update-pins-url="sidebarData.update_pins_url"
/> />
<sidebar-portal-target /> <sidebar-portal-target />
</div> </scroll-scrim>
<div class="gl-p-3"> <div class="gl-p-2">
<help-center ref="helpCenter" :sidebar-data="sidebarData" /> <help-center ref="helpCenter" :sidebar-data="sidebarData" />
<gl-button <gl-button
v-if="sidebarData.is_admin" v-if="sidebarData.is_admin"

View File

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

View File

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

View File

@ -86,11 +86,7 @@ export default {
}, },
showSuperSidebarToggle() { showSuperSidebarToggle() {
return gon.use_new_navigation && sidebarState.isCollapsed; return sidebarState.isCollapsed;
},
topBarClasses() {
return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
}, },
}, },
@ -124,7 +120,7 @@ export default {
<template> <template>
<div> <div>
<div :class="topBarClasses" data-testid="top-bar"> <div class="top-bar-fixed container-fluid" data-testid="top-bar">
<div <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" 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/highlight';
@import 'framework/lists'; @import 'framework/lists';
@import 'framework/logo'; @import 'framework/logo';
@import 'framework/job_log';
@import 'framework/markdown_area'; @import 'framework/markdown_area';
@import 'framework/media_object'; @import 'framework/media_object';
@import 'framework/modal'; @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; 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_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_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_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_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_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present? 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, project.import_url,
schemes: Project::VALID_IMPORT_PROTOCOLS, schemes: Project::VALID_IMPORT_PROTOCOLS,
ports: Project::VALID_IMPORT_PORTS, ports: Project::VALID_IMPORT_PORTS,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
dns_rebind_protection: dns_rebind_protection?) dns_rebind_protection: dns_rebind_protection?)
.then do |(import_url, resolved_host)| .then do |(import_url, resolved_host)|
next '' if resolved_host.nil? || !import_url.scheme.in?(%w[http https]) next '' if resolved_host.nil? || !import_url.scheme.in?(%w[http https])
@ -179,6 +181,11 @@ module Projects
end end
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? def dns_rebind_protection?
return false if Gitlab.http_proxy_env? 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 # 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 initializer :clear_active_connections_again, after: :set_routes_reloader_hook do
# rubocop:disable Database/MultipleDatabases # rubocop:disable Database/MultipleDatabases
ActiveRecord::Base.clear_active_connections! ActiveRecord::Base.connection_handler.clear_active_connections!(ActiveRecord::Base.current_role)
ActiveRecord::Base.flush_idle_connections! ActiveRecord::Base.connection_handler.flush_idle_connections!(ActiveRecord::Base.current_role)
# rubocop:enable Database/MultipleDatabases # rubocop:enable Database/MultipleDatabases
end 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' milestone: '16.5'
type: ops type: ops
group: group::global search group: group::global search
default_enabled: false default_enabled: true

View File

@ -13,7 +13,11 @@ SUGGEST_COMMENT
def check_yaml(saas_feature) def check_yaml(saas_feature)
mr_group_label = helper.group_label 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 rescue Psych::Exception
# YAML could not be parsed, fail the build. # YAML could not be parsed, fail the build.
fail "#{helper.html_link(saas_feature.path)} isn't valid YAML! #{SEE_DOC.capitalize}" fail "#{helper.html_link(saas_feature.path)} isn't valid YAML! #{SEE_DOC.capitalize}"

View File

@ -1,7 +1,9 @@
--- ---
migration_job_name: BackfillHasRemediationsOfVulnerabilityReads migration_job_name: BackfillHasRemediationsOfVulnerabilityReads
description: Backfills has_remediations column for vulnerability_reads table. 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 feature_category: database
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133714 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133714
milestone: 16.5 milestone: 16.7
queued_migration_version: 20231011142714 queued_migration_version: 20231011142714

View File

@ -9,19 +9,15 @@ class QueueBackfillHasRemediationsOfVulnerabilityReads < Gitlab::Database::Migra
restrict_gitlab_migration gitlab_schema: :gitlab_main restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction! disable_ddl_transaction!
def up # per: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#requeuing-batched-background-migrations
queue_batched_background_migration( # > When you requeue the batched background migration, turn the original queuing
MIGRATION, # > into a no-op by clearing up the #up and #down methods of the migration
:vulnerability_reads, # > performing the requeuing. Otherwise, the batched background migration is
:vulnerability_id, # > queued multiple times on systems that are upgrading multiple patch releases
job_interval: DELAY_INTERVAL, # > at once.
queued_migration_version: '20231011142714', #
batch_size: BATCH_SIZE, # being re-run via https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135747
sub_batch_size: SUB_BATCH_SIZE def up; end
)
end
def down def down; end
delete_batched_background_migration(MIGRATION, :vulnerability_reads, :vulnerability_id, [])
end
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 ## Skip the merge train and merge immediately
If you have a high-priority merge request, like a critical patch that must 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: When you merge a merge request immediately:
- The current merge train is recreated. - The commits from the merge request are merged, ignoring the status of the merge train.
- All pipelines restart. - The merge train pipelines for all other merge requests on the train [are cancelled](#automatic-pipeline-cancellation).
- Redundant pipelines [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: WARNING:
Merging immediately can use a lot of CI/CD resources. Use this option 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_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_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_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_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_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. | | `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. 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) - [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. - An audit trail for log edits must be available.
- To avoid data loss, logs must be saved on different storage. - 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. 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) - [Log system in GitLab](../administration/logs/index.md)
- [Audit event development guidelines](../development/audit_event_guide/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 def perform
each_sub_batch do |sub_batch| each_sub_batch do |sub_batch|
reset_has_remediations_attribute(sub_batch)
update_query = update_query_for(sub_batch) update_query = update_query_for(sub_batch)
connection.execute(update_query) connection.execute(update_query)
@ -28,6 +30,10 @@ module Gitlab
private private
def reset_has_remediations_attribute(sub_batch)
sub_batch.update_all(has_remediations: false)
end
def update_query_for(sub_batch) def update_query_for(sub_batch)
subquery = sub_batch.joins(" subquery = sub_batch.joins("
INNER JOIN vulnerability_occurrences ON INNER JOIN vulnerability_occurrences ON

View File

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

View File

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

View File

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

View File

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

View File

@ -548,43 +548,10 @@ module Gitlab
end end
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) def self.filesystem_id(storage)
Gitlab::GitalyClient::ServerService.new(storage).storage_info&.filesystem_id Gitlab::GitalyClient::ServerService.new(storage).storage_info&.filesystem_id
end 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) def self.filesystem_disk_available(storage)
Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.available Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.available
end end

View File

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

View File

@ -87,7 +87,11 @@ namespace :gitlab do
# Skip if databases are yet to be provisioned # Skip if databases are yet to be provisioned
next unless connection[:identifier] && shared_connection[:identifier] 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' " \ warnings << "- The '#{connection[:name]}' since it is using 'database_tasks: false' " \
"should share database with '#{share_with}:'." "should share database with '#{share_with}:'."
end end

View File

@ -23069,6 +23069,9 @@ msgstr ""
msgid "GroupSAML|Could not create SAML group link: %{errors}." msgid "GroupSAML|Could not create SAML group link: %{errors}."
msgstr "" msgstr ""
msgid "GroupSAML|Custom roles"
msgstr ""
msgid "GroupSAML|Default membership role" msgid "GroupSAML|Default membership role"
msgstr "" 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." 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 "" 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}." msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to %{linkStart}reset it%{linkEnd}."
msgstr "" msgstr ""
@ -56719,6 +56725,9 @@ msgstr ""
msgid "ciReport|Automatically apply the patch in a new branch" msgid "ciReport|Automatically apply the patch in a new branch"
msgstr "" msgstr ""
msgid "ciReport|Automatically opens a merge request with a solution generated by AI"
msgstr ""
msgid "ciReport|Base pipeline codequality artifact not found" msgid "ciReport|Base pipeline codequality artifact not found"
msgstr "" msgstr ""
@ -56901,6 +56910,9 @@ msgstr ""
msgid "ciReport|RPS" msgid "ciReport|RPS"
msgstr "" msgstr ""
msgid "ciReport|Resolve with AI"
msgstr ""
msgid "ciReport|Resolve with merge request" msgid "ciReport|Resolve with merge request"
msgstr "" msgstr ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -106,7 +106,6 @@ describe('Shortcuts', () => {
let event; let event;
beforeEach(() => { beforeEach(() => {
window.gon.use_new_navigation = true;
event = new KeyboardEvent('keydown', { cancelable: true }); event = new KeyboardEvent('keydown', { cancelable: true });
Shortcuts.focusSearch(event); 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(); createWrapper();
}); });
it('allows overflow', () => { it('allows overflow with scroll scrim', () => {
expect(findNavContainer().classes()).toContain('gl-overflow-auto'); expect(findNavContainer().element.tagName).toContain('SCROLL-SCRIM');
}); });
}); });

View File

@ -1,8 +1,10 @@
import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui'; import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue'; import UserMenu from '~/super_sidebar/components/user_menu.vue';
import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.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 { mockTracking } from 'helpers/tracking_helper';
import PersistentUserCallout from '~/persistent_user_callout'; import PersistentUserCallout from '~/persistent_user_callout';
import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data'; import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
@ -13,6 +15,7 @@ describe('UserMenu component', () => {
const GlEmoji = { template: '<img/>' }; const GlEmoji = { template: '<img/>' };
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findSetStatusModal = () => wrapper.findComponent(SetStatusModal);
const showDropdown = () => findDropdown().vm.$emit('shown'); const showDropdown = () => findDropdown().vm.$emit('shown');
const closeDropdownSpy = jest.fn(); const closeDropdownSpy = jest.fn();
@ -28,6 +31,7 @@ describe('UserMenu component', () => {
stubs: { stubs: {
GlEmoji, GlEmoji,
GlAvatar: true, GlAvatar: true,
SetStatusModal: stubComponent(SetStatusModal),
...stubs, ...stubs,
}, },
provide: { provide: {
@ -91,31 +95,46 @@ describe('UserMenu component', () => {
describe('User status item', () => { describe('User status item', () => {
let item; let item;
const setItem = ({ can_update, busy, customized, stubs } = {}) => { const setItem = async ({
createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } }, stubs); 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'); item = wrapper.findByTestId('status-item');
}; };
describe('When user cannot update the status', () => { describe('When user cannot update the status', () => {
it('does not render the status menu item', () => { it('does not render the status menu item', async () => {
setItem(); await setItem();
expect(item.exists()).toBe(false); expect(item.exists()).toBe(false);
}); });
}); });
describe('When user can update the status', () => { describe('When user can update the status', () => {
it('renders the status menu item', () => { it('renders the status menu item', async () => {
setItem({ can_update: true }); await setItem({ can_update: true });
expect(item.exists()).toBe(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', () => { it('should close the dropdown when status modal opened', async () => {
setItem({ can_update: true }); await setItem({
expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
});
it('should close the dropdown when status modal opened', () => {
setItem({
can_update: true, can_update: true,
stubs: { stubs: {
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, { GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
@ -139,57 +158,75 @@ describe('UserMenu component', () => {
${true} | ${true} | ${'Edit status'} ${true} | ${true} | ${'Edit status'}
`( `(
'when busy is "$busy" and customized is "$customized" the label is "$label"', 'when busy is "$busy" and customized is "$customized" the label is "$label"',
({ busy, customized, label }) => { async ({ busy, customized, label }) => {
setItem({ can_update: true, busy, customized }); await setItem({ can_update: true, busy, customized });
expect(item.text()).toBe(label); expect(item.text()).toBe(label);
}, },
); );
}); });
});
});
describe('Status update modal wrapper', () => { describe('set status modal', () => {
const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper'); describe('when the user cannot update the status', () => {
it('should not render the modal', () => {
it('renders the modal wrapper', () => { createWrapper({
setItem({ can_update: true }); status: { ...userMenuMockStatus, can_update: false },
expect(findModalWrapper().exists()).toBe(true);
}); });
describe('when user cannot update status', () => { expect(findSetStatusModal().exists()).toBe(false);
it('sets default data attributes', () => { });
setItem({ can_update: true }); });
expect(findModalWrapper().attributes()).toMatchObject({
'data-current-emoji': '', describe('when the user can update the status', () => {
'data-current-message': '', describe.each`
'data-default-emoji': 'speech_balloon', 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` it('casts falsey values to empty strings', () => {
busy | customized createWrapper({
${true} | ${true} status: { can_update: true, busy, customized },
${true} | ${false} });
${false} | ${true}
${false} | ${false} expect(findSetStatusModal().exists()).toBe(true);
`(`when user can update status`, ({ busy, customized }) => { expect(findSetStatusModal().props()).toMatchObject({
it(`and ${busy ? 'is busy' : 'is not busy'} and status ${ defaultEmoji: 'speech_balloon',
customized ? 'is' : 'is not' currentEmoji: '',
} customized sets user status data attributes`, () => { currentMessage: '',
setItem({ can_update: true, busy, customized }); currentAvailability: '',
if (busy || customized) { currentClearStatusAfter: '',
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, describe('and the status is neither busy nor customized', () => {
}); it('should pass an empty status to the modal', () => {
} else { createWrapper({
expect(findModalWrapper().attributes()).toMatchObject({ status: { ...userMenuMockStatus, can_update: true, busy: false, customized: false },
'data-current-emoji': '', });
'data-current-message': '',
'data-default-emoji': 'speech_balloon', 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); expect(findLegacyContainer().exists()).toBe(true);
}); });
describe.each` describe('SuperSidebarToggle', () => {
featureFlag | isSuperSidebarCollapsed | isToggleVisible describe('when collapsed', () => {
${true} | ${true} | ${true} it('shows sidebar toggle', () => {
${true} | ${false} | ${false} sidebarState.isCollapsed = true;
${false} | ${true} | ${false} createComponent();
${false} | ${false} | ${false}
`('Super sidebar toggle', ({ featureFlag, isSuperSidebarCollapsed, isToggleVisible }) => { expect(findSuperSidebarToggle().exists()).toBe(true);
beforeEach(() => { });
sidebarState.isCollapsed = isSuperSidebarCollapsed;
gon.use_new_navigation = featureFlag;
createComponent();
}); });
it(`${isToggleVisible ? 'is visible' : 'is not visible'}`, () => { describe('when not collapsed', () => {
expect(findSuperSidebarToggle().exists()).toBe(isToggleVisible); 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', () => { describe('top bar', () => {
it('adds "top-bar-fixed" and "container-fluid" classes when new navigation enabled', () => { it('has "top-bar-fixed" and "container-fluid" classes', () => {
gon.use_new_navigation = true;
createComponent(); createComponent();
expect(findTopBar().classes()).toEqual(['top-bar-fixed', 'container-fluid']); 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 RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do
describe '.matches?' do describe '.matches?' do
it 'is not implemented' do context 'when given is a hash' do
expect { described_class.matches?(double) }.to raise_error(NotImplementedError) 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
end end

View File

@ -113,6 +113,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr
merge_request.source_branch merge_request.source_branch
).to_s, ).to_s,
'CI_MERGE_REQUEST_TITLE' => merge_request.title, '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_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), '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_NAME' => merge_request.source_branch.to_s,
'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha, 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha,
'CI_MERGE_REQUEST_TITLE' => merge_request.title, '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_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),

View File

@ -15,7 +15,9 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::ForceDisconnectableMixin, :d
end end
let(:config) { ActiveRecord::Base.configurations.find_db_config(Rails.env).configuration_hash.merge(pool: 1) } 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 it 'calls the force disconnect callback on checkin' do
connection = pool.connection connection = pool.connection

View File

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

View File

@ -21,6 +21,8 @@ RSpec.describe Gitlab::Database::Transaction::Observer, feature_category: :datab
it 'tracks transaction data', :aggregate_failures do it 'tracks transaction data', :aggregate_failures do
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
User.first
ActiveRecord::Base.transaction(requires_new: true) do ActiveRecord::Base.transaction(requires_new: true) do
User.first 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 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("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("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 {} subject.run {}
end end

View File

@ -40,16 +40,6 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
end end
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 describe '.filesystem_id' do
it 'returns an empty string when the relevant storage status is not found in the response' do it 'returns an empty string when the relevant storage status is not found in the response' do
response = double("response") response = double("response")
@ -361,19 +351,6 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
end end
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 describe '.connection_data' do
it 'returns connection data' do it 'returns connection data' do
address = 'tcp://localhost:9876' address = 'tcp://localhost:9876'

View File

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

View File

@ -236,14 +236,62 @@ RSpec.describe Trigger, feature_category: :tooling do
describe "TRIGGER_BRANCH" do describe "TRIGGER_BRANCH" do
context 'when CNG_BRANCH is not set' do context 'when CNG_BRANCH is not set' do
it 'sets TRIGGER_BRANCH to master' do context 'with gitlab-org' do
stub_env('CI_PROJECT_NAMESPACE', 'gitlab-org') before do
expect(subject.variables['TRIGGER_BRANCH']).to eq('master') 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 end
it 'sets TRIGGER_BRANCH to main-jh on JH side' do context 'with gitlab-cn' do
stub_env('CI_PROJECT_NAMESPACE', 'gitlab-cn') before do
expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh') 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
end end

View File

@ -352,13 +352,53 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
end end
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 context 'when DNS rebind protection is disabled' do
before do before do
allow(Gitlab::CurrentSettings).to receive(:dns_rebinding_protection_enabled?).and_return(false) allow(Gitlab::CurrentSettings).to receive(:dns_rebinding_protection_enabled?).and_return(false)
project.import_url = "https://example.com/group/project" project.import_url = "https://example.com/group/project"
allow(Gitlab::UrlBlocker).to receive(:validate!) 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]) .and_return([Addressable::URI.parse("https://example.com/group/project"), nil])
end end
@ -386,7 +426,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = "https://example.com/group/project" project.import_url = "https://example.com/group/project"
allow(Gitlab::UrlBlocker).to receive(:validate!) 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']) .and_return([Addressable::URI.parse("https://172.16.123.1/group/project"), 'example.com'])
end end
@ -407,7 +454,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = 'https://gitlab.com/gitlab-org/gitlab-development-kit' project.import_url = 'https://gitlab.com/gitlab-org/gitlab-development-kit'
allow(Gitlab::UrlBlocker).to receive(:validate!) 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']) .and_return([Addressable::URI.parse('https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]/gitlab-org/gitlab-development-kit'), 'gitlab.com'])
end end
@ -430,7 +484,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = "http://example.com/group/project" project.import_url = "http://example.com/group/project"
allow(Gitlab::UrlBlocker).to receive(:validate!) 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']) .and_return([Addressable::URI.parse("http://172.16.123.1/group/project"), 'example.com'])
end end
@ -452,7 +513,14 @@ RSpec.describe Projects::ImportService, feature_category: :importers do
project.import_url = "git://example.com/group/project.git" project.import_url = "git://example.com/group/project.git"
allow(Gitlab::UrlBlocker).to receive(:validate!) 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']) .and_return([Addressable::URI.parse("git://172.16.123.1/group/project"), 'example.com'])
end end

View File

@ -550,7 +550,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
with_them do with_them do
it 'outputs changed message for automation after operations happen' 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) 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 expect { run_rake_task('gitlab:db:unattended') }.to output(/^#{rake_output}$/).to_stdout
end end