Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-08 15:10:00 +00:00
parent ccc2dc45a3
commit 0ebbf19f2d
198 changed files with 1226 additions and 1773 deletions

View File

@ -742,11 +742,6 @@ Style/ExplicitBlockArgument:
Style/FormatString: Style/FormatString:
Enabled: false Enabled: false
# Offense count: 67
# Cop supports --auto-correct.
Style/GlobalStdStream:
Enabled: false
# Offense count: 897 # Offense count: 897
# Configuration parameters: MinBodyLength. # Configuration parameters: MinBodyLength.
Style/GuardClause: Style/GuardClause:

View File

@ -1 +1 @@
8fd337f0f718f257ae72a66c464143a395af4c05 df2eb006d241b399b8b6b877afab97713bb5c36a

View File

@ -30,6 +30,24 @@ let renderedMermaidBlocks = 0;
let mermaidModule = {}; let mermaidModule = {};
// Whitelist pages where we won't impose any restrictions
// on mermaid rendering
const WHITELISTED_PAGES = [
// Group wiki
'groups:wikis:show',
'groups:wikis:edit',
'groups:wikis:create',
// Project wiki
'projects:wikis:show',
'projects:wikis:edit',
'projects:wikis:create',
// Project files
'projects:show',
'projects:blob:show',
];
export function initMermaid(mermaid) { export function initMermaid(mermaid) {
let theme = 'neutral'; let theme = 'neutral';
@ -120,8 +138,10 @@ function renderMermaidEl(el) {
function renderMermaids($els) { function renderMermaids($els) {
if (!$els.length) return; if (!$els.length) return;
const pageName = document.querySelector('body').dataset.page;
// A diagram may have been truncated in search results which will cause errors, so abort the render. // A diagram may have been truncated in search results which will cause errors, so abort the render.
if (document.querySelector('body').dataset.page === 'search:show') return; if (pageName === 'search:show') return;
importMermaidModule() importMermaidModule()
.then(() => { .then(() => {
@ -140,10 +160,11 @@ function renderMermaids($els) {
* up the entire thread and causing a DoS. * up the entire thread and causing a DoS.
*/ */
if ( if (
(source && source.length > MAX_CHAR_LIMIT) || !WHITELISTED_PAGES.includes(pageName) &&
((source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT || renderedChars > MAX_CHAR_LIMIT ||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT || renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
shouldLazyLoadMermaidBlock(source) shouldLazyLoadMermaidBlock(source))
) { ) {
const html = ` const html = `
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert"> <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">

View File

@ -1,6 +1,7 @@
<script> <script>
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
@ -224,9 +225,6 @@ export default {
}, },
methods: { methods: {
...mapActions(['setError', 'unsetError']), ...mapActions(['setError', 'unsetError']),
setIteration(iterationId) {
this.board.iteration_id = iterationId;
},
boardCreateResponse(data) { boardCreateResponse(data) {
return data.createBoard.board.webPath; return data.createBoard.board.webPath;
}, },
@ -237,6 +235,9 @@ export default {
: ''; : '';
return `${path}${param}`; return `${path}${param}`;
}, },
cancel() {
this.$emit('cancel');
},
async createOrUpdateBoard() { async createOrUpdateBoard() {
const response = await this.$apollo.mutate({ const response = await this.$apollo.mutate({
mutation: this.currentMutation, mutation: this.currentMutation,
@ -280,9 +281,6 @@ export default {
} }
} }
}, },
cancel() {
this.$emit('cancel');
},
resetFormState() { resetFormState() {
if (this.isNewForm) { if (this.isNewForm) {
// Clear the form when we open the "New board" modal // Clear the form when we open the "New board" modal
@ -291,6 +289,25 @@ export default {
this.board = { ...boardDefaults, ...this.currentBoard }; this.board = { ...boardDefaults, ...this.currentBoard };
} }
}, },
setIteration(iterationId) {
this.board.iteration_id = iterationId;
},
setBoardLabels(labels) {
labels.forEach((label) => {
if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
this.board.labels.push(
new ListLabel({
id: label.id,
title: label.title,
color: label.color,
textColor: label.text_color,
}),
);
} else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
}
});
},
}, },
}; };
</script> </script>
@ -357,6 +374,7 @@ export default {
:group-id="groupId" :group-id="groupId"
:weights="weights" :weights="weights"
@set-iteration="setIteration" @set-iteration="setIteration"
@set-board-labels="setBoardLabels"
/> />
</form> </form>
</gl-modal> </gl-modal>

View File

@ -1,6 +1,7 @@
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import showToast from '~/vue_shared/plugins/global_toast'; import showToast from '~/vue_shared/plugins/global_toast';
import { import {
@ -16,7 +17,9 @@ export const fetchAwards = async ({ commit, dispatch, state }, page = '1') => {
if (!window.gon?.current_user_id) return; if (!window.gon?.current_user_id) return;
try { try {
const { data, headers } = await axios.get(state.path, { params: { per_page: 100, page } }); const { data, headers } = await axios.get(joinPaths(gon.relative_url_root || '', state.path), {
params: { per_page: 100, page },
});
const normalizedHeaders = normalizeHeaders(headers); const normalizedHeaders = normalizeHeaders(headers);
const nextPage = normalizedHeaders['X-NEXT-PAGE']; const nextPage = normalizedHeaders['X-NEXT-PAGE'];
@ -35,13 +38,15 @@ export const toggleAward = async ({ commit, state }, name) => {
try { try {
if (award) { if (award) {
await axios.delete(`${state.path}/${award.id}`); await axios.delete(joinPaths(gon.relative_url_root || '', `${state.path}/${award.id}`));
commit(REMOVE_AWARD, award.id); commit(REMOVE_AWARD, award.id);
showToast(__('Award removed')); showToast(__('Award removed'));
} else { } else {
const { data } = await axios.post(state.path, { name }); const { data } = await axios.post(joinPaths(gon.relative_url_root || '', state.path), {
name,
});
commit(ADD_NEW_AWARD, data); commit(ADD_NEW_AWARD, data);

View File

@ -1,191 +0,0 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import LabelsSelect from '~/labels_select';
import { __ } from '~/locale';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { DropdownVariant } from '../labels_select_vue/constants';
import DropdownButton from './dropdown_button.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
export default {
DropdownVariant,
components: {
DropdownTitle,
DropdownValue,
DropdownValueCollapsed,
DropdownButton,
DropdownHiddenInput,
DropdownHeader,
DropdownSearchInput,
DropdownFooter,
DropdownCreateLabel,
GlLoadingIcon,
},
props: {
showCreate: {
type: Boolean,
required: false,
default: false,
},
isProject: {
type: Boolean,
required: false,
default: false,
},
abilityName: {
type: String,
required: true,
},
context: {
type: Object,
required: true,
},
namespace: {
type: String,
required: false,
default: '',
},
updatePath: {
type: String,
required: false,
default: '',
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: false,
default: '',
},
labelFilterBasePath: {
type: String,
required: false,
default: '',
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
variant: {
type: String,
required: false,
default: DropdownVariant.Sidebar,
},
},
computed: {
hiddenInputName() {
return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
},
createLabelTitle() {
if (this.isProject) {
return __('Create project label');
}
return __('Create group label');
},
manageLabelsTitle() {
if (this.isProject) {
return __('Manage project labels');
}
return __('Manage group labels');
},
},
mounted() {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
},
};
</script>
<template>
<div class="block labels js-labels-block">
<dropdown-value-collapsed
v-if="showCreate && variant === $options.DropdownVariant.Sidebar"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title :can-edit="canEdit" />
<dropdown-value
:labels="context.labels"
:label-filter-base-path="labelFilterBasePath"
:enable-scoped-labels="enableScopedLabels"
>
<slot></slot>
</dropdown-value>
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none">
<dropdown-hidden-input
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
:value="label.id"
/>
<div ref="dropdown" class="dropdown">
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
:update-path="updatePath"
:labels-path="labelsPath"
:namespace="namespace"
:labels="context.labels"
:show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar"
:enable-scoped-labels="enableScopedLabels"
/>
<div
class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-page-one">
<dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" />
<dropdown-search-input />
<div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div>
<div class="dropdown-loading">
<gl-loading-icon
class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full"
/>
</div>
<dropdown-footer
v-if="showCreate"
:labels-web-url="labelsWebUrl"
:create-label-title="createLabelTitle"
:manage-labels-title="manageLabelsTitle"
/>
</div>
<dropdown-create-label
v-if="showCreate"
:is-project="isProject"
:header-title="createLabelTitle"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,86 +0,0 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
abilityName: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
updatePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
namespace: {
type: String,
required: true,
},
labels: {
type: Array,
required: true,
},
showExtraOptions: {
type: Boolean,
required: true,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
dropdownToggleText() {
if (this.labels.length === 0) {
return __('Label');
}
if (this.labels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.labels[0].title,
remainingLabelCount: this.labels.length - 1,
});
}
return this.labels[0].title;
},
},
};
</script>
<template>
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
ref="dropdownButton"
:class="{ 'js-extra-options': showExtraOptions }"
:data-ability-name="abilityName"
:data-field-name="fieldName"
:data-issue-update="updatePath"
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
:data-scoped-labels="enableScopedLabels"
type="button"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
data-toggle="dropdown"
>
<span class="dropdown-toggle-text"> {{ dropdownToggleText }} </span>
<gl-icon
name="chevron-down"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
:size="16"
/>
</button>
</template>

View File

@ -1,92 +0,0 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
headerTitle: {
type: String,
required: false,
default: () => __('Create new label'),
},
},
created() {
const rawLabelsColors = gon.suggested_label_colors;
this.suggestedColors = Object.keys(rawLabelsColors).map((colorCode) => ({
colorCode,
title: rawLabelsColors[colorCode],
}));
},
};
</script>
<template>
<div class="dropdown-page-two dropdown-new-label">
<div
class="dropdown-title gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<gl-button
:aria-label="__('Go back')"
category="tertiary"
class="dropdown-menu-back"
icon="arrow-left"
size="small"
/>
{{ headerTitle }}
<gl-button
:aria-label="__('Close')"
category="tertiary"
class="dropdown-menu-close"
icon="close"
size="small"
/>
</div>
<div class="dropdown-content">
<div class="dropdown-labels-error js-label-error"></div>
<input
id="new_label_name"
:placeholder="__('Name new label')"
type="text"
class="default-dropdown-input"
/>
<div class="suggest-colors suggest-colors-dropdown">
<a
v-for="(color, index) in suggestedColors"
:key="index"
v-gl-tooltip
:data-color="color.colorCode"
:style="{
backgroundColor: color.colorCode,
}"
:title="color.title"
href="#"
>
&nbsp;
</a>
</div>
<div class="dropdown-label-color-input">
<div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
<input
id="new_label_color"
:placeholder="__('Assign custom color like #FF0000')"
type="text"
class="default-dropdown-input"
/>
</div>
<div class="clearfix">
<gl-button category="secondary" class="float-left js-new-label-btn disabled">
{{ __('Create') }}
</gl-button>
<gl-button category="secondary" class="float-right js-cancel-label-btn">
{{ __('Cancel') }}
</gl-button>
</div>
</div>
</div>
</template>

View File

@ -1,37 +0,0 @@
<script>
import { __ } from '~/locale';
export default {
props: {
labelsWebUrl: {
type: String,
required: true,
},
createLabelTitle: {
type: String,
required: false,
default: () => __('Create new label'),
},
manageLabelsTitle: {
type: String,
required: false,
default: () => __('Manage labels'),
},
},
};
</script>
<template>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<a href="#" class="dropdown-toggle-page"> {{ createLabelTitle }} </a>
</li>
<li>
<a :href="labelsWebUrl" data-is-link="true" class="dropdown-external-link">
{{ manageLabelsTitle }}
</a>
</li>
</ul>
</div>
</template>

View File

@ -1,22 +0,0 @@
<script>
import { GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
},
};
</script>
<template>
<div class="dropdown-title gl-display-flex gl-justify-content-center">
<span class="gl-ml-auto">{{ __('Assign labels') }}</span>
<button
:aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close gl-ml-auto"
>
<gl-icon name="close" class="dropdown-menu-close-icon" />
</button>
</div>
</template>

View File

@ -1,28 +0,0 @@
<script>
import { GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
},
};
</script>
<template>
<div class="dropdown-input">
<input
:placeholder="__('Search')"
autocomplete="off"
class="dropdown-input-field"
type="search"
/>
<gl-icon
name="search"
class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none"
/>
<gl-icon
name="close"
class="dropdown-input-clear js-dropdown-input-clear gl-absolute gl-top-3 gl-right-5 gl-text-gray-500"
/>
</div>
</template>

View File

@ -1,31 +0,0 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
},
props: {
canEdit: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div class="title hide-collapsed gl-mb-3">
{{ __('Labels') }}
<template v-if="canEdit">
<gl-loading-icon inline class="align-text-top block-loading" />
<button
type="button"
class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle"
data-qa-selector="labels_edit_button"
>
{{ __('Edit') }}
</button>
</template>
</div>
</template>

View File

@ -1,65 +0,0 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
GlLabel,
},
props: {
labels: {
type: Array,
required: true,
},
labelFilterBasePath: {
type: String,
required: true,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isEmpty() {
return this.labels.length === 0;
},
},
methods: {
labelFilterUrl(label) {
return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
},
scopedLabelsDescription({ description = '' }) {
return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`;
},
showScopedLabels(label) {
return this.enableScopedLabels && isScopedLabel(label);
},
},
};
</script>
<template>
<div
:class="{
'has-labels': !isEmpty,
}"
class="hide-collapsed value issuable-show-labels js-value"
>
<span v-if="isEmpty" class="text-secondary">
<slot>{{ __('None') }}</slot>
</span>
<template v-for="label in labels" v-else>
<gl-label
:key="label.id"
:target="labelFilterUrl(label)"
:background-color="label.color"
:title="label.title"
:description="label.description"
:scoped="showScopedLabels(label)"
/>
</template>
</div>
</template>

View File

@ -29,7 +29,7 @@ export default {
<gl-loading-icon v-show="labelsSelectInProgress" inline /> <gl-loading-icon v-show="labelsSelectInProgress" inline />
<gl-button <gl-button
variant="link" variant="link"
class="float-right js-sidebar-dropdown-toggle" class="gl-text-gray-800! float-right js-sidebar-dropdown-toggle"
data-qa-selector="labels_edit_button" data-qa-selector="labels_edit_button"
@click="toggleDropdownContents" @click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button >{{ __('Edit') }}</gl-button

View File

@ -5,13 +5,12 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import { DropdownVariant } from './constants'; import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue'; import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import labelsSelectModule from './store'; import labelsSelectModule from './store';
Vue.use(Vuex); Vue.use(Vuex);
@ -61,6 +60,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
hideCollapsedView: {
type: Boolean,
required: false,
default: false,
},
labelsSelectInProgress: { labelsSelectInProgress: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -294,6 +298,7 @@ export default {
> >
<template v-if="isDropdownVariantSidebar"> <template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="!hideCollapsedView"
ref="dropdownButtonCollapsed" ref="dropdownButtonCollapsed"
:labels="selectedLabels" :labels="selectedLabels"
@onValueClick="handleCollapsedValueClick" @onValueClick="handleCollapsedValueClick"

View File

@ -5,7 +5,7 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
import { DropdownVariant } from './constants'; import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue'; import DropdownButton from './dropdown_button.vue';

View File

@ -2,7 +2,7 @@
module Resolvers module Resolvers
class BoardListIssuesResolver < BaseResolver class BoardListIssuesResolver < BaseResolver
include BoardIssueFilterable include BoardItemFilterable
argument :filters, Types::Boards::BoardIssueInputType, argument :filters, Types::Boards::BoardIssueInputType,
required: false, required: false,
@ -13,7 +13,7 @@ module Resolvers
alias_method :list, :object alias_method :list, :object
def resolve(**args) def resolve(**args)
filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id) filter_params = item_filters(args[:filters]).merge(board_id: list.board.id, id: list.id)
service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params) service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
offset_pagination(service.execute) offset_pagination(service.execute)

View File

@ -2,7 +2,7 @@
module Resolvers module Resolvers
class BoardListsResolver < BaseResolver class BoardListsResolver < BaseResolver
include BoardIssueFilterable include BoardItemFilterable
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead include LooksAhead
@ -22,7 +22,7 @@ module Resolvers
def resolve_with_lookahead(id: nil, issue_filters: {}) def resolve_with_lookahead(id: nil, issue_filters: {})
lists = board_lists(id) lists = board_lists(id)
context.scoped_set!(:issue_filters, issue_filters(issue_filters)) context.scoped_set!(:issue_filters, item_filters(issue_filters))
List.preload_preferences_for_user(lists, current_user) if load_preferences? List.preload_preferences_for_user(lists, current_user) if load_preferences?

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module BoardIssueFilterable module BoardItemFilterable
extend ActiveSupport::Concern extend ActiveSupport::Concern
private private
def issue_filters(args) def item_filters(args)
filters = args.to_h filters = args.to_h
set_filter_values(filters) set_filter_values(filters)
@ -32,4 +32,4 @@ module BoardIssueFilterable
end end
end end
::BoardIssueFilterable.prepend_mod_with('Resolvers::BoardIssueFilterable') ::BoardItemFilterable.prepend_mod_with('Resolvers::BoardItemFilterable')

View File

@ -7,6 +7,9 @@ module Ci
include Ci::HasStatus include Ci::HasStatus
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
include Presentable include Presentable
include IgnorableColumns
ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
enum status: Ci::HasStatus::STATUSES_ENUM enum status: Ci::HasStatus::STATUSES_ENUM

View File

@ -11,8 +11,8 @@ module IssueAvailableFeatures
def available_features_for_issue_types def available_features_for_issue_types
{ {
assignee: %w(issue incident), assignee: %w(issue incident),
confidentiality: %(issue incident), confidentiality: %w(issue incident),
time_tracking: %(issue incident) time_tracking: %w(issue incident)
}.with_indifferent_access }.with_indifferent_access
end end
end end

View File

@ -31,8 +31,12 @@ class ForkNamespaceEntity < Grape::Entity
end end
expose :can_create_project do |namespace, options| expose :can_create_project do |namespace, options|
if Feature.enabled?(:fork_project_form, options[:project], default_enabled: :yaml)
true
else
options[:current_user].can?(:create_projects, namespace) options[:current_user].can?(:create_projects, namespace)
end end
end
private private

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
module AuthorizedProjectUpdate
class RecalculateForUserRangeService
def initialize(start_user_id, end_user_id)
@start_user_id = start_user_id
@end_user_id = end_user_id
end
def execute
User.where(id: start_user_id..end_user_id).select(:id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord
Users::RefreshAuthorizedProjectsService.new(user, source: self.class.name).execute
end
end
private
attr_reader :start_user_id, :end_user_id
end
end

View File

@ -46,6 +46,8 @@
= form_tag profile_two_factor_auth_path, method: :post do |f| = form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error - if @error
.gl-alert.gl-alert-danger.gl-mb-5 .gl-alert.gl-alert-danger.gl-mb-5
.gl-alert-container
.gl-alert-content
= @error = @error
.form-group .form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold" = label_tag :pin_code, _('Pin code'), class: "label-bold"

View File

@ -8,7 +8,9 @@
= render "projects/merge_requests/mr_box" = render "projects/merge_requests/mr_box"
.gl-alert.gl-alert-danger .gl-alert.gl-alert-danger
.gl-alert-container
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-content{ role: 'alert' }
%p %p
We cannot render this merge request properly because We cannot render this merge request properly because
- if @merge_request.for_fork? && !@merge_request.source_project - if @merge_request.for_fork? && !@merge_request.source_project

View File

@ -2,10 +2,9 @@
module AuthorizedProjectUpdate module AuthorizedProjectUpdate
class UserRefreshOverUserRangeWorker # rubocop:disable Scalability/IdempotentWorker class UserRefreshOverUserRangeWorker # rubocop:disable Scalability/IdempotentWorker
# When the feature flag named `periodic_project_authorization_update_via_replica` is enabled, # This worker checks if users requires an update to their project_authorizations records.
# this worker checks if a specific user requires an update to their project_authorizations records.
# This check is done via the data read from the database replica (and not from the primary). # This check is done via the data read from the database replica (and not from the primary).
# If this check returns true, a completely new Sidekiq job is enqueued for this specific user # If this check returns true, a completely new Sidekiq job is enqueued for a specific user
# so as to update its project_authorizations records. # so as to update its project_authorizations records.
# There is a possibility that the data in the replica is lagging behind the primary # There is a possibility that the data in the replica is lagging behind the primary
@ -24,27 +23,16 @@ module AuthorizedProjectUpdate
# `data_consistency :delayed` and not `idempotent!` # `data_consistency :delayed` and not `idempotent!`
# See https://gitlab.com/gitlab-org/gitlab/-/issues/325291 # See https://gitlab.com/gitlab-org/gitlab/-/issues/325291
deduplicate :until_executing, including_scheduled: true deduplicate :until_executing, including_scheduled: true
data_consistency :delayed, feature_flag: :delayed_consistency_for_user_refresh_over_range_worker data_consistency :delayed
def perform(start_user_id, end_user_id) def perform(start_user_id, end_user_id)
if Feature.enabled?(:periodic_project_authorization_update_via_replica)
User.where(id: start_user_id..end_user_id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord User.where(id: start_user_id..end_user_id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord
enqueue_project_authorizations_refresh(user) if project_authorizations_needs_refresh?(user) enqueue_project_authorizations_refresh(user) if project_authorizations_needs_refresh?(user)
end end
else
use_primary_database
AuthorizedProjectUpdate::RecalculateForUserRangeService.new(start_user_id, end_user_id).execute
end
end end
private private
def use_primary_database
if ::Gitlab::Database::LoadBalancing.enable?
::Gitlab::Database::LoadBalancing::Session.current.use_primary!
end
end
def project_authorizations_needs_refresh?(user) def project_authorizations_needs_refresh?(user)
AuthorizedProjectUpdate::FindRecordsDueForRefreshService.new(user).needs_refresh? AuthorizedProjectUpdate::FindRecordsDueForRefreshService.new(user).needs_refresh?
end end

View File

@ -1,8 +0,0 @@
---
name: delayed_consistency_for_user_refresh_over_range_worker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61883
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327092
milestone: '13.12'
type: development
group: group::access
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: periodic_project_authorization_update_via_replica
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58752
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327092
milestone: '13.11'
type: development
group: group::access
default_enabled: false

View File

@ -1,8 +1,8 @@
--- ---
name: honor_escaped_markdown name: remove_release_notes_from_tags_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45922 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63392
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300531 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290311
milestone: '13.9' milestone: '14.0'
type: development type: development
group: 'group::project management' group: group::release
default_enabled: false default_enabled: false

View File

@ -1,9 +1,9 @@
--- ---
key_path: reply_by_email_enabled key_path: reply_by_email_enabled
description: Whether incoming email is setup description: Whether incoming email is setup
product_section: growth product_section: dev
product_stage: growth product_stage: plan
product_group: group::product intelligence product_group: group::certify
product_category: collection product_category: collection
value_type: boolean value_type: boolean
status: data_available status: data_available
@ -11,6 +11,8 @@ time_frame: none
data_source: system data_source: system
distribution: distribution:
- ce - ce
- ee
tier: tier:
- free - free
skip_validation: true - premium
- ultimate

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class InitializeConversionOfGeoJobArtifactDeletedEventsToBigint < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
TABLE = :geo_job_artifact_deleted_events
COLUMNS = %i(job_artifact_id)
def up
initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
def down
revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class BackfillGeoJobArtifactDeletedEventsForBigintConversion < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
TABLE = :geo_job_artifact_deleted_events
COLUMNS = %i(job_artifact_id)
def up
backfill_conversion_of_integer_to_bigint TABLE, COLUMNS
end
def down
revert_backfill_conversion_of_integer_to_bigint TABLE, COLUMNS
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class InitializeConversionOfCiStagesToBigint < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
TABLE = :ci_stages
COLUMNS = %i(id)
def up
initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
def down
revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class BackfillCiStagesForBigintConversion < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
TABLE = :ci_stages
COLUMNS = %i(id)
def up
backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
def down
revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class ScheduleDisableExpirationPoliciesLinkedToNoContainerImages < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 30_000
DELAY = 2.minutes.freeze
DOWNTIME = false
MIGRATION = 'DisableExpirationPoliciesLinkedToNoContainerImages'
disable_ddl_transaction!
def up
queue_background_migration_jobs_by_range_at_intervals(
define_batchable_model('container_expiration_policies').where(enabled: true),
MIGRATION,
DELAY,
batch_size: BATCH_SIZE,
track_jobs: false,
primary_column_name: :project_id
)
end
def down
# this migration is irreversible
# we can't accuretaly know which policies were previously enabled during the background migration
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class BackfillDraftStatusOnMergeRequests < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = "tmp_index_merge_requests_draft_and_status"
disable_ddl_transaction!
def up
add_concurrent_index :merge_requests, :id,
where: "draft = false AND state_id = 1 AND ((title)::text ~* '^\\[draft\\]|\\(draft\\)|draft:|draft|\\[WIP\\]|WIP:|WIP'::text)",
name: INDEX_NAME
update_column_in_batches(:merge_requests, :draft, true, batch_size: 100) do |table, query|
query
.where(table[:state_id].eq(1))
.where(table[:draft].eq(false))
.where(table[:title].matches_regexp('^\\[draft\\]|\\(draft\\)|draft:|draft|\\[WIP\\]|WIP:|WIP', false))
end
remove_concurrent_index_by_name :merge_requests, INDEX_NAME
end
def down
remove_concurrent_index_by_name :merge_requests, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
9eb5e68b0d79863687530ff22cbe6a2bffd2e2d31237e919134b9ce77810b1a0

View File

@ -0,0 +1 @@
6568aa11d3652fb7ee23d2e6622a1038d891914f629438608993ff0d8b46b748

View File

@ -0,0 +1 @@
1a877c384c1e4e9e28a64c8c521aa72965c54d528044b076efdc75aeeb83d796

View File

@ -0,0 +1 @@
f80787d85538cedaba34cb204c98df2d0bbbf85f438d4df8f1187d2f4d881588

View File

@ -0,0 +1 @@
c395f52ee34cd758df87ba0f74f4528a189704498e133fa53f0dd3f6f31a77b3

View File

@ -0,0 +1 @@
9f8ff974adc7c20908cd423b2d3f69d8ec16b0fcbb8bfbdb9347a9ff3f3a007a

View File

@ -125,6 +125,15 @@ BEGIN
END; END;
$$; $$;
CREATE FUNCTION trigger_490d204c00b3() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW."id_convert_to_bigint" := NEW."id";
RETURN NEW;
END;
$$;
CREATE FUNCTION trigger_51ab7cef8934() RETURNS trigger CREATE FUNCTION trigger_51ab7cef8934() RETURNS trigger
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
@ -208,6 +217,15 @@ BEGIN
END; END;
$$; $$;
CREATE FUNCTION trigger_f1ca8ec18d78() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW."job_artifact_id_convert_to_bigint" := NEW."job_artifact_id";
RETURN NEW;
END;
$$;
CREATE TABLE audit_events ( CREATE TABLE audit_events (
id bigint NOT NULL, id bigint NOT NULL,
author_id integer NOT NULL, author_id integer NOT NULL,
@ -11195,6 +11213,7 @@ CREATE TABLE ci_stages (
status integer, status integer,
lock_version integer DEFAULT 0, lock_version integer DEFAULT 0,
"position" integer, "position" integer,
id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
CONSTRAINT check_81b431e49b CHECK ((lock_version IS NOT NULL)) CONSTRAINT check_81b431e49b CHECK ((lock_version IS NOT NULL))
); );
@ -13054,7 +13073,8 @@ ALTER SEQUENCE geo_hashed_storage_migrated_events_id_seq OWNED BY geo_hashed_sto
CREATE TABLE geo_job_artifact_deleted_events ( CREATE TABLE geo_job_artifact_deleted_events (
id bigint NOT NULL, id bigint NOT NULL,
job_artifact_id integer NOT NULL, job_artifact_id integer NOT NULL,
file_path character varying NOT NULL file_path character varying NOT NULL,
job_artifact_id_convert_to_bigint bigint DEFAULT 0 NOT NULL
); );
CREATE SEQUENCE geo_job_artifact_deleted_events_id_seq CREATE SEQUENCE geo_job_artifact_deleted_events_id_seq
@ -25314,6 +25334,8 @@ CREATE TRIGGER trigger_21e7a2602957 BEFORE INSERT OR UPDATE ON ci_build_needs FO
CREATE TRIGGER trigger_3f6129be01d2 BEFORE INSERT OR UPDATE ON ci_builds FOR EACH ROW EXECUTE FUNCTION trigger_3f6129be01d2(); CREATE TRIGGER trigger_3f6129be01d2 BEFORE INSERT OR UPDATE ON ci_builds FOR EACH ROW EXECUTE FUNCTION trigger_3f6129be01d2();
CREATE TRIGGER trigger_490d204c00b3 BEFORE INSERT OR UPDATE ON ci_stages FOR EACH ROW EXECUTE FUNCTION trigger_490d204c00b3();
CREATE TRIGGER trigger_51ab7cef8934 BEFORE INSERT OR UPDATE ON ci_builds_runner_session FOR EACH ROW EXECUTE FUNCTION trigger_51ab7cef8934(); CREATE TRIGGER trigger_51ab7cef8934 BEFORE INSERT OR UPDATE ON ci_builds_runner_session FOR EACH ROW EXECUTE FUNCTION trigger_51ab7cef8934();
CREATE TRIGGER trigger_69523443cc10 BEFORE INSERT OR UPDATE ON events FOR EACH ROW EXECUTE FUNCTION trigger_69523443cc10(); CREATE TRIGGER trigger_69523443cc10 BEFORE INSERT OR UPDATE ON events FOR EACH ROW EXECUTE FUNCTION trigger_69523443cc10();
@ -25332,6 +25354,8 @@ CREATE TRIGGER trigger_be1804f21693 BEFORE INSERT OR UPDATE ON ci_job_artifacts
CREATE TRIGGER trigger_cf2f9e35f002 BEFORE INSERT OR UPDATE ON ci_build_trace_chunks FOR EACH ROW EXECUTE FUNCTION trigger_cf2f9e35f002(); CREATE TRIGGER trigger_cf2f9e35f002 BEFORE INSERT OR UPDATE ON ci_build_trace_chunks FOR EACH ROW EXECUTE FUNCTION trigger_cf2f9e35f002();
CREATE TRIGGER trigger_f1ca8ec18d78 BEFORE INSERT OR UPDATE ON geo_job_artifact_deleted_events FOR EACH ROW EXECUTE FUNCTION trigger_f1ca8ec18d78();
CREATE TRIGGER trigger_has_external_issue_tracker_on_delete AFTER DELETE ON services FOR EACH ROW WHEN ((((old.category)::text = 'issue_tracker'::text) AND (old.active = true) AND (old.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker(); CREATE TRIGGER trigger_has_external_issue_tracker_on_delete AFTER DELETE ON services FOR EACH ROW WHEN ((((old.category)::text = 'issue_tracker'::text) AND (old.active = true) AND (old.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker();
CREATE TRIGGER trigger_has_external_issue_tracker_on_insert AFTER INSERT ON services FOR EACH ROW WHEN ((((new.category)::text = 'issue_tracker'::text) AND (new.active = true) AND (new.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker(); CREATE TRIGGER trigger_has_external_issue_tracker_on_insert AFTER INSERT ON services FOR EACH ROW WHEN ((((new.category)::text = 'issue_tracker'::text) AND (new.active = true) AND (new.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker();

View File

@ -79,7 +79,7 @@ require 'json'
require 'mail' require 'mail'
# The incoming variables are in JSON format so we need to parse it first. # The incoming variables are in JSON format so we need to parse it first.
ARGS = JSON.parse(STDIN.read) ARGS = JSON.parse($stdin.read)
# We only want to trigger this file hook on the event project_create # We only want to trigger this file hook on the event project_create
return unless ARGS['event_name'] == 'project_create' return unless ARGS['event_name'] == 'project_create'

View File

@ -26,7 +26,7 @@ You can enable output of Active Record debug logging in the Rails console
session by running: session by running:
```ruby ```ruby
ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.logger = Logger.new($stdout)
``` ```
This will show information about database queries triggered by any Ruby code This will show information about database queries triggered by any Ruby code

View File

@ -100,7 +100,7 @@ Rails.cache.instance_variable_get(:@data).keys
```ruby ```ruby
# Before 11.6.0 # Before 11.6.0
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
admin_token = User.find_by_username('ADMIN_USERNAME').personal_access_tokens.first.token admin_token = User.find_by_username('ADMIN_USERNAME').personal_access_tokens.first.token
app.get("URL/?private_token=#{admin_token}") app.get("URL/?private_token=#{admin_token}")
@ -113,7 +113,7 @@ Gitlab::Profiler.with_user(admin) { app.get(url) }
## Using the GitLab profiler inside console (used as of 10.5) ## Using the GitLab profiler inside console (used as of 10.5)
```ruby ```ruby
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
admin = User.find_by_username('ADMIN_USERNAME') admin = User.find_by_username('ADMIN_USERNAME')
Gitlab::Profiler.profile('URL', logger: logger, user: admin) Gitlab::Profiler.profile('URL', logger: logger, user: admin)
``` ```

View File

@ -46,7 +46,7 @@ Let's enable debug logging for Active Record so we can see the underlying
database queries made: database queries made:
```ruby ```ruby
ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.logger = Logger.new($stdout)
``` ```
Now, let's try retrieving a user from the database: Now, let's try retrieving a user from the database:

View File

@ -8570,6 +8570,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="epicboardlistsepicfilters"></a>`epicFilters` | [`EpicFilters`](#epicfilters) | Filters applied when getting epic metadata in the epic board list. |
| <a id="epicboardlistsid"></a>`id` | [`BoardsEpicListID`](#boardsepiclistid) | Find an epic board list by ID. | | <a id="epicboardlistsid"></a>`id` | [`BoardsEpicListID`](#boardsepiclistid) | Find an epic board list by ID. |
### `EpicDescendantCount` ### `EpicDescendantCount`

View File

@ -255,12 +255,8 @@ batches instead of doing this one by one:
class ScheduleExtractServicesUrl < ActiveRecord::Migration[4.2] class ScheduleExtractServicesUrl < ActiveRecord::Migration[4.2]
disable_ddl_transaction! disable_ddl_transaction!
class Service < ActiveRecord::Base
self.table_name = 'services'
end
def up def up
Service.select(:id).in_batches do |relation| define_batchable_model('services').select(:id).in_batches do |relation|
jobs = relation.pluck(:id).map do |id| jobs = relation.pluck(:id).map do |id|
['ExtractServicesUrl', [id]] ['ExtractServicesUrl', [id]]
end end
@ -286,18 +282,12 @@ this:
class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration[4.2] class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration[4.2]
disable_ddl_transaction! disable_ddl_transaction!
class Service < ActiveRecord::Base
include ::EachBatch
self.table_name = 'services'
end
def up def up
# This must be included # This must be included
Gitlab::BackgroundMigration.steal('ExtractServicesUrl') Gitlab::BackgroundMigration.steal('ExtractServicesUrl')
# This should be included, but can be skipped - see below # This should be included, but can be skipped - see below
Service.where(url: nil).each_batch(of: 50) do |batch| define_batchable_model('services').where(url: nil).each_batch(of: 50) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first range = batch.pluck('MIN(id)', 'MAX(id)').first
Gitlab::BackgroundMigration::ExtractServicesUrl.new.perform(*range) Gitlab::BackgroundMigration::ExtractServicesUrl.new.perform(*range)

View File

@ -308,12 +308,15 @@ We choose to use GitLab major version upgrades as a safe time to remove
backwards compatibility for indices that have not been fully migrated. We backwards compatibility for indices that have not been fully migrated. We
[document this in our upgrade [document this in our upgrade
documentation](../update/index.md#upgrading-to-a-new-major-version). We also documentation](../update/index.md#upgrading-to-a-new-major-version). We also
choose to remove the migration code and tests so that: choose to replace the migration code with the halted migration
and remove tests so that:
- We don't need to maintain any code that is called from our Advanced Search - We don't need to maintain any code that is called from our Advanced Search
migrations. migrations.
- We don't waste CI time running tests for migrations that we don't support - We don't waste CI time running tests for migrations that we don't support
anymore. anymore.
- Operators who have not run this migration and who upgrade directly to the
target version will see a message prompting them to reindex from scratch.
To be extra safe, we will not delete migrations that were created in the last To be extra safe, we will not delete migrations that were created in the last
minor version before the major upgrade. So, if we are upgrading to `%14.0`, minor version before the major upgrade. So, if we are upgrading to `%14.0`,
@ -334,18 +337,10 @@ For every migration that was created 2 minor versions before the major version
being upgraded to, we do the following: being upgraded to, we do the following:
1. Confirm the migration has actually completed successfully for GitLab.com. 1. Confirm the migration has actually completed successfully for GitLab.com.
1. Replace the content of `migrate` and `completed?` methods as follows: 1. Replace the content of the migration with:
```ruby ```ruby
def migrate include Elastic::MigrationObsolete
log_raise "Migration has been deleted in the last major version upgrade." \
"Migrations are supposed to be finished before upgrading major version https://docs.gitlab.com/ee/update/#upgrading-to-a-new-major-version ." \
"To correct this issue, recreate your index from scratch: https://docs.gitlab.com/ee/integration/elasticsearch.html#last-resort-to-recreate-an-index."
end
def completed?
false
end
``` ```
1. Delete any spec files to support this migration. 1. Delete any spec files to support this migration.

View File

@ -976,6 +976,9 @@ If using a model in the migrations, you should first
[clear the column cache](https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-reset_column_information) [clear the column cache](https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-reset_column_information)
using `reset_column_information`. using `reset_column_information`.
If using a model that leverages single table inheritance (STI), there are [special
considerations](single_table_inheritance.md#in-migrations).
This avoids problems where a column that you are using was altered and cached This avoids problems where a column that you are using was altered and cached
in a previous migration. in a previous migration.

View File

@ -49,7 +49,7 @@ ActiveRecord and ActionController log output to that logger. Further options are
documented with the method source. documented with the method source.
```ruby ```ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, logger: Logger.new(STDOUT)) Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, logger: Logger.new($stdout))
``` ```
There is also a RubyProf printer available: There is also a RubyProf printer available:

View File

@ -22,3 +22,42 @@ The solution is very simple: just use a separate table for every type you'd
otherwise store in the same table. For example, instead of having a `keys` table otherwise store in the same table. For example, instead of having a `keys` table
with `type` set to either `Key` or `DeployKey` you'd have two separate tables: with `type` set to either `Key` or `DeployKey` you'd have two separate tables:
`keys` and `deploy_keys`. `keys` and `deploy_keys`.
## In migrations
Whenever a model is used in a migration, single table inheritance should be disabled.
Due to the way Rails loads associations (even in migrations), failing to disable STI
could result in loading unexpected code or associations which may cause unintended
side effects or failures during upgrades.
```ruby
class SomeMigration < ActiveRecord::Migration[6.0]
class Services < ActiveRecord::Base
self.table_name = 'services'
self.inheritance_column = :_type_disabled
end
def up
...
```
If nothing needs to be added to the model other than disabling STI or `EachBatch`,
use the helper `define_batchable_model` instead of defining the class.
This ensures that the migration loads the columns for the migration in isolation,
and the helper disables STI by default.
```ruby
class EnqueueSomeBackgroundMigration < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
define_batchable_model('services').select(:id).in_batches do |relation|
jobs = relation.pluck(:id).map do |id|
['ExtractServicesUrl', [id]]
end
BackgroundMigrationWorker.bulk_perform_async(jobs)
end
end
...
```

View File

@ -11,19 +11,21 @@ in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures `require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
a few other things to make testing Rake tasks easier. a few other things to make testing Rake tasks easier.
At a minimum, requiring the Rake helper redirects `stdout`, include the At a minimum, requiring the Rake helper includes the runtime task helpers, and
runtime task helpers, and include the `RakeHelpers` Spec support module. includes the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available
methods. methods.
`$stdout` can be redirected by adding `:silence_stdout`.
Example: Example:
```ruby ```ruby
require 'rake_helper' require 'rake_helper'
describe 'gitlab:shell rake tasks' do describe 'gitlab:shell rake tasks', :silence_stdout do
before do before do
Rake.application.rake_require 'tasks/gitlab/shell' Rake.application.rake_require 'tasks/gitlab/shell'

View File

@ -15580,11 +15580,11 @@ Whether incoming email is setup
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124916_reply_by_email_enabled.yml) [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124916_reply_by_email_enabled.yml)
Group: `group::product intelligence` Group: `group::certify`
Status: `data_available` Status: `data_available`
Tiers: `free` Tiers: `free`, `premium`, `ultimate`
### `search_unique_visits.i_search_advanced` ### `search_unique_visits.i_search_advanced`

View File

@ -369,6 +369,16 @@ NOTE:
Specific information that follow related to Ruby and Git versions do not apply to [Omnibus installations](https://docs.gitlab.com/omnibus/) Specific information that follow related to Ruby and Git versions do not apply to [Omnibus installations](https://docs.gitlab.com/omnibus/)
and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with appropriate Ruby and Git versions and are not using system binaries for Ruby and Git. There is no need to install Ruby or Git when utilizing these two approaches. and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with appropriate Ruby and Git versions and are not using system binaries for Ruby and Git. There is no need to install Ruby or Git when utilizing these two approaches.
### 14.0.0
In GitLab 13.3 some [pipeline processing methods were deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/218536)
and this code was completely removed in GitLab 14.0. If you plan to upgrade from
**GitLab 13.2 or older** directly to 14.0, you should not have any pipelines running
when you upgrade. The pipelines might report the wrong status when the upgrade completes.
You should shut down GitLab and wait for all pipelines on runners to complete, then upgrade
GitLab to 14.0. Alternatively, you can first upgrade GitLab to a version between 13.3 and
13.12, then upgrade to 14.0.
### 13.11.0 ### 13.11.0
Git 2.31.x and later is required. We recommend you use the Git 2.31.x and later is required. We recommend you use the

View File

@ -1,3 +1,3 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
x = STDIN.read x = $stdin.read
File.write('/tmp/rb-data.txt', x) File.write('/tmp/rb-data.txt', x)

View File

@ -61,6 +61,8 @@ module API
optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database (deprecated in GitLab 11.7)' optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database (deprecated in GitLab 11.7)'
end end
post ':id/repository/tags', :release_orchestration do post ':id/repository/tags', :release_orchestration do
deprecate_release_notes unless params[:release_description].blank?
authorize_admin_tag authorize_admin_tag
result = ::Tags::CreateService.new(user_project, current_user) result = ::Tags::CreateService.new(user_project, current_user)
@ -119,6 +121,7 @@ module API
requires :description, type: String, desc: 'Release notes with markdown support' requires :description, type: String, desc: 'Release notes with markdown support'
end end
post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do
deprecate_release_notes
authorize_create_release! authorize_create_release!
## ##
@ -151,6 +154,7 @@ module API
requires :description, type: String, desc: 'Release notes with markdown support' requires :description, type: String, desc: 'Release notes with markdown support'
end end
put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do
deprecate_release_notes
authorize_update_release! authorize_update_release!
result = ::Releases::UpdateService result = ::Releases::UpdateService
@ -177,6 +181,12 @@ module API
def release def release
@release ||= user_project.releases.find_by_tag(params[:tag]) @release ||= user_project.releases.find_by_tag(params[:tag])
end end
def deprecate_release_notes
return unless Feature.enabled?(:remove_release_notes_from_tags_api, user_project, default_enabled: :yaml)
render_api_error!("Release notes modification via tags API is deprecated, see https://gitlab.com/gitlab-org/gitlab/-/issues/290311", 400)
end
end end
end end
end end

View File

@ -30,8 +30,6 @@ module Banzai
LITERAL_KEYWORD = 'cmliteral' LITERAL_KEYWORD = 'cmliteral'
def call def call
return @text unless Feature.enabled?(:honor_escaped_markdown, context[:group] || context[:project]&.group)
@text.gsub(ASCII_PUNCTUATION) do |match| @text.gsub(ASCII_PUNCTUATION) do |match|
# The majority of markdown does not have literals. If none # The majority of markdown does not have literals. If none
# are found, we can bypass the post filter # are found, we can bypass the post filter

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
BATCH_SIZE = 1000
# This background migration disables container expiration policies connected
# to a project that has no container repositories
class DisableExpirationPoliciesLinkedToNoContainerImages
# rubocop: disable Style/Documentation
class ContainerExpirationPolicy < ActiveRecord::Base
include EachBatch
self.table_name = 'container_expiration_policies'
end
# rubocop: enable Style/Documentation
def perform(from_id, to_id)
ContainerExpirationPolicy.where(enabled: true, project_id: from_id..to_id).each_batch(of: BATCH_SIZE) do |batch|
sql = <<-SQL
WITH batched_relation AS MATERIALIZED (#{batch.select(:project_id).limit(BATCH_SIZE).to_sql})
UPDATE container_expiration_policies
SET enabled = FALSE
FROM batched_relation
WHERE container_expiration_policies.project_id = batched_relation.project_id
AND NOT EXISTS (SELECT 1 FROM "container_repositories" WHERE container_repositories.project_id = container_expiration_policies.project_id)
SQL
execute(sql)
end
end
private
def execute(sql)
ActiveRecord::Base
.connection
.execute(sql)
end
end
end
end

View File

@ -170,7 +170,7 @@ module Gitlab
def self.print_by_total_time(result, options = {}) def self.print_by_total_time(result, options = {})
default_options = { sort_method: :total_time, filter_by: :total_time } default_options = { sort_method: :total_time, filter_by: :total_time }
RubyProf::FlatPrinter.new(result).print(STDOUT, default_options.merge(options)) RubyProf::FlatPrinter.new(result).print($stdout, default_options.merge(options))
end end
end end
end end

View File

@ -22,7 +22,7 @@ module Gitlab
CommandError = Class.new(StandardError) CommandError = Class.new(StandardError)
def initialize(log_output = STDERR) def initialize(log_output = $stderr)
require_relative '../../../lib/gitlab/sidekiq_logging/json_formatter' require_relative '../../../lib/gitlab/sidekiq_logging/json_formatter'
# As recommended by https://github.com/mperham/sidekiq/wiki/Advanced-Options#concurrency # As recommended by https://github.com/mperham/sidekiq/wiki/Advanced-Options#concurrency

View File

@ -13,6 +13,10 @@ module Gitlab
@metrics = init_metrics @metrics = init_metrics
@metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i) @metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i)
if ::Gitlab::Database::LoadBalancing.enable?
@metrics[:sidekiq_load_balancing_count] = ::Gitlab::Metrics.counter(:sidekiq_load_balancing_count, 'Sidekiq jobs with load balancing')
end
end end
def call(worker, job, queue) def call(worker, job, queue)
@ -69,6 +73,15 @@ module Gitlab
@metrics[:sidekiq_redis_requests_duration_seconds].observe(labels, get_redis_time(instrumentation)) @metrics[:sidekiq_redis_requests_duration_seconds].observe(labels, get_redis_time(instrumentation))
@metrics[:sidekiq_elasticsearch_requests_total].increment(labels, get_elasticsearch_calls(instrumentation)) @metrics[:sidekiq_elasticsearch_requests_total].increment(labels, get_elasticsearch_calls(instrumentation))
@metrics[:sidekiq_elasticsearch_requests_duration_seconds].observe(labels, get_elasticsearch_time(instrumentation)) @metrics[:sidekiq_elasticsearch_requests_duration_seconds].observe(labels, get_elasticsearch_time(instrumentation))
if ::Gitlab::Database::LoadBalancing.enable? && job[:database_chosen]
load_balancing_labels = {
database_chosen: job[:database_chosen],
data_consistency: job[:data_consistency]
}
@metrics[:sidekiq_load_balancing_count].increment(labels.merge(load_balancing_labels), 1)
end
end end
end end

View File

@ -61,7 +61,7 @@ module Gitlab
def prompt(message, choices = nil) def prompt(message, choices = nil)
begin begin
print(message) print(message)
answer = STDIN.gets.chomp answer = $stdin.gets.chomp
end while choices.present? && !choices.include?(answer) end while choices.present? && !choices.include?(answer)
answer answer
end end
@ -70,12 +70,12 @@ module Gitlab
# #
# message - custom message to display before input # message - custom message to display before input
def prompt_for_password(message = 'Enter password: ') def prompt_for_password(message = 'Enter password: ')
unless STDIN.tty? unless $stdin.tty?
print(message) print(message)
return STDIN.gets.chomp return $stdin.gets.chomp
end end
STDIN.getpass(message) $stdin.getpass(message)
end end
# Runs the given command and matches the output against the given pattern # Runs the given command and matches the output against the given pattern

View File

@ -9,7 +9,7 @@ module Gitlab
attr_writer :logger attr_writer :logger
def logger def logger
@logger ||= Logger.new(STDOUT) @logger ||= Logger.new($stdout)
end end
end end
@ -67,7 +67,7 @@ module Gitlab
def log_info(details) def log_info(details)
details = base_log_data.merge(details) details = base_log_data.merge(details)
details = details.to_yaml if ActiveSupport::Logger.logger_outputs_to?(Measuring.logger, STDOUT) details = details.to_yaml if ActiveSupport::Logger.logger_outputs_to?(Measuring.logger, $stdout)
Measuring.logger.info(details) Measuring.logger.info(details)
end end
end end

View File

@ -7,7 +7,7 @@ desc 'GitLab | Artifacts | Migrate files for artifacts to comply with new storag
namespace :gitlab do namespace :gitlab do
namespace :artifacts do namespace :artifacts do
task migrate: :environment do task migrate: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
helper = Gitlab::LocalAndRemoteStorageMigration::ArtifactMigrater.new(logger) helper = Gitlab::LocalAndRemoteStorageMigration::ArtifactMigrater.new(logger)
@ -19,7 +19,7 @@ namespace :gitlab do
end end
task migrate_to_local: :environment do task migrate_to_local: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
helper = Gitlab::LocalAndRemoteStorageMigration::ArtifactMigrater.new(logger) helper = Gitlab::LocalAndRemoteStorageMigration::ArtifactMigrater.new(logger)

View File

@ -178,7 +178,7 @@ namespace :gitlab do
return @logger if defined?(@logger) return @logger if defined?(@logger)
@logger = if Rails.env.development? || Rails.env.production? @logger = if Rails.env.development? || Rails.env.production?
Logger.new(STDOUT).tap do |stdout_logger| Logger.new($stdout).tap do |stdout_logger|
stdout_logger.extend(ActiveSupport::Logger.broadcast(Rails.logger)) stdout_logger.extend(ActiveSupport::Logger.broadcast(Rails.logger))
stdout_logger.level = debug? ? Logger::DEBUG : Logger::INFO stdout_logger.level = debug? ? Logger::DEBUG : Logger::INFO
end end

View File

@ -209,7 +209,7 @@ namespace :gitlab do
raise "Index not found or not supported: #{args[:index_name]}" if indexes.empty? raise "Index not found or not supported: #{args[:index_name]}" if indexes.empty?
end end
ActiveRecord::Base.logger = Logger.new(STDOUT) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false) ActiveRecord::Base.logger = Logger.new($stdout) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false)
Gitlab::Database::Reindexing.perform(indexes) Gitlab::Database::Reindexing.perform(indexes)
rescue StandardError => e rescue StandardError => e

View File

@ -14,14 +14,14 @@ namespace :gitlab do
old_path = args.old_path old_path = args.old_path
else else
puts '=> Enter the path of the OLD file:' puts '=> Enter the path of the OLD file:'
old_path = STDIN.gets.chomp old_path = $stdin.gets.chomp
end end
if args.new_path if args.new_path
new_path = args.new_path new_path = args.new_path
else else
puts '=> Enter the path of the NEW file:' puts '=> Enter the path of the NEW file:'
new_path = STDIN.gets.chomp new_path = $stdin.gets.chomp
end end
# #

View File

@ -4,7 +4,7 @@ namespace :gitlab do
namespace :doctor do namespace :doctor do
desc "GitLab | Check if the database encrypted values can be decrypted using current secrets" desc "GitLab | Check if the database encrypted values can be decrypted using current secrets"
task secrets: :gitlab_environment do task secrets: :gitlab_environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.level = Gitlab::Utils.to_boolean(ENV['VERBOSE']) ? Logger::DEBUG : Logger::INFO logger.level = Gitlab::Utils.to_boolean(ENV['VERBOSE']) ? Logger::DEBUG : Logger::INFO

View File

@ -42,7 +42,7 @@ namespace :gitlab do
namespace :secret do namespace :secret do
desc 'GitLab | LDAP | Secret | Write LDAP secrets' desc 'GitLab | LDAP | Secret | Write LDAP secrets'
task write: [:environment] do task write: [:environment] do
content = STDIN.tty? ? STDIN.gets : STDIN.read content = $stdin.tty? ? $stdin.gets : $stdin.read
Gitlab::EncryptedLdapCommand.write(content) Gitlab::EncryptedLdapCommand.write(content)
end end

View File

@ -6,7 +6,7 @@ desc "GitLab | LFS | Migrate LFS objects to remote storage"
namespace :gitlab do namespace :gitlab do
namespace :lfs do namespace :lfs do
task migrate: :environment do task migrate: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Starting transfer of LFS files to object storage') logger.info('Starting transfer of LFS files to object storage')
LfsObject.with_files_stored_locally LfsObject.with_files_stored_locally
@ -20,7 +20,7 @@ namespace :gitlab do
end end
task migrate_to_local: :environment do task migrate_to_local: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Starting transfer of LFS files to local storage') logger.info('Starting transfer of LFS files to local storage')
LfsObject.with_files_stored_remotely LfsObject.with_files_stored_remotely

View File

@ -6,7 +6,7 @@ desc "GitLab | Packages | Build composer cache"
namespace :gitlab do namespace :gitlab do
namespace :packages do namespace :packages do
task build_composer_cache: :environment do task build_composer_cache: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Starting to build composer cache files') logger.info('Starting to build composer cache files')
::Packages::Package.composer.find_in_batches do |packages| ::Packages::Package.composer.find_in_batches do |packages|

View File

@ -14,7 +14,7 @@ namespace :gitlab do
end end
task generate_counts: :environment do task generate_counts: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Building list of package events...') logger.info('Building list of package events...')
path = Gitlab::UsageDataCounters::PackageEventCounter::KNOWN_EVENTS_PATH path = Gitlab::UsageDataCounters::PackageEventCounter::KNOWN_EVENTS_PATH
@ -26,7 +26,7 @@ namespace :gitlab do
end end
task generate_unique: :environment do task generate_unique: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Building list of package events...') logger.info('Building list of package events...')
path = File.join(File.dirname(Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH), 'package_events.yml') path = File.join(File.dirname(Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH), 'package_events.yml')

View File

@ -6,7 +6,7 @@ desc "GitLab | Packages | Migrate packages files to remote storage"
namespace :gitlab do namespace :gitlab do
namespace :packages do namespace :packages do
task migrate: :environment do task migrate: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Starting transfer of package files to object storage') logger.info('Starting transfer of package files to object storage')
unless ::Packages::PackageFileUploader.object_store_enabled? unless ::Packages::PackageFileUploader.object_store_enabled?

View File

@ -35,7 +35,7 @@ namespace :gitlab do
end end
def logger def logger
@logger ||= Logger.new(STDOUT) @logger ||= Logger.new($stdout)
end end
def migration_threads def migration_threads
@ -60,7 +60,7 @@ namespace :gitlab do
namespace :deployments do namespace :deployments do
task migrate_to_object_storage: :gitlab_environment do task migrate_to_object_storage: :gitlab_environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
helper = Gitlab::LocalAndRemoteStorageMigration::PagesDeploymentMigrater.new(logger) helper = Gitlab::LocalAndRemoteStorageMigration::PagesDeploymentMigrater.new(logger)
@ -72,7 +72,7 @@ namespace :gitlab do
end end
task migrate_to_local: :gitlab_environment do task migrate_to_local: :gitlab_environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
helper = Gitlab::LocalAndRemoteStorageMigration::PagesDeploymentMigrater.new(logger) helper = Gitlab::LocalAndRemoteStorageMigration::PagesDeploymentMigrater.new(logger)

View File

@ -6,7 +6,7 @@ desc "GitLab | Terraform | Migrate Terraform states to remote storage"
namespace :gitlab do namespace :gitlab do
namespace :terraform_states do namespace :terraform_states do
task migrate: :environment do task migrate: :environment do
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
logger.info('Starting transfer of Terraform states to object storage') logger.info('Starting transfer of Terraform states to object storage')
begin begin

View File

@ -16,7 +16,7 @@ namespace :gitlab do
# category to object storage # category to object storage
desc 'GitLab | Uploads | Migrate the uploaded files of specified type to object storage' desc 'GitLab | Uploads | Migrate the uploaded files of specified type to object storage'
task :migrate, [:uploader_class, :model_class, :mounted_as] => :environment do |_t, args| task :migrate, [:uploader_class, :model_class, :mounted_as] => :environment do |_t, args|
Gitlab::Uploads::MigrationHelper.new(args, Logger.new(STDOUT)).migrate_to_remote_storage Gitlab::Uploads::MigrationHelper.new(args, Logger.new($stdout)).migrate_to_remote_storage
end end
namespace :migrate_to_local do namespace :migrate_to_local do
@ -31,7 +31,7 @@ namespace :gitlab do
desc 'GitLab | Uploads | Migrate the uploaded files of specified type to local storage' desc 'GitLab | Uploads | Migrate the uploaded files of specified type to local storage'
task :migrate_to_local, [:uploader_class, :model_class, :mounted_as] => :environment do |_t, args| task :migrate_to_local, [:uploader_class, :model_class, :mounted_as] => :environment do |_t, args|
Gitlab::Uploads::MigrationHelper.new(args, Logger.new(STDOUT)).migrate_to_local_storage Gitlab::Uploads::MigrationHelper.new(args, Logger.new($stdout)).migrate_to_local_storage
end end
end end
end end

View File

@ -8,7 +8,7 @@ namespace :gitlab do
args.with_defaults(dry_run: 'true') args.with_defaults(dry_run: 'true')
args.with_defaults(sleep_time: 0.3) args.with_defaults(sleep_time: 0.3)
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
sanitizer = Gitlab::Sanitizers::Exif.new(logger: logger) sanitizer = Gitlab::Sanitizers::Exif.new(logger: logger)
sanitizer.batch_clean(start_id: args.start_id, stop_id: args.stop_id, sanitizer.batch_clean(start_id: args.start_id, stop_id: args.stop_id,

View File

@ -10,7 +10,7 @@ namespace :gitlab do
end end
def update_certificates def update_certificates
logger = Logger.new(STDOUT) logger = Logger.new($stdout)
unless X509CommitSignature.exists? unless X509CommitSignature.exists?
logger.info("Unable to find any x509 commit signatures. Exiting.") logger.info("Unable to find any x509 commit signatures. Exiting.")

View File

@ -38,7 +38,7 @@ class GithubImport
puts "This will import GitHub #{@repo.full_name.bright} into GitLab #{@project_path.bright} as #{@current_user.name}" puts "This will import GitHub #{@repo.full_name.bright} into GitLab #{@project_path.bright} as #{@current_user.name}"
puts "Permission checks are ignored. Press any key to continue.".color(:red) puts "Permission checks are ignored. Press any key to continue.".color(:red)
STDIN.getch $stdin.getch
puts 'Starting the import (this could take a while)'.color(:green) puts 'Starting the import (this could take a while)'.color(:green)
end end
@ -131,7 +131,7 @@ class GithubRepos
end end
def repo_id def repo_id
@repo_id ||= STDIN.gets.chomp.to_i @repo_id ||= $stdin.gets.chomp.to_i
end end
def repos def repos

View File

@ -19,7 +19,7 @@ namespace :tokens do
def reset_all_users_token(reset_token_method) def reset_all_users_token(reset_token_method)
TmpUser.find_in_batches do |batch| TmpUser.find_in_batches do |batch|
puts "Processing batch starting with user ID: #{batch.first.id}" puts "Processing batch starting with user ID: #{batch.first.id}"
STDOUT.flush $stdout.flush
batch.each(&reset_token_method) batch.each(&reset_token_method)
end end

View File

@ -11339,9 +11339,6 @@ msgstr ""
msgid "DevopsAdoption|MRs" msgid "DevopsAdoption|MRs"
msgstr "" msgstr ""
msgid "DevopsAdoption|Maximum %{maxSegments} groups allowed"
msgstr ""
msgid "DevopsAdoption|My group" msgid "DevopsAdoption|My group"
msgstr "" msgstr ""

View File

@ -142,7 +142,7 @@
"lodash": "^4.17.20", "lodash": "^4.17.20",
"marked": "^0.3.12", "marked": "^0.3.12",
"mathjax": "3", "mathjax": "3",
"mermaid": "^8.9.2", "mermaid": "^8.10.2",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"monaco-editor": "^0.20.0", "monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.1", "monaco-editor-webpack-plugin": "^1.9.1",

View File

@ -30,7 +30,7 @@ module QA
element :labels_dropdown_content element :labels_dropdown_content
end end
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue' do base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do
element :labels_edit_button element :labels_edit_button
end end

View File

@ -24,11 +24,11 @@ module QA
element :create_new_board_button element :create_new_board_button
end end
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue' do view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do
element :labels_dropdown_content element :labels_dropdown_content
end end
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue' do view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do
element :labels_edit_button element :labels_edit_button
end end

View File

@ -20,7 +20,7 @@ module QA
end end
def run def run
STDOUT.puts 'Running...' $stdout.puts 'Running...'
# Fetch group's id # Fetch group's id
group_id = fetch_group_id group_id = fetch_group_id
@ -30,16 +30,16 @@ module QA
# Do not delete projects that are less than 4 days old (for debugging purposes) # Do not delete projects that are less than 4 days old (for debugging purposes)
project_ids = fetch_project_ids(group_id, total_project_pages) project_ids = fetch_project_ids(group_id, total_project_pages)
STDOUT.puts "Number of projects to be deleted: #{project_ids.length}" $stdout.puts "Number of projects to be deleted: #{project_ids.length}"
delete_projects(project_ids) unless project_ids.empty? delete_projects(project_ids) unless project_ids.empty?
STDOUT.puts "\nDone" $stdout.puts "\nDone"
end end
private private
def delete_projects(project_ids) def delete_projects(project_ids)
STDOUT.puts "Deleting #{project_ids.length} projects..." $stdout.puts "Deleting #{project_ids.length} projects..."
project_ids.each do |project_id| project_ids.each do |project_id|
delete_response = delete Runtime::API::Request.new(@api_client, "/projects/#{project_id}").url delete_response = delete Runtime::API::Request.new(@api_client, "/projects/#{project_id}").url
dot_or_f = delete_response.code.between?(200, 300) ? "\e[32m.\e[0m" : "\e[31mF\e[0m" dot_or_f = delete_response.code.between?(200, 300) ? "\e[32m.\e[0m" : "\e[31mF\e[0m"

View File

@ -20,7 +20,7 @@ module QA
end end
def run def run
STDOUT.puts 'Running...' $stdout.puts 'Running...'
# Fetch group's id # Fetch group's id
group_id = fetch_group_id group_id = fetch_group_id
@ -29,16 +29,16 @@ module QA
total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages] total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages]
sub_group_ids = fetch_subgroup_ids(group_id, total_sub_group_pages) sub_group_ids = fetch_subgroup_ids(group_id, total_sub_group_pages)
STDOUT.puts "Number of Sub Groups not already marked for deletion: #{sub_group_ids.length}" $stdout.puts "Number of Sub Groups not already marked for deletion: #{sub_group_ids.length}"
delete_subgroups(sub_group_ids) unless sub_group_ids.empty? delete_subgroups(sub_group_ids) unless sub_group_ids.empty?
STDOUT.puts "\nDone" $stdout.puts "\nDone"
end end
private private
def delete_subgroups(sub_group_ids) def delete_subgroups(sub_group_ids)
STDOUT.puts "Deleting #{sub_group_ids.length} subgroups..." $stdout.puts "Deleting #{sub_group_ids.length} subgroups..."
sub_group_ids.each do |subgroup_id| sub_group_ids.each do |subgroup_id|
delete_response = delete Runtime::API::Request.new(@api_client, "/groups/#{subgroup_id}").url delete_response = delete Runtime::API::Request.new(@api_client, "/groups/#{subgroup_id}").url
dot_or_f = delete_response.code == 202 ? "\e[32m.\e[0m" : "\e[31mF\e[0m" dot_or_f = delete_response.code == 202 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"

View File

@ -30,18 +30,18 @@ module QA
end end
def run def run
STDOUT.puts 'Running...' $stdout.puts 'Running...'
keys_head_response = head Runtime::API::Request.new(@api_client, "/user/keys", per_page: ITEMS_PER_PAGE).url keys_head_response = head Runtime::API::Request.new(@api_client, "/user/keys", per_page: ITEMS_PER_PAGE).url
total_pages = keys_head_response.headers[:x_total_pages] total_pages = keys_head_response.headers[:x_total_pages]
test_ssh_key_ids = fetch_test_ssh_key_ids(total_pages) test_ssh_key_ids = fetch_test_ssh_key_ids(total_pages)
STDOUT.puts "Number of test ssh keys to be deleted: #{test_ssh_key_ids.length}" $stdout.puts "Number of test ssh keys to be deleted: #{test_ssh_key_ids.length}"
return if dry_run? return if dry_run?
delete_ssh_keys(test_ssh_key_ids) unless test_ssh_key_ids.empty? delete_ssh_keys(test_ssh_key_ids) unless test_ssh_key_ids.empty?
STDOUT.puts "\nDone" $stdout.puts "\nDone"
end end
private private
@ -50,7 +50,7 @@ module QA
alias_method :dry_run?, :dry_run alias_method :dry_run?, :dry_run
def delete_ssh_keys(ssh_key_ids) def delete_ssh_keys(ssh_key_ids)
STDOUT.puts "Deleting #{ssh_key_ids.length} ssh keys..." $stdout.puts "Deleting #{ssh_key_ids.length} ssh keys..."
ssh_key_ids.each do |key_id| ssh_key_ids.each do |key_id|
delete_response = delete Runtime::API::Request.new(@api_client, "/user/keys/#{key_id}").url delete_response = delete Runtime::API::Request.new(@api_client, "/user/keys/#{key_id}").url
dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF\e[0m" dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"

View File

@ -26,7 +26,7 @@ module QA
end end
def all def all
STDOUT.puts 'Running...' $stdout.puts 'Running...'
group_id = create_group group_id = create_group
create_project(group_id) create_project(group_id)
@ -50,23 +50,23 @@ module QA
end end
threads_arr.each(&:join) threads_arr.each(&:join)
STDOUT.puts "\nURLs: #{@urls}" $stdout.puts "\nURLs: #{@urls}"
File.open("urls.yml", "w") { |file| file.puts @urls.stringify_keys.to_yaml } File.open("urls.yml", "w") { |file| file.puts @urls.stringify_keys.to_yaml }
STDOUT.puts "\nDone" $stdout.puts "\nDone"
end end
def create_group def create_group
group_search_response = create_a_group_api_req(@group_name, @visibility) group_search_response = create_a_group_api_req(@group_name, @visibility)
group = JSON.parse(group_search_response.body) group = JSON.parse(group_search_response.body)
@urls[:group_page] = group["web_url"] @urls[:group_page] = group["web_url"]
STDOUT.puts "Created a group: #{@urls[:group_page]}" $stdout.puts "Created a group: #{@urls[:group_page]}"
group["id"] group["id"]
end end
def create_project(group_id) def create_project(group_id)
create_project_response = create_a_project_api_req(@project_name, group_id, @visibility) create_project_response = create_a_project_api_req(@project_name, group_id, @visibility)
@urls[:project_page] = JSON.parse(create_project_response.body)["web_url"] @urls[:project_page] = JSON.parse(create_project_response.body)["web_url"]
STDOUT.puts "Created a project: #{@urls[:project_page]}" $stdout.puts "Created a project: #{@urls[:project_page]}"
end end
def create_many_issues def create_many_issues
@ -74,7 +74,7 @@ module QA
create_an_issue_api_req("#{@group_name}%2F#{@project_name}", "issue#{i}", "desc#{i}") create_an_issue_api_req("#{@group_name}%2F#{@project_name}", "issue#{i}", "desc#{i}")
end end
@urls[:issues_list_page] = @urls[:project_page] + "/issues" @urls[:issues_list_page] = @urls[:project_page] + "/issues"
STDOUT.puts "Created many issues: #{@urls[:issues_list_page]}" $stdout.puts "Created many issues: #{@urls[:issues_list_page]}"
end end
def create_many_todos def create_many_todos
@ -82,7 +82,7 @@ module QA
create_a_todo_api_req("#{@group_name}%2F#{@project_name}", "#{i + 1}") create_a_todo_api_req("#{@group_name}%2F#{@project_name}", "#{i + 1}")
end end
@urls[:todos_page] = ENV['GITLAB_ADDRESS'] + "/dashboard/todos" @urls[:todos_page] = ENV['GITLAB_ADDRESS'] + "/dashboard/todos"
STDOUT.puts "Created many todos: #{@urls[:todos_page]}" $stdout.puts "Created many todos: #{@urls[:todos_page]}"
end end
def create_many_labels def create_many_labels
@ -90,7 +90,7 @@ module QA
create_a_label_api_req("#{@group_name}%2F#{@project_name}", "label#{i}", "#{Faker::Color.hex_color}") create_a_label_api_req("#{@group_name}%2F#{@project_name}", "label#{i}", "#{Faker::Color.hex_color}")
end end
@urls[:labels_page] = @urls[:project_page] + "/labels" @urls[:labels_page] = @urls[:project_page] + "/labels"
STDOUT.puts "Created many labels: #{@urls[:labels_page]}" $stdout.puts "Created many labels: #{@urls[:labels_page]}"
end end
def create_many_merge_requests def create_many_merge_requests
@ -98,7 +98,7 @@ module QA
create_a_merge_request_api_req("#{@group_name}%2F#{@project_name}", "branch#{i}", Runtime::Env.default_branch, "MR#{i}") create_a_merge_request_api_req("#{@group_name}%2F#{@project_name}", "branch#{i}", Runtime::Env.default_branch, "MR#{i}")
end end
@urls[:mr_list_page] = @urls[:project_page] + "/merge_requests" @urls[:mr_list_page] = @urls[:project_page] + "/merge_requests"
STDOUT.puts "Created many MRs: #{@urls[:mr_list_page]}" $stdout.puts "Created many MRs: #{@urls[:mr_list_page]}"
end end
def create_many_new_files def create_many_new_files
@ -109,7 +109,7 @@ module QA
end end
@urls[:files_page] = @urls[:project_page] + "/tree/#{Runtime::Env.default_branch}" @urls[:files_page] = @urls[:project_page] + "/tree/#{Runtime::Env.default_branch}"
STDOUT.puts "Added many new files: #{@urls[:files_page]}" $stdout.puts "Added many new files: #{@urls[:files_page]}"
end end
def create_many_branches def create_many_branches
@ -117,7 +117,7 @@ module QA
create_a_branch_api_req("branch#{i}", "#{@group_name}%2F#{@project_name}") create_a_branch_api_req("branch#{i}", "#{@group_name}%2F#{@project_name}")
end end
@urls[:branches_page] = @urls[:project_page] + "/-/branches" @urls[:branches_page] = @urls[:project_page] + "/-/branches"
STDOUT.puts "Created many branches: #{@urls[:branches_page]}" $stdout.puts "Created many branches: #{@urls[:branches_page]}"
end end
def create_an_issue_with_many_discussions def create_an_issue_with_many_discussions
@ -130,7 +130,7 @@ module QA
# Add description and labels # Add description and labels
update_an_issue_api_req("#{@group_name}%2F#{@project_name}", issue_id, "#{Faker::Lorem.sentences(500).join(" ")}", labels_list) update_an_issue_api_req("#{@group_name}%2F#{@project_name}", issue_id, "#{Faker::Lorem.sentences(500).join(" ")}", labels_list)
@urls[:large_issue] = @urls[:project_page] + "/issues/#{issue_id}" @urls[:large_issue] = @urls[:project_page] + "/issues/#{issue_id}"
STDOUT.puts "Created an issue with many discussions: #{@urls[:large_issue]}" $stdout.puts "Created an issue with many discussions: #{@urls[:large_issue]}"
end end
def create_an_mr_with_large_files_and_many_mr_discussions def create_an_mr_with_large_files_and_many_mr_discussions
@ -178,7 +178,7 @@ module QA
create_a_discussion_on_mr_api_req("#{@group_name}%2F#{@project_name}", iid, "Let us discuss") create_a_discussion_on_mr_api_req("#{@group_name}%2F#{@project_name}", iid, "Let us discuss")
end end
@urls[:large_mr] = JSON.parse(create_mr_response.body)["web_url"] @urls[:large_mr] = JSON.parse(create_mr_response.body)["web_url"]
STDOUT.puts "Created an MR with many discussions and many very large Files: #{@urls[:large_mr]}" $stdout.puts "Created an MR with many discussions and many very large Files: #{@urls[:large_mr]}"
end end
def create_diff_note(iid, file_count, line_count, head_sha, start_sha, base_sha, line_type) def create_diff_note(iid, file_count, line_count, head_sha, start_sha, base_sha, line_type)
@ -205,7 +205,7 @@ module QA
100.times do |i| 100.times do |i|
update_file_api_req(file_name, branch_name, project_path, Faker::Lorem.sentences(5).join(" "), Faker::Lorem.sentences(500).join("\n")) update_file_api_req(file_name, branch_name, project_path, Faker::Lorem.sentences(5).join(" "), Faker::Lorem.sentences(500).join("\n"))
end end
STDOUT.puts "Using branch: #{branch_name}, created an MR with many commits: #{@urls[:mr_with_many_commits]}" $stdout.puts "Using branch: #{branch_name}, created an MR with many commits: #{@urls[:mr_with_many_commits]}"
end end
private private

View File

@ -12,7 +12,7 @@ module QA
def run def run
do_run do_run
rescue Net::ReadTimeout rescue Net::ReadTimeout
STDOUT.puts 'Net::ReadTimeout during run. Trying again' $stdout.puts 'Net::ReadTimeout during run. Trying again'
run run
end end
@ -23,7 +23,7 @@ module QA
raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD'] raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD']
raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
STDOUT.puts 'Running...' $stdout.puts 'Running...'
Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login) Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials) Page::Main::Login.perform(&:sign_in_using_credentials)

View File

@ -419,7 +419,7 @@ module Trigger
raise "#{self.class.unscoped_class_name} did not succeed!" raise "#{self.class.unscoped_class_name} did not succeed!"
end end
STDOUT.flush $stdout.flush
end end
raise "#{self.class.unscoped_class_name} timed out after waiting for #{duration} minutes!" raise "#{self.class.unscoped_class_name} timed out after waiting for #{duration} minutes!"

View File

@ -211,11 +211,7 @@ RSpec.describe Projects::ForksController do
create(:group, :public).add_owner(user) create(:group, :public).add_owner(user)
# TODO: There is another N+1 caused by user.can?(:create_projects, namespace) expect { do_request.call }.not_to exceed_query_limit(control)
# Defined in ForkNamespaceEntity
extra_count = 1
expect { do_request.call }.not_to exceed_query_limit(control.count + extra_count)
end end
end end
end end

View File

@ -247,6 +247,36 @@ RSpec.describe 'Mermaid rendering', :js do
expect(page).to have_selector('.js-lazy-render-mermaid-container') expect(page).to have_selector('.js-lazy-render-mermaid-container')
end end
end end
it 'renders without any limits on wiki page', :js do
graph_edges = "A-->B;B-->A;"
description = <<~MERMAID
```mermaid
graph LR
#{graph_edges}
```
MERMAID
description *= 51
project = create(:project, :public)
wiki_page = build(:wiki_page, { container: project, content: description })
wiki_page.create message: 'mermaid test commit' # rubocop:disable Rails/SaveBang
wiki_page = project.wiki.find_page(wiki_page.slug)
visit project_wiki_path(project, wiki_page)
wait_for_requests
wait_for_mermaid
page.within('.js-wiki-page-content') do
expect(page).not_to have_selector('.lazy-alert-shown')
expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
end
end
end end
def wait_for_mermaid def wait_for_mermaid

View File

@ -35,15 +35,23 @@ describe('Awards app actions', () => {
}); });
describe('success', () => { describe('success', () => {
describe.each`
relativeRootUrl
${null}
${'/gitlab'}
`('with relative_root_url as $relativeRootUrl', ({ relativeRootUrl }) => {
beforeEach(() => { beforeEach(() => {
window.gon = { relative_url_root: relativeRootUrl };
mock mock
.onGet('/awards', { params: { per_page: 100, page: '1' } }) .onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '1' } })
.reply(200, ['thumbsup'], { 'x-next-page': '2' }); .reply(200, ['thumbsup'], { 'x-next-page': '2' });
mock.onGet('/awards', { params: { per_page: 100, page: '2' } }).reply(200, ['thumbsdown']); mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '2' } })
.reply(200, ['thumbsdown']);
}); });
it('commits FETCH_AWARDS_SUCCESS', async () => { it('commits FETCH_AWARDS_SUCCESS', async () => {
window.gon = { current_user_id: 1 }; window.gon.current_user_id = 1;
await testAction( await testAction(
actions.fetchAwards, actions.fetchAwards,
@ -58,6 +66,7 @@ describe('Awards app actions', () => {
await testAction(actions.fetchAwards, '1', { path: '/awards' }, [], []); await testAction(actions.fetchAwards, '1', { path: '/awards' }, [], []);
}); });
}); });
});
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
@ -85,10 +94,19 @@ describe('Awards app actions', () => {
mock.restore(); mock.restore();
}); });
describe.each`
relativeRootUrl
${null}
${'/gitlab'}
`('with relative_root_url as $relativeRootUrl', ({ relativeRootUrl }) => {
beforeEach(() => {
window.gon = { relative_url_root: relativeRootUrl };
});
describe('adding new award', () => { describe('adding new award', () => {
describe('success', () => { describe('success', () => {
beforeEach(() => { beforeEach(() => {
mock.onPost('/awards').reply(200, { id: 1 }); mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
}); });
it('commits ADD_NEW_AWARD', async () => { it('commits ADD_NEW_AWARD', async () => {
@ -100,7 +118,7 @@ describe('Awards app actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onPost('/awards').reply(500); mock.onPost(`${relativeRootUrl || ''}/awards`).reply(500);
}); });
it('calls Sentry.captureException', async () => { it('calls Sentry.captureException', async () => {
@ -123,7 +141,7 @@ describe('Awards app actions', () => {
describe('success', () => { describe('success', () => {
beforeEach(() => { beforeEach(() => {
mock.onDelete('/awards/1').reply(200); mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(200);
}); });
it('commits REMOVE_AWARD', async () => { it('commits REMOVE_AWARD', async () => {
@ -142,7 +160,7 @@ describe('Awards app actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onDelete('/awards/1').reply(500); mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
}); });
it('calls Sentry.captureException', async () => { it('calls Sentry.captureException', async () => {
@ -165,3 +183,4 @@ describe('Awards app actions', () => {
}); });
}); });
}); });
});

View File

@ -1,127 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import LabelsSelect from '~/labels_select';
import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) =>
shallowMount(BaseComponent, {
propsData: config,
});
describe('BaseComponent', () => {
let wrapper;
let vm;
beforeEach((done) => {
wrapper = createComponent();
({ vm } = wrapper);
Vue.nextTick(done);
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('hiddenInputName', () => {
it('returns correct string when showCreate prop is `true`', () => {
expect(vm.hiddenInputName).toBe('issue[label_names][]');
});
it('returns correct string when showCreate prop is `false`', async () => {
await wrapper.setProps({ showCreate: false });
expect(vm.hiddenInputName).toBe('label_id[]');
});
});
describe('createLabelTitle', () => {
it('returns `Create project label` when `isProject` prop is true', () => {
expect(vm.createLabelTitle).toBe('Create project label');
});
it('return `Create group label` when `isProject` prop is false', async () => {
await wrapper.setProps({ isProject: false });
expect(vm.createLabelTitle).toBe('Create group label');
});
});
describe('manageLabelsTitle', () => {
it('returns `Manage project labels` when `isProject` prop is true', () => {
expect(vm.manageLabelsTitle).toBe('Manage project labels');
});
it('return `Manage group labels` when `isProject` prop is false', async () => {
await wrapper.setProps({ isProject: false });
expect(vm.manageLabelsTitle).toBe('Manage group labels');
});
});
});
describe('methods', () => {
describe('handleClick', () => {
it('emits onLabelClick event with label and list of labels as params', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleClick(mockLabels[0]);
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
});
});
describe('handleCollapsedValueClick', () => {
it('emits toggleCollapse event on component', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleCollapsedValueClick();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
describe('handleDropdownHidden', () => {
it('emits onDropdownClose event on component', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleDropdownHidden();
expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
});
});
});
describe('mounted', () => {
it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
});
});
describe('template', () => {
it('renders component container element with classes `block labels`', () => {
expect(vm.$el.classList.contains('block')).toBe(true);
expect(vm.$el.classList.contains('labels')).toBe(true);
});
it('renders `.selectbox` element', () => {
expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
});
it('renders `.dropdown` element', () => {
expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
});
it('renders `.dropdown-menu` element', () => {
const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
expect(dropdownMenuEl).not.toBeNull();
expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
});
});
});

View File

@ -1,90 +0,0 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
import { mockConfig, mockLabels } from './mock_data';
const componentConfig = {
...mockConfig,
fieldName: 'label_id[]',
labels: mockLabels,
showExtraOptions: false,
};
const createComponent = (config = componentConfig) => {
const Component = Vue.extend(dropdownButtonComponent);
return mountComponent(Component, config);
};
describe('DropdownButtonComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('dropdownToggleText', () => {
it('returns text as `Label` when `labels` prop is empty array', () => {
const mockEmptyLabels = { ...componentConfig, labels: [] };
const vmEmptyLabels = createComponent(mockEmptyLabels);
expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
vmEmptyLabels.$destroy();
});
it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
const mockMoreLabels = { ...componentConfig, labels: mockLabels.concat(mockLabels) };
const vmMoreLabels = createComponent(mockMoreLabels);
expect(vmMoreLabels.dropdownToggleText).toBe(
`Foo Label +${mockMoreLabels.labels.length - 1} more`,
);
vmMoreLabels.$destroy();
});
it('returns first label name when `labels` prop has only one item present', () => {
const singleLabel = { ...componentConfig, labels: [mockLabels[0]] };
const vmSingleLabel = createComponent(singleLabel);
expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
vmSingleLabel.$destroy();
});
});
});
describe('template', () => {
it('renders component container element of type `button`', () => {
expect(vm.$el.nodeName).toBe('BUTTON');
});
it('renders component container element with required data attributes', () => {
expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
expect(vm.$el.dataset.showAny).not.toBeDefined();
});
it('renders dropdown toggle text element', () => {
const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
expect(dropdownToggleTextEl).not.toBeNull();
expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
});
it('renders dropdown button icon', () => {
const dropdownIconEl = vm.$el.querySelector('.dropdown-menu-toggle .gl-icon');
expect(dropdownIconEl).not.toBeNull();
});
});
});

View File

@ -1,103 +0,0 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
import { mockSuggestedColors } from './mock_data';
const createComponent = (headerTitle) => {
const Component = Vue.extend(dropdownCreateLabelComponent);
return mountComponent(Component, {
headerTitle,
});
};
describe('DropdownCreateLabelComponent', () => {
const colorsCount = Object.keys(mockSuggestedColors).length;
let vm;
beforeEach(() => {
gon.suggested_label_colors = mockSuggestedColors;
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('created', () => {
it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
expect(vm.suggestedColors.length).toBe(colorsCount);
});
});
describe('template', () => {
it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
});
it('renders `Go back` button on component header', () => {
const backButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-back');
expect(backButtonEl).not.toBe(null);
expect(backButtonEl.querySelector('[data-testid="arrow-left-icon"]')).not.toBe(null);
});
it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
const headerEl = vm.$el.querySelector('.dropdown-title');
expect(headerEl.innerText.trim()).toContain('Create new label');
});
it('renders component header element with value of `headerTitle` prop', () => {
const headerTitle = 'Create project label';
const vmWithHeaderTitle = createComponent(headerTitle);
const headerEl = vmWithHeaderTitle.$el.querySelector('.dropdown-title');
expect(headerEl.innerText.trim()).toContain(headerTitle);
vmWithHeaderTitle.$destroy();
});
it('renders `Close` button on component header', () => {
const closeButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-close');
expect(closeButtonEl).not.toBe(null);
});
it('renders `Name new label` input element', () => {
expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
});
it('renders suggested colors list elements', () => {
const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
expect(colorsListContainerEl).not.toBe(null);
expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount);
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 153, 102);');
});
it('renders color input element', () => {
expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
expect(
vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview'),
).not.toBe(null);
expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
});
it('renders component action buttons', () => {
const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
expect(createBtnEl).not.toBe(null);
expect(createBtnEl.innerText.trim()).toBe('Create');
expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
});
});
});

View File

@ -1,75 +0,0 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
createLabelTitle,
manageLabelsTitle,
) => {
const Component = Vue.extend(dropdownFooterComponent);
return mountComponent(Component, {
labelsWebUrl,
createLabelTitle,
manageLabelsTitle,
});
};
describe('DropdownFooterComponent', () => {
const createLabelTitle = 'Create project label';
const manageLabelsTitle = 'Manage project labels';
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('renders link element with `Create new label` when `createLabelTitle` prop is not provided', () => {
const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
expect(createLabelEl).not.toBeNull();
expect(createLabelEl.innerText.trim()).toBe('Create new label');
});
it('renders link element with value of `createLabelTitle` prop', () => {
const vmWithCreateLabelTitle = createComponent(mockConfig.labelsWebUrl, createLabelTitle);
const createLabelEl = vmWithCreateLabelTitle.$el.querySelector(
'.dropdown-footer-list .dropdown-toggle-page',
);
expect(createLabelEl.innerText.trim()).toBe(createLabelTitle);
vmWithCreateLabelTitle.$destroy();
});
it('renders link element with `Manage labels` when `manageLabelsTitle` prop is not provided', () => {
const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
expect(manageLabelsEl).not.toBeNull();
expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
});
it('renders link element with value of `manageLabelsTitle` prop', () => {
const vmWithManageLabelsTitle = createComponent(
mockConfig.labelsWebUrl,
createLabelTitle,
manageLabelsTitle,
);
const manageLabelsEl = vmWithManageLabelsTitle.$el.querySelector(
'.dropdown-footer-list .dropdown-external-link',
);
expect(manageLabelsEl.innerText.trim()).toBe(manageLabelsTitle);
vmWithManageLabelsTitle.$destroy();
});
});
});

Some files were not shown because too many files have changed in this diff Show More