Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d5ff067431
commit
a3e6d34643
|
@ -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*
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 #
|
||||||
################
|
################
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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">
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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} user’s menu'), { user: this.data.name });
|
return sprintf(__('%{user} user’s 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>
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
a1bbcd9430acc48bc271dd041c2999932d24d15bfa2ef8766d7bf9920d2d3539
|
|
@ -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
|
||||||
|
|
|
@ -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. |
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 group’s 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 group’s 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 ""
|
||||||
|
|
||||||
|
|
|
@ -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 .",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@ describe('IssueBoardFilter', () => {
|
||||||
fullPath: 'gitlab-org',
|
fullPath: 'gitlab-org',
|
||||||
isGroupBoard: true,
|
isGroupBoard: true,
|
||||||
},
|
},
|
||||||
|
mocks: {
|
||||||
|
$apollo: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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([]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(','),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue