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:
Enabled: false
# Offense count: 67
# Cop supports --auto-correct.
Style/GlobalStdStream:
Enabled: false
# Offense count: 897
# Configuration parameters: MinBodyLength.
Style/GuardClause:

View File

@ -1 +1 @@
8fd337f0f718f257ae72a66c464143a395af4c05
df2eb006d241b399b8b6b877afab97713bb5c36a

View File

@ -30,6 +30,24 @@ let renderedMermaidBlocks = 0;
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) {
let theme = 'neutral';
@ -120,8 +138,10 @@ function renderMermaidEl(el) {
function renderMermaids($els) {
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.
if (document.querySelector('body').dataset.page === 'search:show') return;
if (pageName === 'search:show') return;
importMermaidModule()
.then(() => {
@ -140,10 +160,11 @@ function renderMermaids($els) {
* up the entire thread and causing a DoS.
*/
if (
(source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT ||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
shouldLazyLoadMermaidBlock(source)
!WHITELISTED_PAGES.includes(pageName) &&
((source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT ||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
shouldLazyLoadMermaidBlock(source))
) {
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">

View File

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

View File

@ -1,6 +1,7 @@
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import showToast from '~/vue_shared/plugins/global_toast';
import {
@ -16,7 +17,9 @@ export const fetchAwards = async ({ commit, dispatch, state }, page = '1') => {
if (!window.gon?.current_user_id) return;
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 nextPage = normalizedHeaders['X-NEXT-PAGE'];
@ -35,13 +38,15 @@ export const toggleAward = async ({ commit, state }, name) => {
try {
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);
showToast(__('Award removed'));
} 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);

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-button
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"
@click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button

View File

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

View File

@ -5,7 +5,7 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
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 DropdownButton from './dropdown_button.vue';

View File

@ -2,7 +2,7 @@
module Resolvers
class BoardListIssuesResolver < BaseResolver
include BoardIssueFilterable
include BoardItemFilterable
argument :filters, Types::Boards::BoardIssueInputType,
required: false,
@ -13,7 +13,7 @@ module Resolvers
alias_method :list, :object
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)
offset_pagination(service.execute)

View File

@ -2,7 +2,7 @@
module Resolvers
class BoardListsResolver < BaseResolver
include BoardIssueFilterable
include BoardItemFilterable
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
@ -22,7 +22,7 @@ module Resolvers
def resolve_with_lookahead(id: nil, issue_filters: {})
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?

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
module BoardIssueFilterable
module BoardItemFilterable
extend ActiveSupport::Concern
private
def issue_filters(args)
def item_filters(args)
filters = args.to_h
set_filter_values(filters)
@ -32,4 +32,4 @@ module BoardIssueFilterable
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 Gitlab::OptimisticLocking
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

View File

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

View File

@ -31,7 +31,11 @@ class ForkNamespaceEntity < Grape::Entity
end
expose :can_create_project do |namespace, options|
options[:current_user].can?(:create_projects, namespace)
if Feature.enabled?(:fork_project_form, options[:project], default_enabled: :yaml)
true
else
options[:current_user].can?(:create_projects, namespace)
end
end
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,7 +46,9 @@
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
.gl-alert.gl-alert-danger.gl-mb-5
= @error
.gl-alert-container
.gl-alert-content
= @error
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }

View File

@ -8,21 +8,23 @@
= render "projects/merge_requests/mr_box"
.gl-alert.gl-alert-danger
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%p
We cannot render this merge request properly because
- if @merge_request.for_fork? && !@merge_request.source_project
fork project was removed
- elsif !@merge_request.source_branch_exists?
%span{ class: badge_inverse_css_classes }= @merge_request.source_branch
does not exist in
%span{ class: badge_info_css_classes }= @merge_request.source_project_path
- elsif !@merge_request.target_branch_exists?
%span{ class: badge_inverse_css_classes }= @merge_request.target_branch
does not exist in
%span{ class: badge_info_css_classes }= @merge_request.target_project_path
- else
of internal error
.gl-alert-container
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-content{ role: 'alert' }
%p
We cannot render this merge request properly because
- if @merge_request.for_fork? && !@merge_request.source_project
fork project was removed
- elsif !@merge_request.source_branch_exists?
%span{ class: badge_inverse_css_classes }= @merge_request.source_branch
does not exist in
%span{ class: badge_info_css_classes }= @merge_request.source_project_path
- elsif !@merge_request.target_branch_exists?
%span{ class: badge_inverse_css_classes }= @merge_request.target_branch
does not exist in
%span{ class: badge_info_css_classes }= @merge_request.target_project_path
- else
of internal error
%strong
Please close merge request or change branches with existing one
%strong
Please close merge request or change branches with existing one

View File

@ -2,10 +2,9 @@
module AuthorizedProjectUpdate
class UserRefreshOverUserRangeWorker # rubocop:disable Scalability/IdempotentWorker
# When the feature flag named `periodic_project_authorization_update_via_replica` is enabled,
# this worker checks if a specific user requires an update to their project_authorizations records.
# This worker checks if users 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).
# 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.
# 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!`
# See https://gitlab.com/gitlab-org/gitlab/-/issues/325291
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)
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
enqueue_project_authorizations_refresh(user) if project_authorizations_needs_refresh?(user)
end
else
use_primary_database
AuthorizedProjectUpdate::RecalculateForUserRangeService.new(start_user_id, end_user_id).execute
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)
end
end
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)
AuthorizedProjectUpdate::FindRecordsDueForRefreshService.new(user).needs_refresh?
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
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45922
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300531
milestone: '13.9'
name: remove_release_notes_from_tags_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63392
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290311
milestone: '14.0'
type: development
group: 'group::project management'
group: group::release
default_enabled: false

View File

@ -1,16 +1,18 @@
---
key_path: reply_by_email_enabled
description: Whether incoming email is setup
product_section: growth
product_stage: growth
product_group: group::product intelligence
product_section: dev
product_stage: plan
product_group: group::certify
product_category: collection
value_type: boolean
status: data_available
time_frame: none
data_source: system
distribution:
- ce
- ce
- ee
tier:
- free
skip_validation: true
- free
- 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;
$$;
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
LANGUAGE plpgsql
AS $$
@ -208,6 +217,15 @@ BEGIN
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 (
id bigint NOT NULL,
author_id integer NOT NULL,
@ -11195,6 +11213,7 @@ CREATE TABLE ci_stages (
status integer,
lock_version integer DEFAULT 0,
"position" integer,
id_convert_to_bigint bigint DEFAULT 0 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 (
id bigint 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
@ -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_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_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_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_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'
# 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
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:
```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

View File

@ -100,7 +100,7 @@ Rails.cache.instance_variable_get(:@data).keys
```ruby
# 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
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)
```ruby
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
admin = User.find_by_username('ADMIN_USERNAME')
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:
```ruby
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger = Logger.new($stdout)
```
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 |
| ---- | ---- | ----------- |
| <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. |
### `EpicDescendantCount`

View File

@ -255,12 +255,8 @@ batches instead of doing this one by one:
class ScheduleExtractServicesUrl < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
class Service < ActiveRecord::Base
self.table_name = 'services'
end
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|
['ExtractServicesUrl', [id]]
end
@ -286,18 +282,12 @@ this:
class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
class Service < ActiveRecord::Base
include ::EachBatch
self.table_name = 'services'
end
def up
# This must be included
Gitlab::BackgroundMigration.steal('ExtractServicesUrl')
# 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
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
[document this in our upgrade
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
migrations.
- We don't waste CI time running tests for migrations that we don't support
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
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:
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
def migrate
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
include Elastic::MigrationObsolete
```
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)
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
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.
```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:

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
with `type` set to either `Key` or `DeployKey` you'd have two separate tables:
`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
a few other things to make testing Rake tasks easier.
At a minimum, requiring the Rake helper redirects `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
At a minimum, requiring the Rake helper includes the runtime task helpers, and
includes the `RakeHelpers` Spec support module.
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
methods.
`$stdout` can be redirected by adding `:silence_stdout`.
Example:
```ruby
require 'rake_helper'
describe 'gitlab:shell rake tasks' do
describe 'gitlab:shell rake tasks', :silence_stdout do
before do
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)
Group: `group::product intelligence`
Group: `group::certify`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `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/)
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
Git 2.31.x and later is required. We recommend you use the

View File

@ -1,3 +1,3 @@
#!/usr/bin/env ruby
x = STDIN.read
x = $stdin.read
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)'
end
post ':id/repository/tags', :release_orchestration do
deprecate_release_notes unless params[:release_description].blank?
authorize_admin_tag
result = ::Tags::CreateService.new(user_project, current_user)
@ -119,6 +121,7 @@ module API
requires :description, type: String, desc: 'Release notes with markdown support'
end
post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do
deprecate_release_notes
authorize_create_release!
##
@ -151,6 +154,7 @@ module API
requires :description, type: String, desc: 'Release notes with markdown support'
end
put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do
deprecate_release_notes
authorize_update_release!
result = ::Releases::UpdateService
@ -177,6 +181,12 @@ module API
def release
@release ||= user_project.releases.find_by_tag(params[:tag])
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

View File

@ -30,8 +30,6 @@ module Banzai
LITERAL_KEYWORD = 'cmliteral'
def call
return @text unless Feature.enabled?(:honor_escaped_markdown, context[:group] || context[:project]&.group)
@text.gsub(ASCII_PUNCTUATION) do |match|
# The majority of markdown does not have literals. If none
# 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 = {})
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

View File

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

View File

@ -13,6 +13,10 @@ module Gitlab
@metrics = init_metrics
@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
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_elasticsearch_requests_total].increment(labels, get_elasticsearch_calls(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

View File

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

View File

@ -9,7 +9,7 @@ module Gitlab
attr_writer :logger
def logger
@logger ||= Logger.new(STDOUT)
@logger ||= Logger.new($stdout)
end
end
@ -67,7 +67,7 @@ module Gitlab
def log_info(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)
end
end

View File

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

View File

@ -178,7 +178,7 @@ namespace :gitlab do
return @logger if defined?(@logger)
@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.level = debug? ? Logger::DEBUG : Logger::INFO
end

View File

@ -209,7 +209,7 @@ namespace :gitlab do
raise "Index not found or not supported: #{args[:index_name]}" if indexes.empty?
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)
rescue StandardError => e

View File

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

View File

@ -4,7 +4,7 @@ namespace :gitlab do
namespace :doctor do
desc "GitLab | Check if the database encrypted values can be decrypted using current secrets"
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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ namespace :gitlab do
end
task generate_counts: :environment do
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
logger.info('Building list of package events...')
path = Gitlab::UsageDataCounters::PackageEventCounter::KNOWN_EVENTS_PATH
@ -26,7 +26,7 @@ namespace :gitlab do
end
task generate_unique: :environment do
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
logger.info('Building list of package events...')
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 :packages do
task migrate: :environment do
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
logger.info('Starting transfer of package files to object storage')
unless ::Packages::PackageFileUploader.object_store_enabled?

View File

@ -35,7 +35,7 @@ namespace :gitlab do
end
def logger
@logger ||= Logger.new(STDOUT)
@logger ||= Logger.new($stdout)
end
def migration_threads
@ -60,7 +60,7 @@ namespace :gitlab do
namespace :deployments do
task migrate_to_object_storage: :gitlab_environment do
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
helper = Gitlab::LocalAndRemoteStorageMigration::PagesDeploymentMigrater.new(logger)
@ -72,7 +72,7 @@ namespace :gitlab do
end
task migrate_to_local: :gitlab_environment do
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
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 :terraform_states do
task migrate: :environment do
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
logger.info('Starting transfer of Terraform states to object storage')
begin

View File

@ -16,7 +16,7 @@ namespace :gitlab do
# category 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|
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
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'
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

View File

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

View File

@ -10,7 +10,7 @@ namespace :gitlab do
end
def update_certificates
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
unless X509CommitSignature.exists?
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 "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)
end
@ -131,7 +131,7 @@ class GithubRepos
end
def repo_id
@repo_id ||= STDIN.gets.chomp.to_i
@repo_id ||= $stdin.gets.chomp.to_i
end
def repos

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ module QA
element :labels_dropdown_content
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
end

View File

@ -24,11 +24,11 @@ module QA
element :create_new_board_button
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
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
end

View File

@ -20,7 +20,7 @@ module QA
end
def run
STDOUT.puts 'Running...'
$stdout.puts 'Running...'
# Fetch group's 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)
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?
STDOUT.puts "\nDone"
$stdout.puts "\nDone"
end
private
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|
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"

View File

@ -20,7 +20,7 @@ module QA
end
def run
STDOUT.puts 'Running...'
$stdout.puts 'Running...'
# Fetch group's id
group_id = fetch_group_id
@ -29,16 +29,16 @@ module QA
total_sub_group_pages = sub_groups_head_response.headers[:x_total_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?
STDOUT.puts "\nDone"
$stdout.puts "\nDone"
end
private
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|
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"

View File

@ -30,18 +30,18 @@ module QA
end
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
total_pages = keys_head_response.headers[:x_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?
delete_ssh_keys(test_ssh_key_ids) unless test_ssh_key_ids.empty?
STDOUT.puts "\nDone"
$stdout.puts "\nDone"
end
private
@ -50,7 +50,7 @@ module QA
alias_method :dry_run?, :dry_run
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|
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"

View File

@ -26,7 +26,7 @@ module QA
end
def all
STDOUT.puts 'Running...'
$stdout.puts 'Running...'
group_id = create_group
create_project(group_id)
@ -50,23 +50,23 @@ module QA
end
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 }
STDOUT.puts "\nDone"
$stdout.puts "\nDone"
end
def create_group
group_search_response = create_a_group_api_req(@group_name, @visibility)
group = JSON.parse(group_search_response.body)
@urls[:group_page] = group["web_url"]
STDOUT.puts "Created a group: #{@urls[:group_page]}"
$stdout.puts "Created a group: #{@urls[:group_page]}"
group["id"]
end
def create_project(group_id)
create_project_response = create_a_project_api_req(@project_name, group_id, @visibility)
@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
def create_many_issues
@ -74,7 +74,7 @@ module QA
create_an_issue_api_req("#{@group_name}%2F#{@project_name}", "issue#{i}", "desc#{i}")
end
@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
def create_many_todos
@ -82,7 +82,7 @@ module QA
create_a_todo_api_req("#{@group_name}%2F#{@project_name}", "#{i + 1}")
end
@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
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}")
end
@urls[:labels_page] = @urls[:project_page] + "/labels"
STDOUT.puts "Created many labels: #{@urls[:labels_page]}"
$stdout.puts "Created many labels: #{@urls[:labels_page]}"
end
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}")
end
@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
def create_many_new_files
@ -109,7 +109,7 @@ module QA
end
@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
def create_many_branches
@ -117,7 +117,7 @@ module QA
create_a_branch_api_req("branch#{i}", "#{@group_name}%2F#{@project_name}")
end
@urls[:branches_page] = @urls[:project_page] + "/-/branches"
STDOUT.puts "Created many branches: #{@urls[:branches_page]}"
$stdout.puts "Created many branches: #{@urls[:branches_page]}"
end
def create_an_issue_with_many_discussions
@ -130,7 +130,7 @@ module QA
# Add description and labels
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}"
STDOUT.puts "Created an issue with many discussions: #{@urls[:large_issue]}"
$stdout.puts "Created an issue with many discussions: #{@urls[:large_issue]}"
end
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")
end
@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
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|
update_file_api_req(file_name, branch_name, project_path, Faker::Lorem.sentences(5).join(" "), Faker::Lorem.sentences(500).join("\n"))
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
private

View File

@ -12,7 +12,7 @@ module QA
def run
do_run
rescue Net::ReadTimeout
STDOUT.puts 'Net::ReadTimeout during run. Trying again'
$stdout.puts 'Net::ReadTimeout during run. Trying again'
run
end
@ -23,7 +23,7 @@ module QA
raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD']
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)
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!"
end
STDOUT.flush
$stdout.flush
end
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)
# TODO: There is another N+1 caused by user.can?(:create_projects, namespace)
# Defined in ForkNamespaceEntity
extra_count = 1
expect { do_request.call }.not_to exceed_query_limit(control.count + extra_count)
expect { do_request.call }.not_to exceed_query_limit(control)
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')
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
def wait_for_mermaid

View File

@ -35,27 +35,36 @@ describe('Awards app actions', () => {
});
describe('success', () => {
beforeEach(() => {
mock
.onGet('/awards', { params: { per_page: 100, page: '1' } })
.reply(200, ['thumbsup'], { 'x-next-page': '2' });
mock.onGet('/awards', { params: { per_page: 100, page: '2' } }).reply(200, ['thumbsdown']);
});
describe.each`
relativeRootUrl
${null}
${'/gitlab'}
`('with relative_root_url as $relativeRootUrl', ({ relativeRootUrl }) => {
beforeEach(() => {
window.gon = { relative_url_root: relativeRootUrl };
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '1' } })
.reply(200, ['thumbsup'], { 'x-next-page': '2' });
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '2' } })
.reply(200, ['thumbsdown']);
});
it('commits FETCH_AWARDS_SUCCESS', async () => {
window.gon = { current_user_id: 1 };
it('commits FETCH_AWARDS_SUCCESS', async () => {
window.gon.current_user_id = 1;
await testAction(
actions.fetchAwards,
'1',
{ path: '/awards' },
[{ type: 'FETCH_AWARDS_SUCCESS', payload: ['thumbsup'] }],
[{ type: 'fetchAwards', payload: '2' }],
);
});
await testAction(
actions.fetchAwards,
'1',
{ path: '/awards' },
[{ type: 'FETCH_AWARDS_SUCCESS', payload: ['thumbsup'] }],
[{ type: 'fetchAwards', payload: '2' }],
);
});
it('does not commit FETCH_AWARDS_SUCCESS when user signed out', async () => {
await testAction(actions.fetchAwards, '1', { path: '/awards' }, [], []);
it('does not commit FETCH_AWARDS_SUCCESS when user signed out', async () => {
await testAction(actions.fetchAwards, '1', { path: '/awards' }, [], []);
});
});
});
@ -85,81 +94,91 @@ describe('Awards app actions', () => {
mock.restore();
});
describe('adding new award', () => {
describe('success', () => {
beforeEach(() => {
mock.onPost('/awards').reply(200, { id: 1 });
describe.each`
relativeRootUrl
${null}
${'/gitlab'}
`('with relative_root_url as $relativeRootUrl', ({ relativeRootUrl }) => {
beforeEach(() => {
window.gon = { relative_url_root: relativeRootUrl };
});
describe('adding new award', () => {
describe('success', () => {
beforeEach(() => {
mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
});
it('commits ADD_NEW_AWARD', async () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
{ type: 'ADD_NEW_AWARD', payload: { id: 1 } },
]);
});
});
it('commits ADD_NEW_AWARD', async () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
{ type: 'ADD_NEW_AWARD', payload: { id: 1 } },
]);
describe('error', () => {
beforeEach(() => {
mock.onPost(`${relativeRootUrl || ''}/awards`).reply(500);
});
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
null,
{ path: '/awards', awards: [] },
[],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
},
);
});
});
});
describe('error', () => {
beforeEach(() => {
mock.onPost('/awards').reply(500);
describe('removing a award', () => {
const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
describe('success', () => {
beforeEach(() => {
mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(200);
});
it('commits REMOVE_AWARD', async () => {
testAction(
actions.toggleAward,
'thumbsup',
{
path: '/awards',
currentUserId: 1,
awards: [mockData],
},
[{ type: 'REMOVE_AWARD', payload: 1 }],
);
});
});
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
null,
{ path: '/awards', awards: [] },
[],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
},
);
});
});
});
describe('error', () => {
beforeEach(() => {
mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
});
describe('removing a award', () => {
const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
describe('success', () => {
beforeEach(() => {
mock.onDelete('/awards/1').reply(200);
});
it('commits REMOVE_AWARD', async () => {
testAction(
actions.toggleAward,
'thumbsup',
{
path: '/awards',
currentUserId: 1,
awards: [mockData],
},
[{ type: 'REMOVE_AWARD', payload: 1 }],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onDelete('/awards/1').reply(500);
});
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
'thumbsup',
{
path: '/awards',
currentUserId: 1,
awards: [mockData],
},
[],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
},
);
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
'thumbsup',
{
path: '/awards',
currentUserId: 1,
awards: [mockData],
},
[],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
},
);
});
});
});
});

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