Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f33d28f789
commit
5849e597a0
|
|
@ -1,4 +1,10 @@
|
||||||
import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji';
|
import {
|
||||||
|
initEmojiMap,
|
||||||
|
getEmojiInfo,
|
||||||
|
emojiFallbackImageSrc,
|
||||||
|
emojiImageTag,
|
||||||
|
findCustomEmoji,
|
||||||
|
} from '../emoji';
|
||||||
import isEmojiUnicodeSupported from '../emoji/support';
|
import isEmojiUnicodeSupported from '../emoji/support';
|
||||||
|
|
||||||
class GlEmoji extends HTMLElement {
|
class GlEmoji extends HTMLElement {
|
||||||
|
|
@ -33,6 +39,7 @@ class GlEmoji extends HTMLElement {
|
||||||
this.childNodes &&
|
this.childNodes &&
|
||||||
Array.prototype.every.call(this.childNodes, (childNode) => childNode.nodeType === 3);
|
Array.prototype.every.call(this.childNodes, (childNode) => childNode.nodeType === 3);
|
||||||
|
|
||||||
|
const customEmoji = findCustomEmoji(name);
|
||||||
const hasImageFallback = fallbackSrc?.length > 0;
|
const hasImageFallback = fallbackSrc?.length > 0;
|
||||||
const hasCssSpriteFallback = fallbackSpriteClass?.length > 0;
|
const hasCssSpriteFallback = fallbackSpriteClass?.length > 0;
|
||||||
|
|
||||||
|
|
@ -51,7 +58,7 @@ class GlEmoji extends HTMLElement {
|
||||||
this.classList.add(fallbackSpriteClass);
|
this.classList.add(fallbackSpriteClass);
|
||||||
} else if (hasImageFallback) {
|
} else if (hasImageFallback) {
|
||||||
this.innerHTML = '';
|
this.innerHTML = '';
|
||||||
this.appendChild(emojiImageTag(name, fallbackSrc));
|
this.appendChild(emojiImageTag(name, customEmoji?.src || fallbackSrc));
|
||||||
} else {
|
} else {
|
||||||
const src = emojiFallbackImageSrc(name);
|
const src = emojiFallbackImageSrc(name);
|
||||||
this.innerHTML = '';
|
this.innerHTML = '';
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,9 @@ export default {
|
||||||
<design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
|
<design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
|
||||||
<ul
|
<ul
|
||||||
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
|
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
|
||||||
|
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
||||||
data-qa-selector="design_discussion_content"
|
data-qa-selector="design_discussion_content"
|
||||||
|
data-testid="design-discussion-content"
|
||||||
>
|
>
|
||||||
<design-note
|
<design-note
|
||||||
:note="firstNote"
|
:note="firstNote"
|
||||||
|
|
@ -300,7 +302,6 @@ export default {
|
||||||
:is-resolving="isResolving"
|
:is-resolving="isResolving"
|
||||||
:is-discussion="true"
|
:is-discussion="true"
|
||||||
:noteable-id="noteableId"
|
:noteable-id="noteableId"
|
||||||
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
|
||||||
@delete-note="showDeleteNoteConfirmationModal($event)"
|
@delete-note="showDeleteNoteConfirmationModal($event)"
|
||||||
>
|
>
|
||||||
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
|
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
|
||||||
|
|
@ -343,7 +344,6 @@ export default {
|
||||||
:is-resolving="isResolving"
|
:is-resolving="isResolving"
|
||||||
:noteable-id="noteableId"
|
:noteable-id="noteableId"
|
||||||
:is-discussion="false"
|
:is-discussion="false"
|
||||||
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
|
||||||
@delete-note="showDeleteNoteConfirmationModal($event)"
|
@delete-note="showDeleteNoteConfirmationModal($event)"
|
||||||
/>
|
/>
|
||||||
<li
|
<li
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
|
<timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
|
||||||
<gl-avatar-link :href="author.webUrl" class="gl-float-left gl-mr-3">
|
<gl-avatar-link :href="author.webUrl" class="gl-float-left gl-mr-3 link-inherit-color">
|
||||||
<gl-avatar :size="32" :src="author.avatarUrl" :entity-name="author.username" />
|
<gl-avatar :size="32" :src="author.avatarUrl" :entity-name="author.username" />
|
||||||
</gl-avatar-link>
|
</gl-avatar-link>
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ export default {
|
||||||
<gl-link
|
<gl-link
|
||||||
v-once
|
v-once
|
||||||
:href="author.webUrl"
|
:href="author.webUrl"
|
||||||
class="js-user-link"
|
class="js-user-link link-inherit-color"
|
||||||
data-testid="user-link"
|
data-testid="user-link"
|
||||||
:data-user-id="authorId"
|
:data-user-id="authorId"
|
||||||
:data-username="author.username"
|
:data-username="author.username"
|
||||||
|
|
@ -152,7 +152,7 @@ export default {
|
||||||
<span class="note-headline-light note-headline-meta">
|
<span class="note-headline-light note-headline-meta">
|
||||||
<span class="system-note-message"> <slot></slot> </span>
|
<span class="system-note-message"> <slot></slot> </span>
|
||||||
<gl-link
|
<gl-link
|
||||||
class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm"
|
class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm link-inherit-color"
|
||||||
:href="`#note_${noteAnchorId}`"
|
:href="`#note_${noteAnchorId}`"
|
||||||
>
|
>
|
||||||
<time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
|
<time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
|
||||||
|
|
@ -175,7 +175,6 @@ export default {
|
||||||
<gl-disclosure-dropdown
|
<gl-disclosure-dropdown
|
||||||
v-if="isEditingAndHasPermissions"
|
v-if="isEditingAndHasPermissions"
|
||||||
v-gl-tooltip.hover
|
v-gl-tooltip.hover
|
||||||
toggle-class="btn-sm"
|
|
||||||
icon="ellipsis_v"
|
icon="ellipsis_v"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
data-qa-selector="design_discussion_actions_ellipsis_dropdown"
|
data-qa-selector="design_discussion_actions_ellipsis_dropdown"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ export default {
|
||||||
this.renderGroup = true;
|
this.renderGroup = true;
|
||||||
this.$emit('appear', this.category);
|
this.$emit('appear', this.category);
|
||||||
},
|
},
|
||||||
|
onClick(emoji) {
|
||||||
|
this.$emit('click', { category: this.category, emoji });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -48,7 +51,7 @@ export default {
|
||||||
:key="index"
|
:key="index"
|
||||||
:emojis="emojiGroup"
|
:emojis="emojiGroup"
|
||||||
:render-group="renderGroup"
|
:render-group="renderGroup"
|
||||||
:click-emoji="(emoji) => $emit('click', emoji)"
|
:click-emoji="(emoji) => onClick(emoji)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
|
import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
|
||||||
import { findLastIndex } from 'lodash';
|
import { findLastIndex } from 'lodash';
|
||||||
import VirtualList from 'vue-virtual-scroll-list';
|
import VirtualList from 'vue-virtual-scroll-list';
|
||||||
import { CATEGORY_NAMES } from '~/emoji';
|
import { CATEGORY_NAMES, getEmojiCategoryMap } from '~/emoji';
|
||||||
import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
|
import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
|
||||||
import Category from './category.vue';
|
import Category from './category.vue';
|
||||||
import EmojiList from './emoji_list.vue';
|
import EmojiList from './emoji_list.vue';
|
||||||
|
|
@ -49,6 +49,7 @@ export default {
|
||||||
categoryNames() {
|
categoryNames() {
|
||||||
return CATEGORY_NAMES.filter((c) => {
|
return CATEGORY_NAMES.filter((c) => {
|
||||||
if (c === FREQUENTLY_USED_KEY) return hasFrequentlyUsedEmojis();
|
if (c === FREQUENTLY_USED_KEY) return hasFrequentlyUsedEmojis();
|
||||||
|
if (c === 'custom') return getEmojiCategoryMap()?.custom.length > 0;
|
||||||
return true;
|
return true;
|
||||||
}).map((category) => ({
|
}).map((category) => ({
|
||||||
name: category,
|
name: category,
|
||||||
|
|
@ -66,10 +67,13 @@ export default {
|
||||||
|
|
||||||
this.$refs.virtualScoller.setScrollTop(top);
|
this.$refs.virtualScoller.setScrollTop(top);
|
||||||
},
|
},
|
||||||
selectEmoji(name) {
|
selectEmoji({ category, emoji }) {
|
||||||
this.$emit('click', name);
|
this.$emit('click', emoji);
|
||||||
this.$refs.dropdown.hide();
|
this.$refs.dropdown.hide();
|
||||||
addToFrequentlyUsed(name);
|
|
||||||
|
if (category !== 'custom') {
|
||||||
|
addToFrequentlyUsed(emoji);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getBoundaryElement() {
|
getBoundaryElement() {
|
||||||
return this.boundary || document.querySelector('.content-wrapper') || 'scrollParent';
|
return this.boundary || document.querySelector('.content-wrapper') || 'scrollParent';
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export const getEmojiCategories = memoize(async () => {
|
||||||
|
|
||||||
return Object.freeze(
|
return Object.freeze(
|
||||||
Object.keys(categories)
|
Object.keys(categories)
|
||||||
.filter((c) => c !== FREQUENTLY_USED_KEY)
|
.filter((c) => c !== FREQUENTLY_USED_KEY && categories[c].length)
|
||||||
.reduce((acc, category) => {
|
.reduce((acc, category) => {
|
||||||
const emojis = chunk(categories[category], EMOJIS_PER_ROW);
|
const emojis = chunk(categories[category], EMOJIS_PER_ROW);
|
||||||
const height = generateCategoryHeight(emojis.length);
|
const height = generateCategoryHeight(emojis.length);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis';
|
||||||
|
|
||||||
export const CATEGORY_ICON_MAP = {
|
export const CATEGORY_ICON_MAP = {
|
||||||
[FREQUENTLY_USED_KEY]: 'history',
|
[FREQUENTLY_USED_KEY]: 'history',
|
||||||
|
custom: 'tanuki',
|
||||||
activity: 'dumbbell',
|
activity: 'dumbbell',
|
||||||
people: 'smiley',
|
people: 'smiley',
|
||||||
nature: 'nature',
|
nature: 'nature',
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import { escape, minBy } from 'lodash';
|
import { escape, minBy } from 'lodash';
|
||||||
import emojiRegexFactory from 'emoji-regex';
|
import emojiRegexFactory from 'emoji-regex';
|
||||||
import emojiAliases from 'emojis/aliases.json';
|
import emojiAliases from 'emojis/aliases.json';
|
||||||
|
import createApolloClient from '~/lib/graphql';
|
||||||
import { setAttributes } from '~/lib/utils/dom_utils';
|
import { setAttributes } from '~/lib/utils/dom_utils';
|
||||||
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
||||||
import AccessorUtilities from '../lib/utils/accessor';
|
import AccessorUtilities from '../lib/utils/accessor';
|
||||||
import axios from '../lib/utils/axios_utils';
|
import axios from '../lib/utils/axios_utils';
|
||||||
|
import customEmojiQuery from './queries/custom_emoji.query.graphql';
|
||||||
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
||||||
|
|
||||||
let emojiMap = null;
|
let emojiMap = null;
|
||||||
let validEmojiNames = null;
|
let validEmojiNames = null;
|
||||||
|
|
||||||
export const FALLBACK_EMOJI_KEY = 'grey_question';
|
export const FALLBACK_EMOJI_KEY = 'grey_question';
|
||||||
|
|
||||||
// Keep the version in sync with `lib/gitlab/emoji.rb`
|
// Keep the version in sync with `lib/gitlab/emoji.rb`
|
||||||
|
|
@ -53,9 +56,42 @@ async function loadEmojiWithNames() {
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadCustomEmojiWithNames() {
|
||||||
|
if (document.body?.dataset?.group && window.gon?.features?.customEmoji) {
|
||||||
|
const client = createApolloClient();
|
||||||
|
const { data } = await client.query({
|
||||||
|
query: customEmojiQuery,
|
||||||
|
variables: {
|
||||||
|
groupPath: document.body.dataset.group,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data?.group?.customEmoji?.nodes?.reduce((acc, e) => {
|
||||||
|
// Map the custom emoji into the format of the normal emojis
|
||||||
|
acc[e.name] = {
|
||||||
|
c: 'custom',
|
||||||
|
d: e.name,
|
||||||
|
e: undefined,
|
||||||
|
name: e.name,
|
||||||
|
src: e.url,
|
||||||
|
u: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareEmojiMap() {
|
async function prepareEmojiMap() {
|
||||||
emojiMap = await loadEmojiWithNames();
|
return Promise.all([loadEmojiWithNames(), loadCustomEmojiWithNames()]).then((values) => {
|
||||||
|
emojiMap = {
|
||||||
|
...values[0],
|
||||||
|
...values[1],
|
||||||
|
};
|
||||||
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
|
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initEmojiMap() {
|
export function initEmojiMap() {
|
||||||
|
|
@ -84,6 +120,10 @@ export function getAllEmoji() {
|
||||||
return emojiMap;
|
return emojiMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findCustomEmoji(name) {
|
||||||
|
return emojiMap[name];
|
||||||
|
}
|
||||||
|
|
||||||
function getAliasesMatchingQuery(query) {
|
function getAliasesMatchingQuery(query) {
|
||||||
return Object.keys(emojiAliases)
|
return Object.keys(emojiAliases)
|
||||||
.filter((alias) => alias.includes(query))
|
.filter((alias) => alias.includes(query))
|
||||||
|
|
@ -176,7 +216,7 @@ export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
|
||||||
|
|
||||||
let emojiCategoryMap;
|
let emojiCategoryMap;
|
||||||
export function getEmojiCategoryMap() {
|
export function getEmojiCategoryMap() {
|
||||||
if (!emojiCategoryMap) {
|
if (!emojiCategoryMap && emojiMap) {
|
||||||
emojiCategoryMap = CATEGORY_NAMES.reduce((acc, category) => {
|
emojiCategoryMap = CATEGORY_NAMES.reduce((acc, category) => {
|
||||||
if (category === FREQUENTLY_USED_KEY) {
|
if (category === FREQUENTLY_USED_KEY) {
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -218,10 +258,11 @@ export function getEmojiInfo(query, fallback = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emojiFallbackImageSrc(inputName) {
|
export function emojiFallbackImageSrc(inputName) {
|
||||||
const { name } = getEmojiInfo(inputName);
|
const { name, src } = getEmojiInfo(inputName);
|
||||||
return `${gon.asset_host || ''}${
|
return (
|
||||||
gon.relative_url_root || ''
|
src ||
|
||||||
}/-/emojis/${EMOJI_VERSION}/${name}.png`;
|
`${gon.asset_host || ''}${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/${name}.png`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emojiImageTag(name, src) {
|
export function emojiImageTag(name, src) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
query getCustomEmoji($groupPath: ID!) {
|
||||||
|
group(fullPath: $groupPath) {
|
||||||
|
id
|
||||||
|
customEmoji {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlModal, GlSprintf } from '@gitlab/ui';
|
import { GlModal, GlSprintf } from '@gitlab/ui';
|
||||||
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import csrf from '~/lib/utils/csrf';
|
import csrf from '~/lib/utils/csrf';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
|
|
||||||
|
|
@ -8,6 +9,7 @@ export default {
|
||||||
GlModal,
|
GlModal,
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
},
|
},
|
||||||
|
mixins: [glFeatureFlagMixin()],
|
||||||
props: {
|
props: {
|
||||||
actionUrl: {
|
actionUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -67,6 +69,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
|
textdelay: s__(`Profiles|
|
||||||
|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
|
||||||
|
Once you confirm %{deleteAccount}, it cannot be undone or recovered. You might have to wait seven days before creating a new account with the same username or email.`),
|
||||||
text: s__(`Profiles|
|
text: s__(`Profiles|
|
||||||
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
|
||||||
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
|
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
|
||||||
|
|
@ -85,7 +90,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
|
||||||
@primary="onSubmit"
|
@primary="onSubmit"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<gl-sprintf :message="$options.i18n.text">
|
<gl-sprintf v-if="glFeatures.delayDeleteOwnUser" :message="$options.i18n.textdelay">
|
||||||
|
<template #yourAccount>
|
||||||
|
<strong>{{ s__('Profiles|your account') }}</strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #deleteAccount>
|
||||||
|
<strong>{{ s__('Profiles|Delete account') }}</strong>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
<gl-sprintf v-else :message="$options.i18n.text">
|
||||||
<template #yourAccount>
|
<template #yourAccount>
|
||||||
<strong>{{ s__('Profiles|your account') }}</strong>
|
<strong>{{ s__('Profiles|your account') }}</strong>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -115,11 +115,11 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
||||||
flex-basis: 28%;
|
flex-basis: 28%;
|
||||||
|
|
||||||
.link-inherit-color {
|
.link-inherit-color {
|
||||||
|
&,
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,19 +159,10 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
||||||
transition: background $gl-transition-duration-medium $general-hover-transition-curve;
|
transition: background $gl-transition-duration-medium $general-hover-transition-curve;
|
||||||
border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
|
border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
|
||||||
border-top-right-radius: $border-radius-default;
|
border-top-right-radius: $border-radius-default;
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-text a {
|
|
||||||
color: var(--blue-600, $blue-600);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-wrapper {
|
.reply-wrapper {
|
||||||
padding: $gl-padding-8;
|
padding: $gl-padding-8;
|
||||||
background: $gray-10;
|
|
||||||
border-radius: 0 0 $border-radius-default $border-radius-default;
|
border-radius: 0 0 $border-radius-default $border-radius-default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
|
||||||
urgency :low, [:show]
|
urgency :low, [:show]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
push_frontend_feature_flag(:delay_delete_own_user)
|
||||||
render(locals: show_view_variables)
|
render(locals: show_view_variables)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class AwardEmojisFinder
|
||||||
def validate_params
|
def validate_params
|
||||||
return unless params.present?
|
return unless params.present?
|
||||||
|
|
||||||
validate_name_param
|
validate_name_param unless Feature.enabled?(:custom_emoji)
|
||||||
validate_awarded_by_param
|
validate_awarded_by_param
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -824,16 +824,6 @@ class Group < Namespace
|
||||||
).call
|
).call
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_shared_runners_setting!(state)
|
|
||||||
raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state)
|
|
||||||
|
|
||||||
case state
|
|
||||||
when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override
|
|
||||||
when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE then disable_shared_runners_and_allow_override!
|
|
||||||
when SR_ENABLED then enable_shared_runners! # set both to true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def first_owner
|
def first_owner
|
||||||
owners.first || parent&.first_owner || owner
|
owners.first || parent&.first_owner || owner
|
||||||
end
|
end
|
||||||
|
|
@ -1068,45 +1058,6 @@ class Group < Namespace
|
||||||
Arel::Nodes::SqlLiteral.new(column_alias))
|
Arel::Nodes::SqlLiteral.new(column_alias))
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable_shared_runners!
|
|
||||||
update!(
|
|
||||||
shared_runners_enabled: false,
|
|
||||||
allow_descendants_override_disabled_shared_runners: false)
|
|
||||||
|
|
||||||
group_ids = descendants
|
|
||||||
unless group_ids.empty?
|
|
||||||
Group.by_id(group_ids).update_all(
|
|
||||||
shared_runners_enabled: false,
|
|
||||||
allow_descendants_override_disabled_shared_runners: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
all_projects.update_all(shared_runners_enabled: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def disable_shared_runners_and_allow_override!
|
|
||||||
# enabled -> disabled_and_overridable
|
|
||||||
if shared_runners_enabled?
|
|
||||||
update!(
|
|
||||||
shared_runners_enabled: false,
|
|
||||||
allow_descendants_override_disabled_shared_runners: true)
|
|
||||||
|
|
||||||
group_ids = descendants
|
|
||||||
unless group_ids.empty?
|
|
||||||
Group.by_id(group_ids).update_all(shared_runners_enabled: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
all_projects.update_all(shared_runners_enabled: false)
|
|
||||||
|
|
||||||
# disabled_and_unoverridable -> disabled_and_overridable
|
|
||||||
else
|
|
||||||
update!(allow_descendants_override_disabled_shared_runners: true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def enable_shared_runners!
|
|
||||||
update!(shared_runners_enabled: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def runners_token_prefix
|
def runners_token_prefix
|
||||||
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
|
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2311,7 +2311,7 @@ class User < ApplicationRecord
|
||||||
return super if ::Gitlab::CurrentSettings.email_confirmation_setting_soft?
|
return super if ::Gitlab::CurrentSettings.email_confirmation_setting_soft?
|
||||||
|
|
||||||
# Following devise logic for method, we want to return `true`
|
# Following devise logic for method, we want to return `true`
|
||||||
# See: https://github.com/heartcombo/devise/blob/main/lib/devise/models/confirmable.rb#L191-L218
|
# See: https://github.com/heartcombo/devise/blob/ec0674523e7909579a5a008f16fb9fe0c3a71712/lib/devise/models/confirmable.rb#L191-L218
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
alias_method :in_confirmation_period?, :confirmation_period_valid?
|
alias_method :in_confirmation_period?, :confirmation_period_valid?
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,14 @@ module Groups
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_shared_runners
|
def update_shared_runners
|
||||||
group.update_shared_runners_setting!(params[:shared_runners_setting])
|
case params[:shared_runners_setting]
|
||||||
|
when Namespace::SR_DISABLED_AND_UNOVERRIDABLE
|
||||||
|
disable_shared_runners! # also disallows override
|
||||||
|
when Namespace::SR_DISABLED_WITH_OVERRIDE, Namespace::SR_DISABLED_AND_OVERRIDABLE
|
||||||
|
disable_shared_runners_and_allow_override!
|
||||||
|
when Namespace::SR_ENABLED
|
||||||
|
enable_shared_runners! # set both to true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_pending_builds?
|
def update_pending_builds?
|
||||||
|
|
@ -41,5 +48,42 @@ module Groups
|
||||||
::Ci::UpdatePendingBuildService.new(group, pending_builds_params).execute
|
::Ci::UpdatePendingBuildService.new(group, pending_builds_params).execute
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disable_shared_runners!
|
||||||
|
group.update!(
|
||||||
|
shared_runners_enabled: false,
|
||||||
|
allow_descendants_override_disabled_shared_runners: false)
|
||||||
|
|
||||||
|
group_ids = group.descendants
|
||||||
|
unless group_ids.empty?
|
||||||
|
Group.by_id(group_ids).update_all(
|
||||||
|
shared_runners_enabled: false,
|
||||||
|
allow_descendants_override_disabled_shared_runners: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
group.all_projects.update_all(shared_runners_enabled: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_shared_runners_and_allow_override!
|
||||||
|
# enabled -> disabled_and_overridable
|
||||||
|
if group.shared_runners_enabled?
|
||||||
|
group.update!(
|
||||||
|
shared_runners_enabled: false,
|
||||||
|
allow_descendants_override_disabled_shared_runners: true)
|
||||||
|
|
||||||
|
group_ids = group.descendants
|
||||||
|
Group.by_id(group_ids).update_all(shared_runners_enabled: false) unless group_ids.empty?
|
||||||
|
|
||||||
|
group.all_projects.update_all(shared_runners_enabled: false)
|
||||||
|
|
||||||
|
# disabled_and_unoverridable -> disabled_and_overridable
|
||||||
|
else
|
||||||
|
group.update!(allow_descendants_override_disabled_shared_runners: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_shared_runners!
|
||||||
|
group.update!(shared_runners_enabled: true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,15 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
def run_pipeline_schedule(schedule, user)
|
def run_pipeline_schedule(schedule, user)
|
||||||
response = Ci::CreatePipelineService
|
response = Ci::CreatePipelineService
|
||||||
.new(schedule.project, user, ref: schedule.ref)
|
.new(schedule.project, user, ref: schedule.ref)
|
||||||
.execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
|
.execute(
|
||||||
|
:schedule,
|
||||||
|
save_on_errors: Feature.enabled?(:persist_failed_pipelines_from_schedules, schedule.project),
|
||||||
|
ignore_skip_ci: true, schedule: schedule
|
||||||
|
)
|
||||||
|
|
||||||
return response if response.payload.persisted?
|
return response if response.payload.persisted?
|
||||||
|
|
||||||
|
# Remove with FF persist_failed_pipelines_from_schedules enabled, as corrupted yml is not longer logged
|
||||||
# This is a user operation error such as corrupted .gitlab-ci.yml. Log the error for debugging purpose.
|
# This is a user operation error such as corrupted .gitlab-ci.yml. Log the error for debugging purpose.
|
||||||
log_extra_metadata_on_done(:pipeline_creation_error, response.message)
|
log_extra_metadata_on_done(:pipeline_creation_error, response.message)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: persist_failed_pipelines_from_schedules
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124371
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416297
|
||||||
|
milestone: '16.2'
|
||||||
|
type: development
|
||||||
|
group: group::pipeline execution
|
||||||
|
default_enabled: false
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ChangeUnconfirmedCreatedAtIndexOnUsers < Gitlab::Database::Migration[2.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
OLD_INDEX_NAME = 'index_users_on_unconfirmed_and_created_at_for_active_humans'
|
||||||
|
NEW_INDEX_NAME = 'index_users_on_unconfirmed_created_at_active_type_sign_in_count'
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :users, [:created_at, :id],
|
||||||
|
name: NEW_INDEX_NAME,
|
||||||
|
where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0) AND sign_in_count = 0"
|
||||||
|
|
||||||
|
remove_concurrent_index_by_name :users, OLD_INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_concurrent_index :users, [:created_at, :id],
|
||||||
|
name: OLD_INDEX_NAME,
|
||||||
|
where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0)"
|
||||||
|
|
||||||
|
remove_concurrent_index_by_name :users, NEW_INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
e728befa42eb6749929e758ece0f29ec57cd7614a378b8c5e4dc24f134f39185
|
||||||
|
|
@ -33171,7 +33171,7 @@ CREATE INDEX index_users_on_state_and_user_type ON users USING btree (state, use
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_users_on_static_object_token ON users USING btree (static_object_token);
|
CREATE UNIQUE INDEX index_users_on_static_object_token ON users USING btree (static_object_token);
|
||||||
|
|
||||||
CREATE INDEX index_users_on_unconfirmed_and_created_at_for_active_humans ON users USING btree (created_at, id) WHERE ((confirmed_at IS NULL) AND ((state)::text = 'active'::text) AND (user_type = 0));
|
CREATE INDEX index_users_on_unconfirmed_created_at_active_type_sign_in_count ON users USING btree (created_at, id) WHERE ((confirmed_at IS NULL) AND ((state)::text = 'active'::text) AND (user_type = 0) AND (sign_in_count = 0));
|
||||||
|
|
||||||
CREATE INDEX index_users_on_unconfirmed_email ON users USING btree (unconfirmed_email) WHERE (unconfirmed_email IS NOT NULL);
|
CREATE INDEX index_users_on_unconfirmed_email ON users USING btree (unconfirmed_email) WHERE (unconfirmed_email IS NOT NULL);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,9 @@ For an overview, see
|
||||||
|
|
||||||
After you add a group, the following data is synced to Jira for all projects in that group:
|
After you add a group, the following data is synced to Jira for all projects in that group:
|
||||||
|
|
||||||
- New merge requests, branches, and commits
|
- New merge requests, branches, and commits.
|
||||||
- Existing merge requests (GitLab 13.8 and later)
|
- Existing merge requests (GitLab 13.8 and later).
|
||||||
- Existing branches and commits (GitLab 15.11 and later)
|
- Existing branches and commits (GitLab 15.11 and later). You must delete and add any namespaces that were added to the GitLab for Jira Cloud app in GitLab 15.10 and earlier.
|
||||||
|
|
||||||
## Update the GitLab for Jira Cloud app
|
## Update the GitLab for Jira Cloud app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ module Backup
|
||||||
progress.flush
|
progress.flush
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
::Gitlab::Database::EachDatabase.each_database_connection(
|
::Gitlab::Database::EachDatabase.each_connection(
|
||||||
only: base_models_for_backup.keys, include_shared: false
|
only: base_models_for_backup.keys, include_shared: false
|
||||||
) do |connection, _|
|
) do |connection, _|
|
||||||
Gitlab::Database::TransactionTimeoutSettings.new(connection).restore_timeouts
|
Gitlab::Database::TransactionTimeoutSettings.new(connection).restore_timeouts
|
||||||
|
|
@ -259,7 +259,7 @@ module Backup
|
||||||
@database_to_snapshot_id = {}
|
@database_to_snapshot_id = {}
|
||||||
|
|
||||||
if @database_to_snapshot_id.empty?
|
if @database_to_snapshot_id.empty?
|
||||||
::Gitlab::Database::EachDatabase.each_database_connection(
|
::Gitlab::Database::EachDatabase.each_connection(
|
||||||
only: base_models_for_backup.keys, include_shared: false
|
only: base_models_for_backup.keys, include_shared: false
|
||||||
) do |connection, database_name|
|
) do |connection, database_name|
|
||||||
@database_to_snapshot_id[database_name] = nil
|
@database_to_snapshot_id[database_name] = nil
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ module Gitlab
|
||||||
class ProjectPipelineStatus
|
class ProjectPipelineStatus
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
STATUS_KEY_TTL = 8.hours
|
||||||
|
|
||||||
attr_accessor :sha, :status, :ref, :project, :loaded
|
attr_accessor :sha, :status, :ref, :project, :loaded
|
||||||
|
|
||||||
def self.load_for_project(project)
|
def self.load_for_project(project)
|
||||||
|
|
@ -89,12 +91,17 @@ module Gitlab
|
||||||
self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)
|
self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)
|
||||||
|
|
||||||
self.status = nil if self.status.empty?
|
self.status = nil if self.status.empty?
|
||||||
|
|
||||||
|
redis.expire(cache_key, STATUS_KEY_TTL)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_in_cache
|
def store_in_cache
|
||||||
with_redis do |redis|
|
with_redis do |redis|
|
||||||
redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
|
redis.pipelined do |p|
|
||||||
|
p.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
|
||||||
|
p.expire(cache_key, STATUS_KEY_TTL)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ module Gitlab
|
||||||
module Database
|
module Database
|
||||||
module EachDatabase
|
module EachDatabase
|
||||||
class << self
|
class << self
|
||||||
def each_database_connection(only: nil, include_shared: true)
|
def each_connection(only: nil, include_shared: true)
|
||||||
selected_names = Array.wrap(only)
|
selected_names = Array.wrap(only)
|
||||||
base_models = select_base_models(selected_names)
|
base_models = select_base_models(selected_names)
|
||||||
|
|
||||||
|
|
@ -18,7 +18,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
alias_method :each_db_connection, :each_database_connection
|
|
||||||
|
|
||||||
def each_model_connection(models, only_on: nil, &blk)
|
def each_model_connection(models, only_on: nil, &blk)
|
||||||
selected_databases = Array.wrap(only_on).map(&:to_sym)
|
selected_databases = Array.wrap(only_on).map(&:to_sym)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ module Gitlab
|
||||||
result_dir = background_migrations_dir(for_database, legacy_mode)
|
result_dir = background_migrations_dir(for_database, legacy_mode)
|
||||||
|
|
||||||
# Only one loop iteration since we pass `only:` here
|
# Only one loop iteration since we pass `only:` here
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: for_database) do |connection|
|
Gitlab::Database::EachDatabase.each_connection(only: for_database) do |connection|
|
||||||
from_id = batched_migrations_last_id(for_database).read
|
from_id = batched_migrations_last_id(for_database).read
|
||||||
|
|
||||||
runner = Gitlab::Database::Migrations::TestBatchedBackgroundRunner
|
runner = Gitlab::Database::Migrations::TestBatchedBackgroundRunner
|
||||||
|
|
@ -68,7 +68,7 @@ module Gitlab
|
||||||
runner = nil
|
runner = nil
|
||||||
base_dir = background_migrations_dir(for_database, false)
|
base_dir = background_migrations_dir(for_database, false)
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: for_database) do |connection|
|
Gitlab::Database::EachDatabase.each_connection(only: for_database) do |connection|
|
||||||
runner = Gitlab::Database::Migrations::BatchedMigrationLastId
|
runner = Gitlab::Database::Migrations::BatchedMigrationLastId
|
||||||
.new(connection, base_dir)
|
.new(connection, base_dir)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ module Gitlab
|
||||||
next if model < ::Gitlab::Database::SharedModel && !(model < TableWithoutModel)
|
next if model < ::Gitlab::Database::SharedModel && !(model < TableWithoutModel)
|
||||||
|
|
||||||
model_connection_name = model.connection_db_config.name
|
model_connection_name = model.connection_db_config.name
|
||||||
Gitlab::Database::EachDatabase.each_db_connection(include_shared: false) do |connection, connection_name|
|
Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection, connection_name|
|
||||||
if connection_name != model_connection_name
|
if connection_name != model_connection_name
|
||||||
PartitionManager.new(model, connection: connection).sync_partitions
|
PartitionManager.new(model, connection: connection).sync_partitions
|
||||||
end
|
end
|
||||||
|
|
@ -64,7 +64,7 @@ module Gitlab
|
||||||
|
|
||||||
Gitlab::AppLogger.info(message: 'Dropping detached postgres partitions')
|
Gitlab::AppLogger.info(message: 'Dropping detached postgres partitions')
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do
|
Gitlab::Database::EachDatabase.each_connection do
|
||||||
DetachedPartitionDropper.new.perform
|
DetachedPartitionDropper.new.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.invoke(database = nil)
|
def self.invoke(database = nil)
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection, connection_name|
|
Gitlab::Database::EachDatabase.each_connection do |connection, connection_name|
|
||||||
next if database && database.to_s != connection_name.to_s
|
next if database && database.to_s != connection_name.to_s
|
||||||
|
|
||||||
Gitlab::Database::SharedModel.logger = Logger.new($stdout) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false)
|
Gitlab::Database::SharedModel.logger = Logger.new($stdout) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def unlock_writes
|
def unlock_writes
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name|
|
Gitlab::Database::EachDatabase.each_connection do |connection, database_name|
|
||||||
tables_to_lock(connection) do |table_name, schema_name|
|
tables_to_lock(connection) do |table_name, schema_name|
|
||||||
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/366834
|
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/366834
|
||||||
next if schema_name.in? GITLAB_SCHEMAS_TO_IGNORE
|
next if schema_name.in? GITLAB_SCHEMAS_TO_IGNORE
|
||||||
|
|
@ -28,7 +28,7 @@ module Gitlab
|
||||||
# It locks the tables on the database where they don't belong. Also it unlocks the tables
|
# It locks the tables on the database where they don't belong. Also it unlocks the tables
|
||||||
# on the database where they belong
|
# on the database where they belong
|
||||||
def lock_writes
|
def lock_writes
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection, database_name|
|
Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection, database_name|
|
||||||
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
||||||
|
|
||||||
tables_to_lock(connection) do |table_name, schema_name|
|
tables_to_lock(connection) do |table_name, schema_name|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ module Gitlab
|
||||||
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
|
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
|
||||||
push_frontend_feature_flag(:remove_monitor_metrics)
|
push_frontend_feature_flag(:remove_monitor_metrics)
|
||||||
push_frontend_feature_flag(:gitlab_duo, current_user)
|
push_frontend_feature_flag(:gitlab_duo, current_user)
|
||||||
|
push_frontend_feature_flag(:custom_emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Exposes the state of a feature flag to the frontend code.
|
# Exposes the state of a feature flag to the frontend code.
|
||||||
|
|
|
||||||
|
|
@ -126,12 +126,12 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.without_statement_timeout
|
def self.without_statement_timeout
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection|
|
Gitlab::Database::EachDatabase.each_connection do |connection|
|
||||||
connection.execute('SET statement_timeout=0')
|
connection.execute('SET statement_timeout=0')
|
||||||
end
|
end
|
||||||
yield
|
yield
|
||||||
ensure
|
ensure
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection|
|
Gitlab::Database::EachDatabase.each_connection do |connection|
|
||||||
connection.execute('RESET statement_timeout')
|
connection.execute('RESET statement_timeout')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace :dev do
|
||||||
ENV['force'] = 'yes'
|
ENV['force'] = 'yes'
|
||||||
Rake::Task["gitlab:setup"].invoke
|
Rake::Task["gitlab:setup"].invoke
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection|
|
Gitlab::Database::EachDatabase.each_connection do |connection|
|
||||||
# Make sure DB statistics are up to date.
|
# Make sure DB statistics are up to date.
|
||||||
# gitlab:setup task can insert quite a bit of data, especially with MASS_INSERT=1
|
# gitlab:setup task can insert quite a bit of data, especially with MASS_INSERT=1
|
||||||
# so ANALYZE can take more than default 15s statement timeout. This being a dev task,
|
# so ANALYZE can take more than default 15s statement timeout. This being a dev task,
|
||||||
|
|
@ -61,7 +61,7 @@ namespace :dev do
|
||||||
AND pid <> pg_backend_pid();
|
AND pid <> pg_backend_pid();
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection|
|
Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection|
|
||||||
connection.execute(cmd)
|
connection.execute(cmd)
|
||||||
rescue ActiveRecord::NoDatabaseError
|
rescue ActiveRecord::NoDatabaseError
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ namespace :gitlab do
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |connection, name|
|
Gitlab::Database::EachDatabase.each_connection(only: only_on) do |connection, name|
|
||||||
connection.execute("INSERT INTO schema_migrations (version) VALUES (#{connection.quote(version)})")
|
connection.execute("INSERT INTO schema_migrations (version) VALUES (#{connection.quote(version)})")
|
||||||
|
|
||||||
puts "Successfully marked '#{version}' as complete on database #{name}".color(:green)
|
puts "Successfully marked '#{version}' as complete on database #{name}".color(:green)
|
||||||
|
|
@ -57,7 +57,7 @@ namespace :gitlab do
|
||||||
end
|
end
|
||||||
|
|
||||||
def drop_tables(only_on: nil)
|
def drop_tables(only_on: nil)
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |connection, name|
|
Gitlab::Database::EachDatabase.each_connection(only: only_on) do |connection, name|
|
||||||
# In PostgreSQLAdapter, data_sources returns both views and tables, so use tables instead
|
# In PostgreSQLAdapter, data_sources returns both views and tables, so use tables instead
|
||||||
tables = connection.tables
|
tables = connection.tables
|
||||||
|
|
||||||
|
|
@ -292,7 +292,7 @@ namespace :gitlab do
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: database_name) do
|
Gitlab::Database::EachDatabase.each_connection(only: database_name) do
|
||||||
Gitlab::Database::AsyncIndexes.execute_pending_actions!(how_many: args[:pick].to_i)
|
Gitlab::Database::AsyncIndexes.execute_pending_actions!(how_many: args[:pick].to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -322,7 +322,7 @@ namespace :gitlab do
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: database_name) do
|
Gitlab::Database::EachDatabase.each_connection(only: database_name) do
|
||||||
Gitlab::Database::AsyncConstraints.validate_pending_entries!(how_many: args[:pick].to_i)
|
Gitlab::Database::AsyncConstraints.validate_pending_entries!(how_many: args[:pick].to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -413,7 +413,7 @@ namespace :gitlab do
|
||||||
|
|
||||||
desc 'Run all pending batched migrations'
|
desc 'Run all pending batched migrations'
|
||||||
task execute_batched_migrations: :environment do
|
task execute_batched_migrations: :environment do
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection, name|
|
Gitlab::Database::EachDatabase.each_connection do |connection, name|
|
||||||
Gitlab::Database::BackgroundMigration::BatchedMigration.with_status(:active).queue_order.each do |migration|
|
Gitlab::Database::BackgroundMigration::BatchedMigration.with_status(:active).queue_order.each do |migration|
|
||||||
Gitlab::AppLogger.info("Executing batched migration #{migration.id} on database #{name} inline")
|
Gitlab::AppLogger.info("Executing batched migration #{migration.id} on database #{name} inline")
|
||||||
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.new(connection: connection).run_entire_migration(migration)
|
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.new(connection: connection).run_entire_migration(migration)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ task migration_fix_15_11: [:environment] do
|
||||||
next if Gitlab.com?
|
next if Gitlab.com?
|
||||||
|
|
||||||
only_on = %i[main ci].select { |db| Gitlab::Database.has_database?(db) }
|
only_on = %i[main ci].select { |db| Gitlab::Database.has_database?(db) }
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |conn, database|
|
Gitlab::Database::EachDatabase.each_connection(only: only_on) do |conn, database|
|
||||||
begin
|
begin
|
||||||
first_migration = conn.execute('SELECT * FROM schema_migrations ORDER BY version ASC LIMIT 1')
|
first_migration = conn.execute('SELECT * FROM schema_migrations ORDER BY version ASC LIMIT 1')
|
||||||
rescue ActiveRecord::StatementInvalid
|
rescue ActiveRecord::StatementInvalid
|
||||||
|
|
|
||||||
|
|
@ -1959,6 +1959,9 @@ msgstr ""
|
||||||
msgid "AI|I don't see how I can help. Please give better instructions!"
|
msgid "AI|I don't see how I can help. Please give better instructions!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AI|May provide inappropriate responses not representative of GitLab's views. Do not input personal data."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "AI|Populate issue description"
|
msgid "AI|Populate issue description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -35122,6 +35125,9 @@ msgstr ""
|
||||||
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
|
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered. You might have to wait seven days before creating a new account with the same username or email."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
|
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -55056,6 +55062,9 @@ msgstr ""
|
||||||
msgid "must be before %{expiry_date}"
|
msgid "must be before %{expiry_date}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "must be false when email confirmation setting is off"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "must be greater than start date"
|
msgid "must be greater than start date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ RSpec.describe 'Database schema', feature_category: :database do
|
||||||
}.with_indifferent_access.freeze
|
}.with_indifferent_access.freeze
|
||||||
|
|
||||||
context 'for table' do
|
context 'for table' do
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection, _|
|
Gitlab::Database::EachDatabase.each_connection do |connection, _|
|
||||||
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
||||||
(connection.tables - TABLE_PARTITIONS).sort.each do |table|
|
(connection.tables - TABLE_PARTITIONS).sort.each do |table|
|
||||||
table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
|
table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
|
||||||
|
|
@ -300,7 +300,7 @@ RSpec.describe 'Database schema', feature_category: :database do
|
||||||
|
|
||||||
context 'primary keys' do
|
context 'primary keys' do
|
||||||
it 'expects every table to have a primary key defined' do
|
it 'expects every table to have a primary key defined' do
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection, _|
|
Gitlab::Database::EachDatabase.each_connection do |connection, _|
|
||||||
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
||||||
|
|
||||||
problematic_tables = connection.tables.select do |table|
|
problematic_tables = connection.tables.select do |table|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ RSpec.describe AwardEmojisFinder do
|
||||||
let_it_be(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) }
|
let_it_be(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) }
|
||||||
let_it_be(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) }
|
let_it_be(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(custom_emoji: false)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'param validation' do
|
describe 'param validation' do
|
||||||
it 'raises an error if `name` is invalid' do
|
it 'raises an error if `name` is invalid' do
|
||||||
expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error(
|
expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
|
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import { createMockClient } from 'helpers/mock_apollo_helper';
|
||||||
import installGlEmojiElement from '~/behaviors/gl_emoji';
|
import installGlEmojiElement from '~/behaviors/gl_emoji';
|
||||||
import { EMOJI_VERSION } from '~/emoji';
|
import { EMOJI_VERSION } from '~/emoji';
|
||||||
|
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
|
||||||
|
|
||||||
import * as EmojiUnicodeSupport from '~/emoji/support';
|
import * as EmojiUnicodeSupport from '~/emoji/support';
|
||||||
|
|
||||||
|
let mockClient;
|
||||||
|
|
||||||
jest.mock('~/emoji/support');
|
jest.mock('~/emoji/support');
|
||||||
|
jest.mock('~/lib/graphql', () => {
|
||||||
|
return () => mockClient;
|
||||||
|
});
|
||||||
|
|
||||||
describe('gl_emoji', () => {
|
describe('gl_emoji', () => {
|
||||||
const emojiData = {
|
const emojiData = {
|
||||||
|
|
@ -36,16 +43,17 @@ describe('gl_emoji', () => {
|
||||||
return div.firstElementChild;
|
return div.firstElementChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await initEmojiMock(emojiData);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
clearEmojiMock();
|
clearEmojiMock();
|
||||||
|
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('standard emoji', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await initEmojiMock(emojiData);
|
||||||
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
[
|
[
|
||||||
'bomb emoji just with name attribute',
|
'bomb emoji just with name attribute',
|
||||||
|
|
@ -134,3 +142,45 @@ describe('gl_emoji', () => {
|
||||||
expect(window.gon.emoji_sprites_css_added).toBe(true);
|
expect(window.gon.emoji_sprites_css_added).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('custom emoji', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockClient = createMockClient([
|
||||||
|
[
|
||||||
|
customEmojiQuery,
|
||||||
|
jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
group: {
|
||||||
|
id: 1,
|
||||||
|
customEmoji: {
|
||||||
|
nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
window.gon = { features: { customEmoji: true } };
|
||||||
|
document.body.dataset.group = 'test-group';
|
||||||
|
|
||||||
|
await initEmojiMock(emojiData);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.gon = {};
|
||||||
|
delete document.body.dataset.group;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders custom emoji', async () => {
|
||||||
|
const glEmojiElement = markupToDomElement('<gl-emoji data-name="parrot"></gl-emoji>');
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
const img = glEmojiElement.querySelector('img');
|
||||||
|
|
||||||
|
expect(glEmojiElement.dataset.unicodeVersion).toBe('custom');
|
||||||
|
expect(img.getAttribute('src')).toBe('parrot.gif');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ exports[`Design note component should match the snapshot 1`] = `
|
||||||
id="note_123"
|
id="note_123"
|
||||||
>
|
>
|
||||||
<glavatarlink-stub
|
<glavatarlink-stub
|
||||||
class="gl-float-left gl-mr-3"
|
class="gl-float-left gl-mr-3 link-inherit-color"
|
||||||
href="https://gitlab.com/user"
|
href="https://gitlab.com/user"
|
||||||
>
|
>
|
||||||
<glavatar-stub
|
<glavatar-stub
|
||||||
|
|
@ -24,7 +24,7 @@ exports[`Design note component should match the snapshot 1`] = `
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<gllink-stub
|
<gllink-stub
|
||||||
class="js-user-link"
|
class="js-user-link link-inherit-color"
|
||||||
data-testid="user-link"
|
data-testid="user-link"
|
||||||
data-user-id="1"
|
data-user-id="1"
|
||||||
data-username="foo-bar"
|
data-username="foo-bar"
|
||||||
|
|
@ -53,7 +53,7 @@ exports[`Design note component should match the snapshot 1`] = `
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<gllink-stub
|
<gllink-stub
|
||||||
class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm"
|
class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm link-inherit-color"
|
||||||
href="#note_123"
|
href="#note_123"
|
||||||
>
|
>
|
||||||
<timeagotooltip-stub
|
<timeagotooltip-stub
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const DEFAULT_TODO_COUNT = 2;
|
||||||
describe('Design discussions component', () => {
|
describe('Design discussions component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
const findDesignNotesList = () => wrapper.find('[data-testid="design-discussion-content"]');
|
||||||
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
|
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
|
||||||
const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder);
|
const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder);
|
||||||
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
|
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
|
||||||
|
|
@ -287,7 +288,7 @@ describe('Design discussions component', () => {
|
||||||
|
|
||||||
describe('when any note from a discussion is active', () => {
|
describe('when any note from a discussion is active', () => {
|
||||||
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
|
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
|
||||||
'applies correct class to all notes in the active discussion',
|
'applies correct class to the active discussion',
|
||||||
(note) => {
|
(note) => {
|
||||||
createComponent({
|
createComponent({
|
||||||
props: { discussion: mockDiscussion },
|
props: { discussion: mockDiscussion },
|
||||||
|
|
@ -299,11 +300,7 @@ describe('Design discussions component', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(findDesignNotesList().classes('gl-bg-blue-50')).toBe(true);
|
||||||
wrapper
|
|
||||||
.findAllComponents(DesignNote)
|
|
||||||
.wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')),
|
|
||||||
).toBe(true);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
clearEmojiMock,
|
clearEmojiMock,
|
||||||
} from 'helpers/emoji';
|
} from 'helpers/emoji';
|
||||||
import { trimText } from 'helpers/text_helper';
|
import { trimText } from 'helpers/text_helper';
|
||||||
|
import { createMockClient } from 'helpers/mock_apollo_helper';
|
||||||
import {
|
import {
|
||||||
glEmojiTag,
|
glEmojiTag,
|
||||||
searchEmoji,
|
searchEmoji,
|
||||||
|
|
@ -14,6 +15,8 @@ import {
|
||||||
sortEmoji,
|
sortEmoji,
|
||||||
initEmojiMap,
|
initEmojiMap,
|
||||||
getAllEmoji,
|
getAllEmoji,
|
||||||
|
emojiFallbackImageSrc,
|
||||||
|
loadCustomEmojiWithNames,
|
||||||
} from '~/emoji';
|
} from '~/emoji';
|
||||||
|
|
||||||
import isEmojiUnicodeSupported, {
|
import isEmojiUnicodeSupported, {
|
||||||
|
|
@ -25,6 +28,12 @@ import isEmojiUnicodeSupported, {
|
||||||
isPersonZwjEmoji,
|
isPersonZwjEmoji,
|
||||||
} from '~/emoji/support/is_emoji_unicode_supported';
|
} from '~/emoji/support/is_emoji_unicode_supported';
|
||||||
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
|
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
|
||||||
|
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
|
||||||
|
|
||||||
|
let mockClient;
|
||||||
|
jest.mock('~/lib/graphql', () => {
|
||||||
|
return () => mockClient;
|
||||||
|
});
|
||||||
|
|
||||||
const emptySupportMap = {
|
const emptySupportMap = {
|
||||||
personZwj: false,
|
personZwj: false,
|
||||||
|
|
@ -45,12 +54,35 @@ const emptySupportMap = {
|
||||||
1.1: false,
|
1.1: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createMockEmojiClient() {
|
||||||
|
mockClient = createMockClient([
|
||||||
|
[
|
||||||
|
customEmojiQuery,
|
||||||
|
jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
group: {
|
||||||
|
id: 1,
|
||||||
|
customEmoji: {
|
||||||
|
nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
window.gon = { features: { customEmoji: true } };
|
||||||
|
document.body.dataset.group = 'test-group';
|
||||||
|
}
|
||||||
|
|
||||||
describe('emoji', () => {
|
describe('emoji', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await initEmojiMock();
|
await initEmojiMock();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
window.gon = {};
|
||||||
|
delete document.body.dataset.group;
|
||||||
clearEmojiMock();
|
clearEmojiMock();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -690,4 +722,67 @@ describe('emoji', () => {
|
||||||
expect(scoredItems.sort(sortEmoji)).toEqual(expected);
|
expect(scoredItems.sort(sortEmoji)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('emojiFallbackImageSrc', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
createMockEmojiClient();
|
||||||
|
|
||||||
|
await initEmojiMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
emoji | src
|
||||||
|
${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'}
|
||||||
|
${'parrot'} | ${'parrot.gif'}
|
||||||
|
`('returns $src for emoji with name $emoji', ({ emoji, src }) => {
|
||||||
|
expect(emojiFallbackImageSrc(emoji)).toBe(src);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadCustomEmojiWithNames', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createMockEmojiClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flag disabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.gon = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object', async () => {
|
||||||
|
const result = await loadCustomEmojiWithNames();
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when not in a group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
delete document.body.dataset.group;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object', async () => {
|
||||||
|
const result = await loadCustomEmojiWithNames();
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when in a group with flag enabled', () => {
|
||||||
|
it('returns empty object', async () => {
|
||||||
|
const result = await loadCustomEmojiWithNames();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
parrot: {
|
||||||
|
c: 'custom',
|
||||||
|
d: 'parrot',
|
||||||
|
e: undefined,
|
||||||
|
name: 'parrot',
|
||||||
|
src: 'parrot.gif',
|
||||||
|
u: 'custom',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
|
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
|
||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||||
|
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
|
||||||
import { sprintf } from '~/locale';
|
import { sprintf } from '~/locale';
|
||||||
import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
|
import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
|
||||||
import * as utils from '~/gitlab_version_check/utils';
|
import * as utils from '~/gitlab_version_check/utils';
|
||||||
|
|
@ -14,6 +15,8 @@ import {
|
||||||
describe('SecurityPatchUpgradeAlertModal', () => {
|
describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let trackingSpy;
|
let trackingSpy;
|
||||||
|
const hideMock = jest.fn();
|
||||||
|
const { i18n } = SecurityPatchUpgradeAlertModal;
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
currentVersion: '11.1.1',
|
currentVersion: '11.1.1',
|
||||||
|
|
@ -28,14 +31,20 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
GlModal,
|
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
|
GlModal: stubComponent(GlModal, {
|
||||||
|
methods: {
|
||||||
|
hide: hideMock,
|
||||||
|
},
|
||||||
|
template: RENDER_ALL_SLOTS_TEMPLATE,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
unmockTracking();
|
unmockTracking();
|
||||||
|
hideMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectDispatchedTracking = (action, label) => {
|
const expectDispatchedTracking = (action, label) => {
|
||||||
|
|
@ -63,12 +72,12 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the modal title correctly', () => {
|
it('renders the modal title correctly', () => {
|
||||||
expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle);
|
expect(findGlModalTitle().text()).toBe(i18n.modalTitle);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders modal body without suggested versions', () => {
|
it('renders modal body without suggested versions', () => {
|
||||||
expect(findGlModalBody().text()).toBe(
|
expect(findGlModalBody().text()).toBe(
|
||||||
sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, {
|
sprintf(i18n.modalBodyNoStableVersions, {
|
||||||
currentVersion: defaultProps.currentVersion,
|
currentVersion: defaultProps.currentVersion,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -90,7 +99,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
|
|
||||||
describe('Learn more link', () => {
|
describe('Learn more link', () => {
|
||||||
it('renders with correct text and link', () => {
|
it('renders with correct text and link', () => {
|
||||||
expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore);
|
expect(findGlLink().text()).toBe(i18n.learnMore);
|
||||||
expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
|
expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -102,12 +111,8 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Remind me button', () => {
|
describe('Remind me button', () => {
|
||||||
beforeEach(() => {
|
|
||||||
wrapper.vm.$refs.alertModal.hide = jest.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with correct text', () => {
|
it('renders with correct text', () => {
|
||||||
expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText);
|
expect(findGlRemindButton().text()).toBe(i18n.secondaryButtonText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
|
it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
|
||||||
|
|
@ -126,13 +131,13 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
it('hides the modal', async () => {
|
it('hides the modal', async () => {
|
||||||
await findGlRemindButton().vm.$emit('click');
|
await findGlRemindButton().vm.$emit('click');
|
||||||
|
|
||||||
expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled();
|
expect(hideMock).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Upgrade button', () => {
|
describe('Upgrade button', () => {
|
||||||
it('renders with correct text and link', () => {
|
it('renders with correct text and link', () => {
|
||||||
expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText);
|
expect(findGlUpgradeButton().text()).toBe(i18n.primaryButtonText);
|
||||||
expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
|
expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -160,7 +165,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
|
|
||||||
it('renders modal body with suggested versions', () => {
|
it('renders modal body with suggested versions', () => {
|
||||||
expect(findGlModalBody().text()).toBe(
|
expect(findGlModalBody().text()).toBe(
|
||||||
sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, {
|
sprintf(i18n.modalBodyStableVersions, {
|
||||||
currentVersion: defaultProps.currentVersion,
|
currentVersion: defaultProps.currentVersion,
|
||||||
latestStableVersions: latestStableVersions.join(', '),
|
latestStableVersions: latestStableVersions.join(', '),
|
||||||
}),
|
}),
|
||||||
|
|
@ -176,9 +181,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders modal details', () => {
|
it('renders modal details', () => {
|
||||||
expect(findGlModalDetails().text()).toBe(
|
expect(findGlModalDetails().text()).toBe(sprintf(i18n.modalDetails, { details }));
|
||||||
sprintf(wrapper.vm.$options.i18n.modalDetails, { details }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { DRAWIO_ORIGIN } from 'spec/test_constants';
|
import { DRAWIO_ORIGIN } from 'spec/test_constants';
|
||||||
|
|
||||||
jest.mock('~/emoji');
|
jest.mock('~/emoji');
|
||||||
|
jest.mock('~/lib/graphql');
|
||||||
|
|
||||||
describe('WikiForm', () => {
|
describe('WikiForm', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
|
||||||
jest.mock('~/emoji');
|
jest.mock('~/emoji');
|
||||||
jest.mock('autosize');
|
jest.mock('autosize');
|
||||||
|
jest.mock('~/lib/graphql');
|
||||||
|
|
||||||
describe('vue_shared/component/markdown/markdown_editor', () => {
|
describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
useLocalStorageSpy();
|
useLocalStorageSpy();
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,11 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac
|
||||||
|
|
||||||
pipeline_status.store_in_cache
|
pipeline_status.store_in_cache
|
||||||
read_sha, read_status = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status) }
|
read_sha, read_status = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status) }
|
||||||
|
ttl = Gitlab::Redis::Cache.with { |redis| redis.ttl(cache_key) }
|
||||||
|
|
||||||
expect(read_sha).to eq('123456')
|
expect(read_sha).to eq('123456')
|
||||||
expect(read_status).to eq('failed')
|
expect(read_status).to eq('failed')
|
||||||
|
expect(ttl).to be > 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -254,14 +256,24 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#load_from_cache' do
|
describe '#load_from_cache' do
|
||||||
|
subject { pipeline_status.load_from_cache }
|
||||||
|
|
||||||
it 'reads the status from redis_cache' do
|
it 'reads the status from redis_cache' do
|
||||||
pipeline_status.load_from_cache
|
subject
|
||||||
|
|
||||||
expect(pipeline_status.sha).to eq(sha)
|
expect(pipeline_status.sha).to eq(sha)
|
||||||
expect(pipeline_status.status).to eq(status)
|
expect(pipeline_status.status).to eq(status)
|
||||||
expect(pipeline_status.ref).to eq(ref)
|
expect(pipeline_status.ref).to eq(ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'refreshes ttl' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
ttl = Gitlab::Redis::Cache.with { |redis| redis.ttl(cache_key) }
|
||||||
|
|
||||||
|
expect(ttl).to be > 0
|
||||||
|
end
|
||||||
|
|
||||||
context 'when status is empty string' do
|
context 'when status is empty string' do
|
||||||
before do
|
before do
|
||||||
Gitlab::Redis::Cache.with do |redis|
|
Gitlab::Redis::Cache.with do |redis|
|
||||||
|
|
@ -271,7 +283,7 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'reads the status as nil' do
|
it 'reads the status as nil' do
|
||||||
pipeline_status.load_from_cache
|
subject
|
||||||
|
|
||||||
expect(pipeline_status.status).to eq(nil)
|
expect(pipeline_status.status).to eq(nil)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::Database::EachDatabase do
|
RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
describe '.each_database_connection', :add_ci_connection do
|
describe '.each_connection', :add_ci_connection do
|
||||||
let(:database_base_models) { { main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access }
|
let(:database_base_models) { { main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
@ -17,7 +17,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
||||||
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
||||||
|
|
||||||
expect { |b| described_class.each_database_connection(&b) }
|
expect { |b| described_class.each_connection(&b) }
|
||||||
.to yield_successive_args(
|
.to yield_successive_args(
|
||||||
[ActiveRecord::Base.connection, 'main'],
|
[ActiveRecord::Base.connection, 'main'],
|
||||||
[Ci::ApplicationRecord.connection, 'ci']
|
[Ci::ApplicationRecord.connection, 'ci']
|
||||||
|
|
@ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
||||||
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
||||||
|
|
||||||
expect { |b| described_class.each_database_connection(only: 'ci', &b) }
|
expect { |b| described_class.each_connection(only: 'ci', &b) }
|
||||||
.to yield_successive_args([Ci::ApplicationRecord.connection, 'ci'])
|
.to yield_successive_args([Ci::ApplicationRecord.connection, 'ci'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
||||||
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
||||||
|
|
||||||
expect { |b| described_class.each_database_connection(only: :ci, &b) }
|
expect { |b| described_class.each_connection(only: :ci, &b) }
|
||||||
.to yield_successive_args([Ci::ApplicationRecord.connection, 'ci'])
|
.to yield_successive_args([Ci::ApplicationRecord.connection, 'ci'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -46,7 +46,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
context 'when the selected names are invalid' do
|
context 'when the selected names are invalid' do
|
||||||
it 'does not yield any connections' do
|
it 'does not yield any connections' do
|
||||||
expect do |b|
|
expect do |b|
|
||||||
described_class.each_database_connection(only: :notvalid, &b)
|
described_class.each_connection(only: :notvalid, &b)
|
||||||
rescue ArgumentError => e
|
rescue ArgumentError => e
|
||||||
expect(e.message).to match(/notvalid is not a valid database name/)
|
expect(e.message).to match(/notvalid is not a valid database name/)
|
||||||
end.not_to yield_control
|
end.not_to yield_control
|
||||||
|
|
@ -54,7 +54,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
|
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
expect do
|
expect do
|
||||||
described_class.each_database_connection(only: :notvalid) {}
|
described_class.each_connection(only: :notvalid) {}
|
||||||
end.to raise_error(ArgumentError, /notvalid is not a valid database name/)
|
end.to raise_error(ArgumentError, /notvalid is not a valid database name/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -78,7 +78,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
|
||||||
db_config.name != 'main' ? 'main' : nil
|
db_config.name != 'main' ? 'main' : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
expect { |b| described_class.each_database_connection(include_shared: false, &b) }
|
expect { |b| described_class.each_connection(include_shared: false, &b) }
|
||||||
.to yield_successive_args([ActiveRecord::Base.connection, 'main'])
|
.to yield_successive_args([ActiveRecord::Base.connection, 'main'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ RSpec.describe Gitlab::Database::Partitioning, feature_category: :database do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'drops detached partitions for each database' do
|
it 'drops detached partitions for each database' do
|
||||||
expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection).and_yield
|
expect(Gitlab::Database::EachDatabase).to receive(:each_connection).and_yield
|
||||||
|
|
||||||
expect { described_class.drop_detached_partitions }
|
expect { described_class.drop_detached_partitions }
|
||||||
.to change { Postgresql::DetachedPartition.count }.from(2).to(0)
|
.to change { Postgresql::DetachedPartition.count }.from(2).to(0)
|
||||||
|
|
|
||||||
|
|
@ -2657,232 +2657,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#update_shared_runners_setting!' do
|
|
||||||
context 'enabled' do
|
|
||||||
subject { group.update_shared_runners_setting!('enabled') }
|
|
||||||
|
|
||||||
context 'group that its ancestors have shared runners disabled' do
|
|
||||||
let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled) }
|
|
||||||
let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, parent: parent) }
|
|
||||||
let_it_be(:project, reload: true) { create(:project, shared_runners_enabled: false, group: group) }
|
|
||||||
|
|
||||||
it 'raises exception' do
|
|
||||||
expect { subject }
|
|
||||||
.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not enable shared runners' do
|
|
||||||
expect do
|
|
||||||
begin
|
|
||||||
subject
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
parent.reload
|
|
||||||
group.reload
|
|
||||||
project.reload
|
|
||||||
end.to not_change { parent.shared_runners_enabled }
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
.and not_change { project.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'root group with shared runners disabled' do
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
||||||
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
|
||||||
|
|
||||||
it 'enables shared Runners only for itself' do
|
|
||||||
expect { subject_and_reload(group, sub_group, project) }
|
|
||||||
.to change { group.shared_runners_enabled }.from(false).to(true)
|
|
||||||
.and not_change { sub_group.shared_runners_enabled }
|
|
||||||
.and not_change { project.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'disabled_and_unoverridable' do
|
|
||||||
let_it_be(:group) { create(:group) }
|
|
||||||
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
|
|
||||||
let_it_be(:sub_group_2) { create(:group, parent: group) }
|
|
||||||
let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) }
|
|
||||||
let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) }
|
|
||||||
|
|
||||||
subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_UNOVERRIDABLE) }
|
|
||||||
|
|
||||||
it 'disables shared Runners for all descendant groups and projects' do
|
|
||||||
expect { subject_and_reload(group, sub_group, sub_group_2, project, project_2) }
|
|
||||||
.to change { group.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and not_change { group.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { sub_group.shared_runners_enabled }
|
|
||||||
.and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
|
||||||
.and change { sub_group_2.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and not_change { sub_group_2.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and change { project.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and change { project_2.shared_runners_enabled }.from(true).to(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with override on self' do
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
|
||||||
|
|
||||||
it 'disables it' do
|
|
||||||
expect { subject_and_reload(group) }
|
|
||||||
.to not_change { group.shared_runners_enabled }
|
|
||||||
.and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'disabled_and_overridable' do
|
|
||||||
subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_OVERRIDABLE) }
|
|
||||||
|
|
||||||
context 'top level group' do
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
||||||
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
|
||||||
|
|
||||||
it 'enables allow descendants to override only for itself' do
|
|
||||||
expect { subject_and_reload(group, sub_group, project) }
|
|
||||||
.to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
.and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { sub_group.shared_runners_enabled }
|
|
||||||
.and not_change { project.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'group that its ancestors have shared Runners disabled but allows to override' do
|
|
||||||
let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
|
||||||
|
|
||||||
it 'enables allow descendants to override' do
|
|
||||||
expect { subject_and_reload(parent, group, project) }
|
|
||||||
.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { parent.shared_runners_enabled }
|
|
||||||
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
.and not_change { project.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when parent does not allow' do
|
|
||||||
let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
|
|
||||||
let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
|
|
||||||
|
|
||||||
it 'raises exception' do
|
|
||||||
expect { subject }
|
|
||||||
.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not allow descendants to override' do
|
|
||||||
expect do
|
|
||||||
begin
|
|
||||||
subject
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
parent.reload
|
|
||||||
group.reload
|
|
||||||
end.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { parent.shared_runners_enabled }
|
|
||||||
.and not_change { group.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'top level group that has shared Runners enabled' do
|
|
||||||
let_it_be(:group) { create(:group, shared_runners_enabled: true) }
|
|
||||||
let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
|
|
||||||
|
|
||||||
it 'enables allow descendants to override & disables shared runners everywhere' do
|
|
||||||
expect { subject_and_reload(group, sub_group, project) }
|
|
||||||
.to change { group.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
||||||
.and change { sub_group.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and change { project.shared_runners_enabled }.from(true).to(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'disabled_with_override (deprecated)' do
|
|
||||||
subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_WITH_OVERRIDE) }
|
|
||||||
|
|
||||||
context 'top level group' do
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
||||||
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
|
||||||
|
|
||||||
it 'enables allow descendants to override only for itself' do
|
|
||||||
expect { subject_and_reload(group, sub_group, project) }
|
|
||||||
.to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
.and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { sub_group.shared_runners_enabled }
|
|
||||||
.and not_change { project.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'group that its ancestors have shared Runners disabled but allows to override' do
|
|
||||||
let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
|
||||||
|
|
||||||
it 'enables allow descendants to override' do
|
|
||||||
expect { subject_and_reload(parent, group, project) }
|
|
||||||
.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { parent.shared_runners_enabled }
|
|
||||||
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
.and not_change { project.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when parent does not allow' do
|
|
||||||
let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
|
|
||||||
let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
|
|
||||||
|
|
||||||
it 'raises exception' do
|
|
||||||
expect { subject }
|
|
||||||
.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not allow descendants to override' do
|
|
||||||
expect do
|
|
||||||
begin
|
|
||||||
subject
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
parent.reload
|
|
||||||
group.reload
|
|
||||||
end.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { parent.shared_runners_enabled }
|
|
||||||
.and not_change { group.allow_descendants_override_disabled_shared_runners }
|
|
||||||
.and not_change { group.shared_runners_enabled }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'top level group that has shared Runners enabled' do
|
|
||||||
let_it_be(:group) { create(:group, shared_runners_enabled: true) }
|
|
||||||
let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
|
|
||||||
let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
|
|
||||||
|
|
||||||
it 'enables allow descendants to override & disables shared runners everywhere' do
|
|
||||||
expect { subject_and_reload(group, sub_group, project) }
|
|
||||||
.to change { group.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
||||||
.and change { sub_group.shared_runners_enabled }.from(true).to(false)
|
|
||||||
.and change { project.shared_runners_enabled }.from(true).to(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#default_branch_name" do
|
describe "#default_branch_name" do
|
||||||
context "when group.namespace_settings does not have a default branch name" do
|
context "when group.namespace_settings does not have a default branch name" do
|
||||||
it "returns nil" do
|
it "returns nil" do
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,17 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and_projects do
|
RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and_projects do
|
||||||
|
include ReloadHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:group) { create(:group) }
|
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
|
let(:service) { described_class.new(group, user, params) }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
subject { described_class.new(group, user, params).execute }
|
subject { service.execute }
|
||||||
|
|
||||||
context 'when current_user is not the group owner' do
|
context 'when current_user is not the group owner' do
|
||||||
let_it_be(:group) { create(:group) }
|
let(:group) { create(:group) }
|
||||||
|
|
||||||
let(:params) { { shared_runners_setting: 'enabled' } }
|
let(:params) { { shared_runners_setting: 'enabled' } }
|
||||||
|
|
||||||
|
|
@ -19,9 +21,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
||||||
group.add_maintainer(user)
|
group.add_maintainer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'results error and does not call any method' do
|
it 'returns error' do
|
||||||
expect(group).not_to receive(:update_shared_runners_setting!)
|
|
||||||
|
|
||||||
expect(subject[:status]).to eq(:error)
|
expect(subject[:status]).to eq(:error)
|
||||||
expect(subject[:message]).to eq('Operation not allowed')
|
expect(subject[:message]).to eq('Operation not allowed')
|
||||||
expect(subject[:http_status]).to eq(403)
|
expect(subject[:http_status]).to eq(403)
|
||||||
|
|
@ -36,23 +36,36 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
||||||
context 'enable shared Runners' do
|
context 'enable shared Runners' do
|
||||||
let(:params) { { shared_runners_setting: 'enabled' } }
|
let(:params) { { shared_runners_setting: 'enabled' } }
|
||||||
|
|
||||||
context 'group that its ancestors have shared runners disabled' do
|
context 'when ancestor disable shared runners' do
|
||||||
let_it_be(:parent) { create(:group, :shared_runners_disabled) }
|
let(:parent) { create(:group, :shared_runners_disabled) }
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
let(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
||||||
|
let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
||||||
|
|
||||||
it 'results error' do
|
it 'returns an error and does not enable shared runners' do
|
||||||
|
expect do
|
||||||
expect(subject[:status]).to eq(:error)
|
expect(subject[:status]).to eq(:error)
|
||||||
expect(subject[:message]).to eq('Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
|
expect(subject[:message]).to eq('Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
|
||||||
|
|
||||||
|
reload_models(parent, group, project)
|
||||||
|
end.to not_change { parent.shared_runners_enabled }
|
||||||
|
.and not_change { group.shared_runners_enabled }
|
||||||
|
.and not_change { project.shared_runners_enabled }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'root group with shared runners disabled' do
|
context 'when updating root group' do
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
let(:group) { create(:group, :shared_runners_disabled) }
|
||||||
|
let(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
||||||
it 'receives correct method and succeeds' do
|
let!(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
||||||
expect(group).to receive(:update_shared_runners_setting!).with('enabled')
|
|
||||||
|
|
||||||
|
it 'enables shared Runners only for itself' do
|
||||||
|
expect do
|
||||||
expect(subject[:status]).to eq(:success)
|
expect(subject[:status]).to eq(:success)
|
||||||
|
|
||||||
|
reload_models(group, sub_group, project)
|
||||||
|
end.to change { group.shared_runners_enabled }.from(false).to(true)
|
||||||
|
.and not_change { sub_group.shared_runners_enabled }.from(false)
|
||||||
|
.and not_change { project.shared_runners_enabled }.from(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -75,7 +88,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
||||||
let(:params) { { shared_runners_setting: 'invalid_enabled' } }
|
let(:params) { { shared_runners_setting: 'invalid_enabled' } }
|
||||||
|
|
||||||
it 'does not update pending builds for the group' do
|
it 'does not update pending builds for the group' do
|
||||||
expect(::Ci::UpdatePendingBuildService).not_to receive(:new).and_call_original
|
expect(::Ci::UpdatePendingBuildService).not_to receive(:new)
|
||||||
|
|
||||||
subject
|
subject
|
||||||
|
|
||||||
|
|
@ -87,20 +100,46 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'disable shared Runners' do
|
context 'disable shared Runners' do
|
||||||
let_it_be(:group) { create(:group) }
|
let!(:group) { create(:group) }
|
||||||
|
let!(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
|
||||||
|
let!(:sub_group2) { create(:group, parent: group) }
|
||||||
|
let!(:project) { create(:project, group: group, shared_runners_enabled: true) }
|
||||||
|
let!(:project2) { create(:project, group: sub_group2, shared_runners_enabled: true) }
|
||||||
|
|
||||||
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE } }
|
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE } }
|
||||||
|
|
||||||
it 'receives correct method and succeeds' do
|
it 'disables shared Runners for all descendant groups and projects' do
|
||||||
expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_AND_UNOVERRIDABLE)
|
expect do
|
||||||
|
|
||||||
expect(subject[:status]).to eq(:success)
|
expect(subject[:status]).to eq(:success)
|
||||||
|
|
||||||
|
reload_models(group, sub_group, sub_group2, project, project2)
|
||||||
|
end.to change { group.shared_runners_enabled }.from(true).to(false)
|
||||||
|
.and not_change { group.allow_descendants_override_disabled_shared_runners }
|
||||||
|
.and not_change { sub_group.shared_runners_enabled }
|
||||||
|
.and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
||||||
|
.and change { sub_group2.shared_runners_enabled }.from(true).to(false)
|
||||||
|
.and not_change { sub_group2.allow_descendants_override_disabled_shared_runners }
|
||||||
|
.and change { project.shared_runners_enabled }.from(true).to(false)
|
||||||
|
.and change { project2.shared_runners_enabled }.from(true).to(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with override on self' do
|
||||||
|
let(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
||||||
|
|
||||||
|
it 'disables it' do
|
||||||
|
expect do
|
||||||
|
expect(subject[:status]).to eq(:success)
|
||||||
|
|
||||||
|
group.reload
|
||||||
|
end
|
||||||
|
.to not_change { group.shared_runners_enabled }
|
||||||
|
.and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when group has pending builds' do
|
context 'when group has pending builds' do
|
||||||
let_it_be(:project) { create(:project, namespace: group) }
|
let!(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
|
||||||
let_it_be(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
|
let!(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
|
||||||
let_it_be(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
|
|
||||||
|
|
||||||
it 'updates pending builds for the group' do
|
it 'updates pending builds for the group' do
|
||||||
expect(::Ci::UpdatePendingBuildService).to receive(:new).and_call_original
|
expect(::Ci::UpdatePendingBuildService).to receive(:new).and_call_original
|
||||||
|
|
@ -113,52 +152,90 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'allow descendants to override' do
|
shared_examples 'allow descendants to override' do
|
||||||
|
context 'top level group' do
|
||||||
|
let!(:group) { create(:group, :shared_runners_disabled) }
|
||||||
|
let!(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
||||||
|
let!(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
||||||
|
|
||||||
|
it 'enables allow descendants to override only for itself' do
|
||||||
|
expect do
|
||||||
|
expect(subject[:status]).to eq(:success)
|
||||||
|
|
||||||
|
reload_models(group, sub_group, project)
|
||||||
|
end.to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
||||||
|
.and not_change { group.shared_runners_enabled }
|
||||||
|
.and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
|
||||||
|
.and not_change { sub_group.shared_runners_enabled }
|
||||||
|
.and not_change { project.shared_runners_enabled }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ancestor disables shared Runners but allows to override' do
|
||||||
|
let!(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
||||||
|
let!(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
||||||
|
let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
||||||
|
|
||||||
|
it 'enables allow descendants to override' do
|
||||||
|
expect do
|
||||||
|
expect(subject[:status]).to eq(:success)
|
||||||
|
|
||||||
|
reload_models(parent, group, project)
|
||||||
|
end
|
||||||
|
.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
||||||
|
.and not_change { parent.shared_runners_enabled }
|
||||||
|
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
||||||
|
.and not_change { group.shared_runners_enabled }
|
||||||
|
.and not_change { project.shared_runners_enabled }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ancestor disables shared runners' do
|
||||||
|
let(:parent) { create(:group, :shared_runners_disabled) }
|
||||||
|
let(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
||||||
|
let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
||||||
|
|
||||||
|
it 'returns an error and does not enable shared runners' do
|
||||||
|
expect do
|
||||||
|
expect(subject[:status]).to eq(:error)
|
||||||
|
expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
|
||||||
|
|
||||||
|
reload_models(parent, group, project)
|
||||||
|
end.to not_change { parent.shared_runners_enabled }
|
||||||
|
.and not_change { group.shared_runners_enabled }
|
||||||
|
.and not_change { project.shared_runners_enabled }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'top level group that has shared Runners enabled' do
|
||||||
|
let!(:group) { create(:group, shared_runners_enabled: true) }
|
||||||
|
let!(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
|
||||||
|
let!(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
|
||||||
|
|
||||||
|
it 'enables allow descendants to override & disables shared runners everywhere' do
|
||||||
|
expect do
|
||||||
|
expect(subject[:status]).to eq(:success)
|
||||||
|
|
||||||
|
reload_models(group, sub_group, project)
|
||||||
|
end
|
||||||
|
.to change { group.shared_runners_enabled }.from(true).to(false)
|
||||||
|
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
||||||
|
.and change { sub_group.shared_runners_enabled }.from(true).to(false)
|
||||||
|
.and change { project.shared_runners_enabled }.from(true).to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when using SR_DISABLED_AND_OVERRIDABLE" do
|
||||||
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_OVERRIDABLE } }
|
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_OVERRIDABLE } }
|
||||||
|
|
||||||
context 'top level group' do
|
include_examples 'allow descendants to override'
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
||||||
|
|
||||||
it 'receives correct method and succeeds' do
|
|
||||||
expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_AND_OVERRIDABLE)
|
|
||||||
|
|
||||||
expect(subject[:status]).to eq(:success)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when parent does not allow' do
|
context "when using SR_DISABLED_WITH_OVERRIDE" do
|
||||||
let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
|
|
||||||
|
|
||||||
it 'results error' do
|
|
||||||
expect(subject[:status]).to eq(:error)
|
|
||||||
expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when using DISABLED_WITH_OVERRIDE (deprecated)' do
|
|
||||||
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } }
|
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } }
|
||||||
|
|
||||||
context 'top level group' do
|
include_examples 'allow descendants to override'
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
||||||
|
|
||||||
it 'receives correct method and succeeds' do
|
|
||||||
expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_WITH_OVERRIDE)
|
|
||||||
|
|
||||||
expect(subject[:status]).to eq(:success)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when parent does not allow' do
|
|
||||||
let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
|
|
||||||
let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
|
|
||||||
|
|
||||||
it 'results error' do
|
|
||||||
expect(subject[:status]).to eq(:error)
|
|
||||||
expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1030,17 +1030,17 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
group.update_shared_runners_setting!(shared_runners_setting)
|
group.update!(shared_runners_enabled: shared_runners_enabled,
|
||||||
|
allow_descendants_override_disabled_shared_runners: allow_to_override)
|
||||||
|
|
||||||
user.refresh_authorized_projects # Ensure cache is warm
|
user.refresh_authorized_projects # Ensure cache is warm
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'default value based on parent group setting' do
|
context 'default value based on parent group setting' do
|
||||||
where(:shared_runners_setting, :desired_config_for_new_project, :expected_result_for_project) do
|
where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project, :expected_result_for_project) do
|
||||||
Namespace::SR_ENABLED | nil | true
|
true | false | nil | true
|
||||||
Namespace::SR_DISABLED_WITH_OVERRIDE | nil | false
|
false | true | nil | false
|
||||||
Namespace::SR_DISABLED_AND_OVERRIDABLE | nil | false
|
false | false | nil | false
|
||||||
Namespace::SR_DISABLED_AND_UNOVERRIDABLE | nil | false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
|
@ -1057,14 +1057,12 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'parent group is present and allows desired config' do
|
context 'parent group is present and allows desired config' do
|
||||||
where(:shared_runners_setting, :desired_config_for_new_project, :expected_result_for_project) do
|
where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project, :expected_result_for_project) do
|
||||||
Namespace::SR_ENABLED | true | true
|
true | false | true | true
|
||||||
Namespace::SR_ENABLED | false | false
|
true | false | false | false
|
||||||
Namespace::SR_DISABLED_WITH_OVERRIDE | false | false
|
false | true | false | false
|
||||||
Namespace::SR_DISABLED_WITH_OVERRIDE | true | true
|
false | true | true | true
|
||||||
Namespace::SR_DISABLED_AND_OVERRIDABLE | false | false
|
false | false | false | false
|
||||||
Namespace::SR_DISABLED_AND_OVERRIDABLE | true | true
|
|
||||||
Namespace::SR_DISABLED_AND_UNOVERRIDABLE | false | false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
|
@ -1080,8 +1078,8 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'parent group is present and disallows desired config' do
|
context 'parent group is present and disallows desired config' do
|
||||||
where(:shared_runners_setting, :desired_config_for_new_project) do
|
where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project) do
|
||||||
Namespace::SR_DISABLED_AND_UNOVERRIDABLE | true
|
false | false | true
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ module DbCleaner
|
||||||
AND pid <> pg_backend_pid();
|
AND pid <> pg_backend_pid();
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection|
|
Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection|
|
||||||
connection.execute(cmd)
|
connection.execute(cmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ module Database
|
||||||
def execute_on_each_database(query, databases: %I[main ci])
|
def execute_on_each_database(query, databases: %I[main ci])
|
||||||
databases = databases.select { |database_name| database_exists?(database_name) }
|
databases = databases.select { |database_name| database_exists?(database_name) }
|
||||||
|
|
||||||
Gitlab::Database::EachDatabase.each_database_connection(only: databases, include_shared: false) do |connection, _|
|
Gitlab::Database::EachDatabase.each_connection(only: databases, include_shared: false) do |connection, _|
|
||||||
next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_shared)
|
next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_shared)
|
||||||
|
|
||||||
connection.execute(query)
|
connection.execute(query)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,4 @@ module ReloadHelpers
|
||||||
def reload_models(*models)
|
def reload_models(*models)
|
||||||
models.compact.map(&:reload)
|
models.compact.map(&:reload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def subject_and_reload(...)
|
|
||||||
subject
|
|
||||||
reload_models(...)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ RSpec.describe 'dev rake tasks' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def expect_connections_to_be_terminated
|
def expect_connections_to_be_terminated
|
||||||
expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection)
|
expect(Gitlab::Database::EachDatabase).to receive(:each_connection)
|
||||||
.with(include_shared: false)
|
.with(include_shared: false)
|
||||||
.and_call_original
|
.and_call_original
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1109,7 +1109,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
|
||||||
before do
|
before do
|
||||||
each_database = class_double('Gitlab::Database::EachDatabase').as_stubbed_const
|
each_database = class_double('Gitlab::Database::EachDatabase').as_stubbed_const
|
||||||
|
|
||||||
allow(each_database).to receive(:each_database_connection)
|
allow(each_database).to receive(:each_connection)
|
||||||
.and_yield(connections[:main], 'main')
|
.and_yield(connections[:main], 'main')
|
||||||
.and_yield(connections[:ci], 'ci')
|
.and_yield(connections[:ci], 'ci')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,21 @@ RSpec.describe PipelineScheduleWorker, :sidekiq_inline, feature_category: :conti
|
||||||
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
|
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not creates a new pipeline' do
|
it 'creates a new pipeline' do
|
||||||
|
expect { subject }.to change { project.ci_pipelines.count }.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag persist_failed_pipelines_from_schedules disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(persist_failed_pipelines_from_schedules: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a new pipeline' do
|
||||||
expect { subject }.not_to change { project.ci_pipelines.count }
|
expect { subject }.not_to change { project.ci_pipelines.count }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the schedule is not runnable by the user' do
|
context 'when the schedule is not runnable by the user' do
|
||||||
it 'does not deactivate the schedule' do
|
it 'does not deactivate the schedule' do
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
|
||||||
before do
|
before do
|
||||||
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
|
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
|
||||||
|
|
||||||
expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
|
expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: pipeline_schedule).and_return(service_response)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when pipeline is persisted" do
|
context "when pipeline is persisted" do
|
||||||
|
|
@ -124,7 +124,26 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
|
||||||
|
|
||||||
it 'creates a pipeline' do
|
it 'creates a pipeline' do
|
||||||
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
|
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
|
||||||
expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
|
expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: pipeline_schedule).and_return(service_response)
|
||||||
|
|
||||||
|
worker.perform(pipeline_schedule.id, user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag persist_failed_pipelines_from_schedules disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(persist_failed_pipelines_from_schedules: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not save_on_errors' do
|
||||||
|
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
|
||||||
|
|
||||||
|
expect(create_pipeline_service).to receive(:execute).with(
|
||||||
|
:schedule,
|
||||||
|
ignore_skip_ci: true,
|
||||||
|
save_on_errors: false,
|
||||||
|
schedule: pipeline_schedule
|
||||||
|
)
|
||||||
|
|
||||||
worker.perform(pipeline_schedule.id, user.id)
|
worker.perform(pipeline_schedule.id, user.id)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue