Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-12 12:10:49 +00:00
parent 9c07ab8c69
commit bbfd13e575
93 changed files with 1909 additions and 593 deletions

View File

@ -11,6 +11,11 @@ export default {
BoardListHeader, BoardListHeader,
BoardList, BoardList,
}, },
inject: {
boardId: {
default: '',
},
},
props: { props: {
list: { list: {
type: Object, type: Object,
@ -27,11 +32,6 @@ export default {
default: false, default: false,
}, },
}, },
inject: {
boardId: {
default: '',
},
},
data() { data() {
return { return {
detailIssue: boardsStore.detail, detailIssue: boardsStore.detail,

View File

@ -9,6 +9,11 @@ export default {
BoardListHeader, BoardListHeader,
BoardList, BoardList,
}, },
inject: {
boardId: {
default: '',
},
},
props: { props: {
list: { list: {
type: Object, type: Object,
@ -25,11 +30,6 @@ export default {
default: false, default: false,
}, },
}, },
inject: {
boardId: {
default: '',
},
},
computed: { computed: {
...mapState(['filterParams']), ...mapState(['filterParams']),
...mapGetters(['getIssuesByList']), ...mapGetters(['getIssuesByList']),

View File

@ -49,6 +49,14 @@ export default {
GlModal, GlModal,
BoardConfigurationOptions, BoardConfigurationOptions,
}, },
inject: {
fullPath: {
default: '',
},
rootPath: {
default: '',
},
},
props: { props: {
canAdminBoard: { canAdminBoard: {
type: Boolean, type: Boolean,
@ -92,14 +100,6 @@ export default {
required: true, required: true,
}, },
}, },
inject: {
fullPath: {
default: '',
},
rootPath: {
default: '',
},
},
data() { data() {
return { return {
board: { ...boardDefaults, ...this.currentBoard }, board: { ...boardDefaults, ...this.currentBoard },

View File

@ -31,6 +31,11 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: {
boardId: {
default: '',
},
},
props: { props: {
list: { list: {
type: Object, type: Object,
@ -47,11 +52,6 @@ export default {
default: false, default: false,
}, },
}, },
inject: {
boardId: {
default: '',
},
},
data() { data() {
return { return {
weightFeatureAvailable: false, weightFeatureAvailable: false,

View File

@ -37,6 +37,20 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: {
boardId: {
default: '',
},
weightFeatureAvailable: {
default: false,
},
scopedLabelsAvailable: {
default: false,
},
currentUserId: {
default: null,
},
},
props: { props: {
list: { list: {
type: Object, type: Object,
@ -53,20 +67,6 @@ export default {
default: false, default: false,
}, },
}, },
inject: {
boardId: {
default: '',
},
weightFeatureAvailable: {
default: false,
},
scopedLabelsAvailable: {
default: false,
},
currentUserId: {
default: null,
},
},
computed: { computed: {
...mapState(['activeId']), ...mapState(['activeId']),
isLoggedIn() { isLoggedIn() {

View File

@ -16,13 +16,13 @@ export default {
GlButton, GlButton,
}, },
mixins: [glFeatureFlagMixin()], mixins: [glFeatureFlagMixin()],
inject: ['groupId'],
props: { props: {
list: { list: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
inject: ['groupId'],
data() { data() {
return { return {
title: '', title: '',

View File

@ -18,13 +18,13 @@ export default {
GlButton, GlButton,
}, },
mixins: [glFeatureFlagMixin()], mixins: [glFeatureFlagMixin()],
inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'],
props: { props: {
list: { list: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'],
data() { data() {
return { return {
title: '', title: '',

View File

@ -27,6 +27,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [issueCardInner], mixins: [issueCardInner],
inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'],
props: { props: {
issue: { issue: {
type: Object, type: Object,
@ -43,7 +44,6 @@ export default {
default: false, default: false,
}, },
}, },
inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'],
data() { data() {
return { return {
limitBeforeCounter: 2, limitBeforeCounter: 2,

View File

@ -25,6 +25,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [issueCardInner], mixins: [issueCardInner],
inject: ['groupId', 'rootPath'],
props: { props: {
issue: { issue: {
type: Object, type: Object,
@ -41,7 +42,6 @@ export default {
default: false, default: false,
}, },
}, },
inject: ['groupId', 'rootPath'],
data() { data() {
return { return {
limitBeforeCounter: 2, limitBeforeCounter: 2,

View File

@ -11,13 +11,13 @@ export default {
GlIcon, GlIcon,
GlTooltip, GlTooltip,
}, },
inject: ['timeTrackingLimitToHours'],
props: { props: {
estimate: { estimate: {
type: Number, type: Number,
required: true, required: true,
}, },
}, },
inject: ['timeTrackingLimitToHours'],
computed: { computed: {
title() { title() {
return stringifyTime( return stringifyTime(

View File

@ -33,13 +33,13 @@ export default {
GlDropdownText, GlDropdownText,
GlSearchBoxByType, GlSearchBoxByType,
}, },
inject: ['groupId'],
props: { props: {
list: { list: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
inject: ['groupId'],
data() { data() {
return { return {
initialLoading: true, initialLoading: true,

View File

@ -3,6 +3,7 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui';
export default { export default {
components: { GlButton, GlLoadingIcon }, components: { GlButton, GlLoadingIcon },
inject: ['canUpdate'],
props: { props: {
title: { title: {
type: String, type: String,
@ -25,7 +26,6 @@ export default {
default: true, default: true,
}, },
}, },
inject: ['canUpdate'],
data() { data() {
return { return {
edit: false, edit: false,

View File

@ -14,12 +14,12 @@ export default {
LabelsSelect, LabelsSelect,
GlLabel, GlLabel,
}, },
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
data() { data() {
return { return {
loading: false, loading: false,
}; };
}, },
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: { computed: {
...mapGetters(['activeIssue', 'projectPathForActiveIssue']), ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
selectedLabels() { selectedLabels() {

View File

@ -18,6 +18,10 @@ export default (params = {}) => {
BoardsSelector, BoardsSelector,
}, },
apolloProvider, apolloProvider,
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
},
data() { data() {
const { dataset } = boardsSwitcherElement; const { dataset } = boardsSwitcherElement;
@ -35,10 +39,6 @@ export default (params = {}) => {
return { boardsSelectorProps }; return { boardsSelectorProps };
}, },
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
},
render(createElement) { render(createElement) {
return createElement(BoardsSelector, { return createElement(BoardsSelector, {
props: this.boardsSelectorProps, props: this.boardsSelectorProps,

View File

@ -1,5 +1,5 @@
<script> <script>
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -28,12 +28,13 @@ const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export default { export default {
components: { components: {
CommitForm,
CiLint, CiLint,
CommitForm,
EditorTab, EditorTab,
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
GlTabs, GlTabs,
GlTab,
PipelineGraph, PipelineGraph,
TextEditor, TextEditor,
ValidationSegment, ValidationSegment,
@ -317,16 +318,15 @@ export default {
:commit-sha="lastCommitSha" :commit-sha="lastCommitSha"
/> />
</editor-tab> </editor-tab>
<editor-tab <gl-tab
v-if="glFeatures.ciConfigVisualizationTab" v-if="glFeatures.ciConfigVisualizationTab"
:lazy="true" :lazy="true"
:title="$options.i18n.tabGraph" :title="$options.i18n.tabGraph"
:title-link-attributes="{ 'data-testid': 'visualization-tab-btn' }"
data-testid="visualization-tab" data-testid="visualization-tab"
> >
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
<pipeline-graph v-else :pipeline-data="ciConfigData" /> <pipeline-graph v-else :pipeline-data="ciConfigData" />
</editor-tab> </gl-tab>
<editor-tab :title="$options.i18n.tabLint"> <editor-tab :title="$options.i18n.tabLint">
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />

View File

@ -1,21 +1,37 @@
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as Flash } from '../../../flash'; import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../mr_widget_author.vue'; import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants'; import { AUTO_MERGE_STRATEGIES } from '../../constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default { export default {
name: 'MRWidgetAutoMergeEnabled', name: 'MRWidgetAutoMergeEnabled',
apollo: {
state: {
query: autoMergeEnabledQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest,
},
},
components: { components: {
MrWidgetAuthor, MrWidgetAuthor,
statusIcon, statusIcon,
GlLoadingIcon, GlLoadingIcon,
GlSkeletonLoader,
}, },
mixins: [autoMergeMixin], mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: { props: {
mr: { mr: {
type: Object, type: Object,
@ -30,20 +46,47 @@ export default {
}, },
data() { data() {
return { return {
state: {},
isCancellingAutoMerge: false, isCancellingAutoMerge: false,
isRemovingSourceBranch: false, isRemovingSourceBranch: false,
}; };
}, },
computed: { computed: {
canRemoveSourceBranch() { loading() {
const { return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
shouldRemoveSourceBranch, },
canRemoveSourceBranch, mergeUser() {
mergeUserId, if (this.glFeatures.mergeRequestWidgetGraphql) {
currentUserId, return this.state.mergeUser;
} = this.mr; }
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId; return this.mr.setToAutoMergeBy;
},
targetBranch() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch;
},
shouldRemoveSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
}
return this.mr.shouldRemoveSourceBranch;
},
autoMergeStrategy() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy;
},
canRemoveSourceBranch() {
const { currentUserId } = this.mr;
const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql
? this.state.mergeUser?.id
: this.mr.mergeUserId;
const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql
? this.state.userPermissions.removeSourceBranch
: this.mr.canRemoveSourceBranch;
return (
!this.shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId
);
}, },
}, },
methods: { methods: {
@ -63,7 +106,7 @@ export default {
removeSourceBranch() { removeSourceBranch() {
const options = { const options = {
sha: this.mr.sha, sha: this.mr.sha,
auto_merge_strategy: this.mr.autoMergeStrategy, auto_merge_strategy: this.autoMergeStrategy,
should_remove_source_branch: true, should_remove_source_branch: true,
}; };
@ -86,49 +129,64 @@ export default {
</script> </script>
<template> <template>
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" /> <div v-if="loading" class="gl-w-full mr-conflict-loader">
<div class="media-body"> <gl-skeleton-loader :width="334" :height="30">
<h4 class="d-flex align-items-start"> <rect x="0" y="3" width="24" height="24" rx="4" />
<span class="gl-mr-3"> <rect x="32" y="7" width="150" height="16" rx="4" />
<span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span> <rect x="190" y="7" width="144" height="16" rx="4" />
<mr-widget-author :author="mr.setToAutoMergeBy" /> </gl-skeleton-loader>
<span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
</span>
<a
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="d-flex align-items-start">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
@click.prevent="removeSourceBranch"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
</a>
</p>
</section>
</div> </div>
<template v-else>
<status-icon status="success" />
<div class="media-body">
<h4 class="gl-display-flex">
<span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{
statusTextBeforeAuthor
}}</span>
<mr-widget-author :author="mergeUser" />
<span class="js-status-text-after-author" data-testid="afterStatusText">{{
statusTextAfterAuthor
}}</span>
</span>
<a
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a>
</p>
<p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
data-testid="removeSourceBranchButton"
@click.prevent="removeSourceBranch"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
</a>
</p>
</section>
</div>
</template>
</div> </div>
</template> </template>

View File

@ -1,7 +1,10 @@
<script> <script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default { export default {
name: 'MRWidgetAutoMergeFailed', name: 'MRWidgetAutoMergeFailed',
@ -10,6 +13,19 @@ export default {
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
}, },
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
mergeError: {
query: autoMergeFailedQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest?.mergeError,
},
},
props: { props: {
mr: { mr: {
type: Object, type: Object,
@ -18,6 +34,7 @@ export default {
}, },
data() { data() {
return { return {
mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError,
isRefreshing: false, isRefreshing: false,
}; };
}, },
@ -36,7 +53,7 @@ export default {
<status-icon status="warning" /> <status-icon status="warning" />
<div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center"> <div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center">
<span class="bold"> <span class="bold">
<template v-if="mr.mergeError">{{ mr.mergeError }}</template> <template v-if="mergeError">{{ mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }} {{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span> </span>
<gl-button <gl-button

View File

@ -0,0 +1,15 @@
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
name
username
webUrl
avatarUrl
}
targetBranch
shouldRemoveSourceBranch
forceRemoveSourceBranch
userPermissions {
removeSourceBranch
}
}

View File

@ -0,0 +1,10 @@
#import "./auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
mergeTrainsCount
}
}
}

View File

@ -0,0 +1,7 @@
query autoMergeFailedQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
mergeError
}
}
}

View File

@ -4,6 +4,9 @@ import { spriteIcon } from '~/lib/utils/common_utils';
const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
// Number of users to show in the autocomplete menu to avoid doing a mass fetch of 100+ avatars
const memberLimit = 10;
const nonWordOrInteger = /\W|^\d+$/; const nonWordOrInteger = /\W|^\d+$/;
export const GfmAutocompleteType = { export const GfmAutocompleteType = {
@ -74,6 +77,7 @@ export const tributeConfig = {
fillAttr: 'username', fillAttr: 'username',
lookup: (value) => lookup: (value) =>
value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`, value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`,
menuItemLimit: memberLimit,
menuItemTemplate: ({ original }) => { menuItemTemplate: ({ original }) => {
const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0'; const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small const noAvatarClasses = `${commonClasses} gl-rounded-small

View File

@ -5,6 +5,10 @@
min-width: auto; min-width: auto;
} }
.filtered-search-box .form-control {
min-width: unset;
}
.sort-control { .sort-control {
.btn { .btn {
padding-right: 2rem; padding-right: 2rem;

View File

@ -6,7 +6,7 @@ module Types
graphql_name 'AlertManagementDomainFilter' graphql_name 'AlertManagementDomainFilter'
description 'Filters the alerts based on given domain' description 'Filters the alerts based on given domain'
value 'operations', description: 'Alerts for operations domain ' value 'operations', description: 'Alerts for operations domain'
value 'threat_monitoring', description: 'Alerts for threat monitoring domain' value 'threat_monitoring', description: 'Alerts for threat monitoring domain'
end end
end end

View File

@ -175,6 +175,10 @@ module Types
calls_gitaly: true, description: 'Merge request commits excluding merge commits' calls_gitaly: true, description: 'Merge request commits excluding merge commits'
field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true, field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.' description: 'Indicates if the merge request is created by @GitLab-Security-Bot.'
field :auto_merge_strategy, GraphQL::STRING_TYPE, null: true,
description: 'Selected auto merge strategy'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request'
def approved_by def approved_by
object.approved_by_users object.approved_by_users

View File

@ -4,9 +4,25 @@ class Namespace::PackageSetting < ApplicationRecord
self.primary_key = :namespace_id self.primary_key = :namespace_id
self.table_name = 'namespace_package_settings' self.table_name = 'namespace_package_settings'
PackageSettingNotImplemented = Class.new(StandardError)
PACKAGES_WITH_SETTINGS = %w[maven].freeze
belongs_to :namespace, inverse_of: :package_setting_relation belongs_to :namespace, inverse_of: :package_setting_relation
validates :namespace, presence: true validates :namespace, presence: true
validates :maven_duplicates_allowed, inclusion: { in: [true, false] } validates :maven_duplicates_allowed, inclusion: { in: [true, false] }
validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
class << self
def duplicates_allowed?(package)
return true unless package
raise PackageSettingNotImplemented unless PACKAGES_WITH_SETTINGS.include?(package.package_type)
duplicates_allowed = package.package_settings["#{package.package_type}_duplicates_allowed"]
regex = ::Gitlab::UntrustedRegexp.new("\\A#{package.package_settings["#{package.package_type}_duplicate_exception_regex"]}\\z")
duplicates_allowed || regex.match?(package.name)
end
end
end end

View File

@ -200,6 +200,12 @@ class Packages::Package < ApplicationRecord
debian? && !version.nil? debian? && !version.nil?
end end
def package_settings
strong_memoize(:package_settings) do
project.namespace.package_settings
end
end
private private
def composer_tag_version? def composer_tag_version?

View File

@ -1333,19 +1333,11 @@ class Project < ApplicationRecord
end end
def external_wiki def external_wiki
if has_external_wiki.nil? cache_has_external_wiki if has_external_wiki.nil?
cache_has_external_wiki
end
if has_external_wiki return unless has_external_wiki?
@external_wiki ||= services.external_wikis.first
else
nil
end
end
def cache_has_external_wiki @external_wiki ||= services.external_wikis.first
update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end end
def find_or_initialize_services def find_or_initialize_services
@ -2707,6 +2699,10 @@ class Project < ApplicationRecord
objects.each_batch { |relation| out.concat(relation.pluck(:oid)) } objects.each_batch { |relation| out.concat(relation.pluck(:oid)) }
end end
end end
def cache_has_external_wiki
update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end
end end
Project.prepend_if_ee('EE::Project') Project.prepend_if_ee('EE::Project')

View File

@ -48,7 +48,6 @@ class Service < ApplicationRecord
after_commit :reset_updated_properties after_commit :reset_updated_properties
after_commit :cache_project_has_external_issue_tracker after_commit :cache_project_has_external_issue_tracker
after_commit :cache_project_has_external_wiki
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
belongs_to :group, inverse_of: :services belongs_to :group, inverse_of: :services
@ -469,12 +468,6 @@ class Service < ApplicationRecord
end end
end end
def cache_project_has_external_wiki
if project && !project.destroyed?
project.cache_has_external_wiki
end
end
def valid_recipients? def valid_recipients?
activated? && !importing? activated? && !importing?
end end

View File

@ -38,10 +38,6 @@ class BulkCreateIntegrationService
if integration.external_issue_tracker? if integration.external_issue_tracker?
Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true) Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true)
end end
if integration.external_wiki?
Project.where(id: batch.select(:id)).update_all(has_external_wiki: true)
end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord

View File

@ -10,6 +10,10 @@ module Packages
::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project) ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project)
.execute .execute
unless Namespace::PackageSetting.duplicates_allowed?(package)
return ServiceResponse.error(message: 'Duplicate package is not allowed')
end
unless package unless package
# Maven uploads several files during `mvn deploy` in next order: # Maven uploads several files during `mvn deploy` in next order:
# - my-company/my-app/1.0-SNAPSHOT/my-app.jar # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
@ -48,7 +52,7 @@ module Packages
package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present? package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present?
package ServiceResponse.success(payload: { package: package })
end end
end end
end end

View File

@ -4,7 +4,7 @@
%li{ class: "branch-item js-branch-item js-branch-#{branch.name}", data: { name: branch.name } } %li{ class: "branch-item js-branch-item js-branch-#{branch.name}", data: { name: branch.name } }
.branch-info .branch-info
.branch-title .branch-title
= sprite_icon('fork', size: 12) = sprite_icon('fork', size: 12, css_class: 'gl-flex-shrink-0')
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do
= branch.name = branch.name
- if branch.name == @repository.root_ref - if branch.name == @repository.root_ref

View File

@ -1433,6 +1433,14 @@
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: bulk_import - :name: bulk_import
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: bulk_imports_entity
:feature_category: :importers :feature_category: :importers
:has_external_dependencies: true :has_external_dependencies: true
:urgency: :low :urgency: :low

View File

@ -7,9 +7,58 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: false, dead: false sidekiq_options retry: false, dead: false
worker_has_external_dependencies! PERFORM_DELAY = 5.seconds
DEFAULT_BATCH_SIZE = 5
def perform(bulk_import_id) def perform(bulk_import_id)
BulkImports::Importers::GroupsImporter.new(bulk_import_id).execute @bulk_import = BulkImport.find_by_id(bulk_import_id)
return unless @bulk_import
return if @bulk_import.finished?
return @bulk_import.finish! if all_entities_processed? && @bulk_import.started?
return re_enqueue if max_batch_size_exceeded? # Do not start more jobs if max allowed are already running
@bulk_import.start! if @bulk_import.created?
created_entities.first(next_batch_size).each do |entity|
entity.start!
BulkImports::EntityWorker.perform_async(entity.id)
end
re_enqueue
end
private
def entities
@entities ||= @bulk_import.entities
end
def started_entities
entities.with_status(:started)
end
def created_entities
entities.with_status(:created)
end
def all_entities_processed?
entities.all? { |entity| entity.finished? || entity.failed? }
end
def max_batch_size_exceeded?
started_entities.count >= DEFAULT_BATCH_SIZE
end
def next_batch_size
[DEFAULT_BATCH_SIZE - started_entities.count, 0].max
end
# A new BulkImportWorker job is enqueued to either
# - Process the new BulkImports::Entity created during import (e.g. for the subgroups)
# - Or to mark the `bulk_import` as finished
def re_enqueue
BulkImportWorker.perform_in(PERFORM_DELAY, @bulk_import.id)
end end
end end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module BulkImports
class EntityWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category :importers
sidekiq_options retry: false, dead: false
worker_has_external_dependencies!
def perform(entity_id)
entity = BulkImports::Entity.with_status(:started).find_by_id(entity_id)
if entity
entity.update!(jid: jid)
BulkImports::Importers::GroupImporter.new(entity).execute
end
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Check namespace package settings when creating Maven packages
merge_request: 50691
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix mobile layout Error Tracking details page
merge_request: 50970
author: Kev @KevSlashNull
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add PostgreSQL trigger to maintain projects.has_external_wiki
merge_request: 49916
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix fork icon shrinks if branch name is very long
merge_request: 50915
author: Kev @KevSlashNull
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Expose hide_backlog_list and hide_closed_list to project and group boards REST API
merge_request: 49815
author: Mathieu Parent
type: added

View File

@ -0,0 +1,5 @@
---
title: Move Group Migration entities import to individual sidekiq jobs
merge_request: 50781
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Skip secret_detection on tags
merge_request: 51129
author:
type: changed

View File

@ -50,6 +50,8 @@
- 1 - 1
- - bulk_import - - bulk_import
- 1 - 1
- - bulk_imports_entity
- 1
- - chaos - - chaos
- 2 - 2
- - chat_notification - - chat_notification

View File

@ -63,7 +63,7 @@
stage: Release stage: Release
self-managed: true self-managed: true
gitlab-com: true gitlab-com: true
packages: [starter, premium, ultimate] packages: [Starter, Premium, Ultimate]
url: https://www.youtube.com/embed/1FBRaBQTQZk url: https://www.youtube.com/embed/1FBRaBQTQZk
image_url: https://img.youtube.com/vi/1FBRaBQTQZk/hqdefault.jpg image_url: https://img.youtube.com/vi/1FBRaBQTQZk/hqdefault.jpg
published_at: 2020-09-22 published_at: 2020-09-22

View File

@ -47,4 +47,5 @@
stage: Verify stage: Verify
body: | body: |
Available today is the GitLab Runner container image for the [Red Hat OpenShift Container Platform](https://www.openshift.com/products/container-platform). To install the runner on OpenShift, you can use the new [GitLab Runner Operator](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator) available from the beta channel in Red Hat's Operator Hub - a web console for OpenShift cluster administrators to discover and select Operators to install on their cluster. Operator Hub is deployed by default in the OpenShift Container Platform. We plan to transition the GitLab Runner Operator to the stable channel, and by extension [GA](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator/-/issues/6), in early 2021. Finally, we are also developing an operator for GitLab, so stay tuned to future release posts for those announcements. Available today is the GitLab Runner container image for the [Red Hat OpenShift Container Platform](https://www.openshift.com/products/container-platform). To install the runner on OpenShift, you can use the new [GitLab Runner Operator](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator) available from the beta channel in Red Hat's Operator Hub - a web console for OpenShift cluster administrators to discover and select Operators to install on their cluster. Operator Hub is deployed by default in the OpenShift Container Platform. We plan to transition the GitLab Runner Operator to the stable channel, and by extension [GA](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator/-/issues/6), in early 2021. Finally, we are also developing an operator for GitLab, so stay tuned to future release posts for those announcements.
published_at: 2020-12-22
release: 13.7

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
class AddHasExternalWikiTrigger < ActiveRecord::Migration[6.0]
include Gitlab::Database::SchemaHelpers
DOWNTIME = false
FUNCTION_NAME = 'set_has_external_wiki'.freeze
TRIGGER_ON_INSERT_NAME = 'trigger_has_external_wiki_on_insert'.freeze
TRIGGER_ON_UPDATE_NAME = 'trigger_has_external_wiki_on_update'.freeze
TRIGGER_ON_DELETE_NAME = 'trigger_has_external_wiki_on_delete'.freeze
def up
create_trigger_function(FUNCTION_NAME, replace: true) do
<<~SQL
UPDATE projects SET has_external_wiki = COALESCE(NEW.active, FALSE)
WHERE projects.id = COALESCE(NEW.project_id, OLD.project_id);
RETURN NULL;
SQL
end
execute(<<~SQL)
CREATE TRIGGER #{TRIGGER_ON_INSERT_NAME}
AFTER INSERT ON services
FOR EACH ROW
WHEN (NEW.active = TRUE AND NEW.type = 'ExternalWikiService' AND NEW.project_id IS NOT NULL)
EXECUTE FUNCTION #{FUNCTION_NAME}();
SQL
execute(<<~SQL)
CREATE TRIGGER #{TRIGGER_ON_UPDATE_NAME}
AFTER UPDATE ON services
FOR EACH ROW
WHEN (NEW.type = 'ExternalWikiService' AND OLD.active != NEW.active AND NEW.project_id IS NOT NULL)
EXECUTE FUNCTION #{FUNCTION_NAME}();
SQL
execute(<<~SQL)
CREATE TRIGGER #{TRIGGER_ON_DELETE_NAME}
AFTER DELETE ON services
FOR EACH ROW
WHEN (OLD.type = 'ExternalWikiService' AND OLD.project_id IS NOT NULL)
EXECUTE FUNCTION #{FUNCTION_NAME}();
SQL
end
def down
drop_trigger(:services, TRIGGER_ON_INSERT_NAME)
drop_trigger(:services, TRIGGER_ON_UPDATE_NAME)
drop_trigger(:services, TRIGGER_ON_DELETE_NAME)
drop_function(FUNCTION_NAME)
end
end

View File

@ -0,0 +1 @@
db23b5315386ad5d5fec5a14958769cc1e62a0a89ec3246edb9fc024607e917b

View File

@ -10,6 +10,17 @@ CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE FUNCTION set_has_external_wiki() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE projects SET has_external_wiki = COALESCE(NEW.active, FALSE)
WHERE projects.id = COALESCE(NEW.project_id, OLD.project_id);
RETURN NULL;
END
$$;
CREATE FUNCTION table_sync_function_2be879775d() RETURNS trigger CREATE FUNCTION table_sync_function_2be879775d() RETURNS trigger
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
@ -23559,6 +23570,12 @@ ALTER INDEX product_analytics_events_experimental_pkey ATTACH PARTITION gitlab_p
CREATE TRIGGER table_sync_trigger_ee39a25f9d AFTER INSERT OR DELETE OR UPDATE ON audit_events FOR EACH ROW EXECUTE PROCEDURE table_sync_function_2be879775d(); CREATE TRIGGER table_sync_trigger_ee39a25f9d AFTER INSERT OR DELETE OR UPDATE ON audit_events FOR EACH ROW EXECUTE PROCEDURE table_sync_function_2be879775d();
CREATE TRIGGER trigger_has_external_wiki_on_delete AFTER DELETE ON services FOR EACH ROW WHEN ((((old.type)::text = 'ExternalWikiService'::text) AND (old.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_wiki();
CREATE TRIGGER trigger_has_external_wiki_on_insert AFTER INSERT ON services FOR EACH ROW WHEN (((new.active = true) AND ((new.type)::text = 'ExternalWikiService'::text) AND (new.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_wiki();
CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON services FOR EACH ROW WHEN ((((new.type)::text = 'ExternalWikiService'::text) AND (old.active <> new.active) AND (new.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_wiki();
ALTER TABLE ONLY chat_names ALTER TABLE ONLY chat_names
ADD CONSTRAINT fk_00797a2bf9 FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE; ADD CONSTRAINT fk_00797a2bf9 FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE;

View File

@ -19,7 +19,7 @@ before/after the brackets. Also, some shells (for example, `zsh`) can interpret
## Caveats ## Caveats
If the GitHub [rate limit](https://developer.github.com/v3/#rate-limiting) is reached while importing, If the GitHub [rate limit](https://docs.github.com/v3/#rate-limiting) is reached while importing,
the importing process waits (`sleep()`) until it can continue importing. the importing process waits (`sleep()`) until it can continue importing.
## Importing multiple projects ## Importing multiple projects

View File

@ -121,6 +121,9 @@ to the default installation:
- Enable zero-downtime upgrades. - Enable zero-downtime upgrades.
- Increase availability. - Increase availability.
For more details on how to configure a traffic load balancer with GitLab, you can refer
to any of the [available reference architectures](#available-reference-architectures) with more than 1,000 users.
### Zero downtime updates **(STARTER ONLY)** ### Zero downtime updates **(STARTER ONLY)**
> - Level of complexity: **Medium** > - Level of complexity: **Medium**

View File

@ -157,7 +157,7 @@ See current settings with:
```shell ```shell
sudo gitlab-rails runner "c = ApplicationRecord.connection ; puts c.execute('SHOW statement_timeout').to_a ; sudo gitlab-rails runner "c = ApplicationRecord.connection ; puts c.execute('SHOW statement_timeout').to_a ;
puts c.execute('SHOW lock_timeout').to_a ; puts c.execute('SHOW deadlock_timeout').to_a ;
puts c.execute('SHOW idle_in_transaction_session_timeout').to_a ;" puts c.execute('SHOW idle_in_transaction_session_timeout').to_a ;"
``` ```
@ -165,9 +165,19 @@ It may take a little while to respond.
```ruby ```ruby
{"statement_timeout"=>"1min"} {"statement_timeout"=>"1min"}
{"lock_timeout"=>"0"} {"deadlock_timeout"=>"0"}
{"idle_in_transaction_session_timeout"=>"1min"} {"idle_in_transaction_session_timeout"=>"1min"}
``` ```
These settings can be updated in `/etc/gitlab/gitlab.rb` with:
```ruby
postgresql['deadlock_timeout'] = '5s'
postgresql['statement_timeout'] = '15s'
postgresql['idle_in_transaction_session_timeout'] = '60s'
```
Once saved, [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
NOTE: NOTE:
These are Omnibus GitLab settings. If an external database, such as a customer's PostgreSQL installation or Amazon RDS is being used, these values don't get set, and would have to be set externally. These are Omnibus GitLab settings. If an external database, such as a customer's PostgreSQL installation or Amazon RDS is being used, these values don't get set, and would have to be set externally.

View File

@ -29,7 +29,7 @@ documentation for some popular browsers.
- [Network Monitor - Firefox Developer Tools](https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor) - [Network Monitor - Firefox Developer Tools](https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor)
- [Inspect Network Activity In Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/network/) - [Inspect Network Activity In Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/network/)
- [Safari Web Development Tools](https://developer.apple.com/safari/tools/) - [Safari Web Development Tools](https://developer.apple.com/safari/tools/)
- [Microsoft Edge Network panel](https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide/network#request-details) - [Microsoft Edge Network panel](https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/network/)
To locate a relevant request and view its correlation ID: To locate a relevant request and view its correlation ID:

View File

@ -600,7 +600,7 @@ Filters the alerts based on given domain
""" """
enum AlertManagementDomainFilter { enum AlertManagementDomainFilter {
""" """
Alerts for operations domain Alerts for operations domain
""" """
operations operations
@ -13836,6 +13836,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
""" """
autoMergeEnabled: Boolean! autoMergeEnabled: Boolean!
"""
Selected auto merge strategy
"""
autoMergeStrategy: String
""" """
Array of available auto merge strategies Array of available auto merge strategies
""" """
@ -14075,6 +14080,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
""" """
mergeTrainsCount: Int mergeTrainsCount: Int
"""
User who merged this merge request
"""
mergeUser: User
""" """
Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)
""" """

View File

@ -1500,7 +1500,7 @@
"enumValues": [ "enumValues": [
{ {
"name": "operations", "name": "operations",
"description": "Alerts for operations domain ", "description": "Alerts for operations domain",
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
@ -37994,6 +37994,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "autoMergeStrategy",
"description": "Selected auto merge strategy",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "availableAutoMergeStrategies", "name": "availableAutoMergeStrategies",
"description": "Array of available auto merge strategies", "description": "Array of available auto merge strategies",
@ -38645,6 +38659,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "mergeUser",
"description": "User who merged this merge request",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "mergeWhenPipelineSucceeds", "name": "mergeWhenPipelineSucceeds",
"description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)", "description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)",

View File

@ -2095,6 +2095,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `assignees` | UserConnection | Assignees of the merge request | | `assignees` | UserConnection | Assignees of the merge request |
| `author` | User | User who created this merge request | | `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request | | `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
| `autoMergeStrategy` | String | Selected auto merge strategy |
| `availableAutoMergeStrategies` | String! => Array | Array of available auto merge strategies | | `availableAutoMergeStrategies` | String! => Array | Array of available auto merge strategies |
| `commitCount` | Int | Number of commits in the merge request | | `commitCount` | Int | Number of commits in the merge request |
| `commitsWithoutMergeCommits` | CommitConnection | Merge request commits excluding merge commits | | `commitsWithoutMergeCommits` | CommitConnection | Merge request commits excluding merge commits |
@ -2125,6 +2126,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring | | `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
| `mergeStatus` | String | Status of the merge request | | `mergeStatus` | String | Status of the merge request |
| `mergeTrainsCount` | Int | | | `mergeTrainsCount` | Int | |
| `mergeUser` | User | User who merged this merge request |
| `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) | | `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) |
| `mergeable` | Boolean! | Indicates if the merge request is mergeable | | `mergeable` | Boolean! | Indicates if the merge request is mergeable |
| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged | | `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
@ -4162,7 +4164,7 @@ Filters the alerts based on given domain.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| `operations` | Alerts for operations domain | | `operations` | Alerts for operations domain |
| `threat_monitoring` | Alerts for threat monitoring domain | | `threat_monitoring` | Alerts for threat monitoring domain |
### AlertManagementIntegrationType ### AlertManagementIntegrationType

View File

@ -279,7 +279,7 @@ Example response:
} }
``` ```
## Update a group issue board **(PREMIUM)** ## Update a group issue board
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in GitLab 11.1. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in GitLab 11.1.
@ -289,15 +289,17 @@ Updates a Group Issue Board.
PUT /groups/:id/boards/:board_id PUT /groups/:id/boards/:board_id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- | | ---------------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
| `name` | string | no | The new name of the board | | `name` | string | no | The new name of the board |
| `assignee_id` | integer | no | The assignee the board should be scoped to | | `hide_backlog_list` | boolean | no | Hide the Open list |
| `milestone_id` | integer | no | The milestone the board should be scoped to | | `hide_closed_list` | boolean | no | Hide the Closed list |
| `labels` | string | no | Comma-separated list of label names which the board should be scoped to | | `assignee_id` **(PREMIUM)** | integer | no | The assignee the board should be scoped to |
| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to | | `milestone_id` **(PREMIUM)** | integer | no | The milestone the board should be scoped to |
| `labels` **(PREMIUM)** | string | no | Comma-separated list of label names which the board should be scoped to |
| `weight` **(PREMIUM)** | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
```shell ```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/boards/1?name=new_name&milestone_id=44&assignee_id=1&labels=GroupLabel&weight=4" curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/boards/1?name=new_name&milestone_id=44&assignee_id=1&labels=GroupLabel&weight=4"

View File

@ -7,10 +7,10 @@ module API
prepend_if_ee('EE::API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule prepend_if_ee('EE::API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule
before { authenticate! }
feature_category :boards feature_category :boards
before { authenticate! }
helpers do helpers do
def board_parent def board_parent
user_project user_project

View File

@ -80,10 +80,20 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label' requires :label_id, type: Integer, desc: 'The ID of an existing label'
end end
params :update_params do params :update_params_ce do
optional :name, type: String, desc: 'The board name'
optional :hide_backlog_list, type: Grape::API::Boolean, desc: 'Hide the Open list'
optional :hide_closed_list, type: Grape::API::Boolean, desc: 'Hide the Closed list'
end
params :update_params_ee do
# Configurable issue boards are not available in CE/EE Core. # Configurable issue boards are not available in CE/EE Core.
# https://docs.gitlab.com/ee/user/project/issue_board.html#configurable-issue-boards # https://docs.gitlab.com/ee/user/project/issue_board.html#configurable-issue-boards
optional :name, type: String, desc: 'The board name' end
params :update_params do
use :update_params_ce
use :update_params_ee
end end
end end
end end

View File

@ -5,6 +5,8 @@ module API
class Board < Grape::Entity class Board < Grape::Entity
expose :id expose :id
expose :name expose :name
expose :hide_backlog_list
expose :hide_closed_list
expose :project, using: Entities::BasicProjectDetails expose :project, using: Entities::BasicProjectDetails
expose :lists, using: Entities::List do |board| expose :lists, using: Entities::List do |board|

View File

@ -9,9 +9,7 @@ module API
feature_category :boards feature_category :boards
before do before { authenticate! }
authenticate!
end
helpers do helpers do
def board_parent def board_parent
@ -22,18 +20,8 @@ module API
params do params do
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
end end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/boards' do segment ':id/boards' do
desc 'Find a group board' do
detail 'This feature was introduced in 10.6'
success ::API::Entities::Board
end
get '/:board_id' do
authorize!(:read_board, user_group)
present board, with: ::API::Entities::Board
end
desc 'Get all group boards' do desc 'Get all group boards' do
detail 'This feature was introduced in 10.6' detail 'This feature was introduced in 10.6'
success Entities::Board success Entities::Board
@ -45,6 +33,28 @@ module API
authorize!(:read_board, user_group) authorize!(:read_board, user_group)
present paginate(board_parent.boards.with_associations), with: Entities::Board present paginate(board_parent.boards.with_associations), with: Entities::Board
end end
desc 'Find a group board' do
detail 'This feature was introduced in 10.6'
success Entities::Board
end
get '/:board_id' do
authorize!(:read_board, user_group)
present board, with: Entities::Board
end
desc 'Update a group board' do
detail 'This feature was introduced in 11.0'
success Entities::Board
end
params do
use :update_params
end
put '/:board_id' do
authorize!(:admin_board, board_parent)
update_board
end
end end
params do params do

View File

@ -220,9 +220,13 @@ module API
file_name, format = extract_format(params[:file_name]) file_name, format = extract_format(params[:file_name])
package = ::Packages::Maven::FindOrCreatePackageService result = ::Packages::Maven::FindOrCreatePackageService
.new(user_project, current_user, params.merge(build: current_authenticated_job)).execute .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute
bad_request!(result.errors.first) if result.error?
package = result.payload[:package]
case format case format
when 'sha1' when 'sha1'
# After uploading a file, Maven tries to upload a sha1 and md5 version of it. # After uploading a file, Maven tries to upload a sha1 and md5 version of it.

View File

@ -8,7 +8,6 @@ module BulkImports
end end
def execute def execute
entity.start!
bulk_import = entity.bulk_import bulk_import = entity.bulk_import
configuration = bulk_import.configuration configuration = bulk_import.configuration

View File

@ -1,36 +0,0 @@
# frozen_string_literal: true
module BulkImports
module Importers
class GroupsImporter
def initialize(bulk_import_id)
@bulk_import = BulkImport.find(bulk_import_id)
end
def execute
bulk_import.start! unless bulk_import.started?
if entities_to_import.empty?
bulk_import.finish!
else
entities_to_import.each do |entity|
BulkImports::Importers::GroupImporter.new(entity).execute
end
# A new BulkImportWorker job is enqueued to either
# - Process the new BulkImports::Entity created for the subgroups
# - Or to mark the `bulk_import` as finished.
BulkImportWorker.perform_async(bulk_import.id)
end
end
private
attr_reader :bulk_import
def entities_to_import
@entities_to_import ||= bulk_import.entities.with_status(:created)
end
end
end
end

View File

@ -37,6 +37,7 @@ secret_detection:
when: never when: never
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
script: script:
- if [[ $CI_COMMIT_TAG ]]; echo "Skipping Secret Detection for tags. No code changes have occurred."; then exit 0; fi
- git fetch origin $CI_DEFAULT_BRANCH $CI_COMMIT_REF_NAME - git fetch origin $CI_DEFAULT_BRANCH $CI_COMMIT_REF_NAME
- git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_COMMIT_REF_NAME > "$CI_COMMIT_SHA"_commit_list.txt - git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_COMMIT_REF_NAME > "$CI_COMMIT_SHA"_commit_list.txt
- export SECRET_DETECTION_COMMITS_FILE="$CI_COMMIT_SHA"_commit_list.txt - export SECRET_DETECTION_COMMITS_FILE="$CI_COMMIT_SHA"_commit_list.txt

View File

@ -5,9 +5,10 @@ module Gitlab
module Reindexing module Reindexing
# This can be used to send annotations for reindexing to a Grafana API # This can be used to send annotations for reindexing to a Grafana API
class GrafanaNotifier class GrafanaNotifier
def initialize(api_key = ENV['GITLAB_GRAFANA_API_KEY'], api_url = ENV['GITLAB_GRAFANA_API_URL']) def initialize(api_key = ENV['GITLAB_GRAFANA_API_KEY'], api_url = ENV['GITLAB_GRAFANA_API_URL'], additional_tag = ENV['GITLAB_REINDEXING_GRAFANA_TAG'] || Rails.env)
@api_key = api_key @api_key = api_key
@api_url = api_url @api_url = api_url
@additional_tag = additional_tag
end end
def notify_start(action) def notify_start(action)
@ -37,7 +38,7 @@ module Gitlab
def base_payload(action) def base_payload(action)
{ {
time: (action.action_start.utc.to_f * 1000).to_i, time: (action.action_start.utc.to_f * 1000).to_i,
tags: ['reindex', action.index.tablename, action.index.name] tags: ['reindex', @additional_tag, action.index.tablename, action.index.name].compact
} }
end end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
module ReleaseHighlights
class Validator
attr_reader :errors, :file
def initialize(file:)
@file = file
@errors = []
end
def valid?
document = YAML.parse(File.read(file))
document.root.children.each do |entry|
entry = ReleaseHighlights::Validator::Entry.new(entry)
errors.push(entry.errors.full_messages) unless entry.valid?
end
errors.none?
end
def self.validate_all!
@all_errors = []
ReleaseHighlight.file_paths.each do |file_path|
instance = self.new(file: file_path)
@all_errors.push([instance.errors, instance.file]) unless instance.valid?
end
@all_errors.none?
end
def self.error_message
io = StringIO.new
@all_errors.each do |errors, file|
message = "Validation failed for #{file}"
line = -> { io.puts "-" * message.length }
line.call
io.puts message
line.call
errors.flatten.each { |error| io.puts "* #{error}" }
io.puts
end
io.string
end
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
module ReleaseHighlights
class Validator::Entry
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
PACKAGES = %w(Core Starter Premium Ultimate).freeze
attr_reader :entry
validates :title, :body, :stage, presence: true
validates :'self-managed', :'gitlab-com', inclusion: { in: [true, false], message: "must be a boolean" }
validates :url, :image_url, format: { with: URI::DEFAULT_PARSER.make_regexp, message: 'must be a URL' }
validates :release, numericality: true
validate :validate_published_at
validate :validate_packages
after_validation :add_line_numbers_to_errors!
def initialize(entry)
@entry = entry
end
def validate_published_at
published_at = value_for('published_at')
return if published_at.is_a?(Date)
errors.add(:published_at, 'must be valid Date')
end
def validate_packages
packages = value_for('packages')
if !packages.is_a?(Array) || packages.empty? || packages.any? { |p| PACKAGES.exclude?(p) }
errors.add(:packages, "must be one of #{PACKAGES}")
end
end
def read_attribute_for_validation(key)
value_for(key)
end
private
def add_line_numbers_to_errors!
errors.messages.each do |attribute, messages|
messages.map! { |m| "#{m} (line #{line_number_for(attribute)})" }
end
end
def line_number_for(key)
node = find_node(key)
(node&.start_line || @entry.start_line) + 1
end
def value_for(key)
node = find_node(key)
return if node.nil?
index = entry.children.find_index(node)
next_node = entry.children[index + 1]
next_node&.to_ruby
end
def find_node(key)
entry.children.find {|node| node.try(:value) == key.to_s }
end
end
end

View File

@ -34,7 +34,8 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
let(:experiment_active) { true } let(:experiment_active) { true }
let(:in_experiment_group) { true } let(:in_experiment_group) { true }
it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js do it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js,
{ quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297347' } } do
expect(page).to have_css('.gitlab-ci-syntax-yml-selector') expect(page).to have_css('.gitlab-ci-syntax-yml-selector')
find('.js-gitlab-ci-syntax-yml-selector').click find('.js-gitlab-ci-syntax-yml-selector').click

9
spec/fixtures/whats_new/blank.yml vendored Normal file
View File

@ -0,0 +1,9 @@
- title:
body:
stage:
self-managed:
gitlab-com:
url:
image_url:
published_at:
release:

20
spec/fixtures/whats_new/invalid.yml vendored Normal file
View File

@ -0,0 +1,20 @@
- title: Create and view requirements in GitLab
body: The first step towards managing requirements from within GitLab is here! This initial release allows users to create and view requirements at a project level. As Requirements Management evolves in GitLab, stay tuned for support for traceability between all artifacts, creating a seamless workflow to visually demonstrate completeness and compliance.
stage: Plan
self-managed: true
gitlab-com: true
packages: [ALL]
url: https://docs.gitlab.com/ee/user/project/requirements/index.html
image_url: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png
published_at: 2020-04-22
release: 12.10
- title: Retrieve CI/CD secrets from HashiCorp Vault
body: In this release, GitLab adds support for lightweight JSON Web Token (JWT) authentication to integrate with your existing HashiCorp Vault. Now, you can seamlessly provide secrets to CI/CD jobs by taking advantage of HashiCorp's JWT authentication method rather than manually having to provide secrets as a variable in GitLab.
stage: Release
self-managed: true
gitlab-com: true
packages: [Starter]
url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html
image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png
published_at: 2020-04-22
release: 12.10

20
spec/fixtures/whats_new/valid.yml vendored Normal file
View File

@ -0,0 +1,20 @@
- title: Create and view requirements in GitLab
body: The first step towards managing requirements from within GitLab is here! This initial release allows users to create and view requirements at a project level. As Requirements Management evolves in GitLab, stay tuned for support for traceability between all artifacts, creating a seamless workflow to visually demonstrate completeness and compliance.
stage: Plan
self-managed: true
gitlab-com: true
packages: [Ultimate]
url: https://docs.gitlab.com/ee/user/project/requirements/index.html
image_url: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png
published_at: 2020-04-22
release: 12.10
- title: Retrieve CI/CD secrets from HashiCorp Vault
body: In this release, GitLab adds support for lightweight JSON Web Token (JWT) authentication to integrate with your existing HashiCorp Vault. Now, you can seamlessly provide secrets to CI/CD jobs by taking advantage of HashiCorp's JWT authentication method rather than manually having to provide secrets as a variable in GitLab.
stage: Release
self-managed: true
gitlab-com: true
packages: [Starter]
url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html
image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png
published_at: 2020-04-22
release: 12.10

View File

@ -198,21 +198,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
expect(findPipelineGraph().exists()).toBe(false); expect(findPipelineGraph().exists()).toBe(false);
}); });
it('displays the graph only after the tab is mounted and selected', async () => {
createComponent({ mountFn: mount });
expect(findTabAt(1).find(PipelineGraph).exists()).toBe(false);
await nextTick();
// Select visualization tab
wrapper.find('[data-testid="visualization-tab-btn"]').trigger('click');
await nextTick();
expect(findTabAt(1).find(PipelineGraph).exists()).toBe(true);
});
}); });
describe('with feature flag off', () => { describe('with feature flag off', () => {

View File

@ -0,0 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;

View File

@ -1,20 +1,81 @@
import Vue from 'vue'; import { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper'; import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'jest/helpers/vue_test_utils_helper';
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue'; import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
let wrapper;
let mergeRequestWidgetGraphqlEnabled = false;
function convertPropsToGraphqlState(props) {
return {
autoMergeStrategy: props.autoMergeStrategy,
cancelAutoMergePath: 'http://text.com',
mergeUser: {
id: props.mergeUserId,
...props.setToAutoMergeBy,
},
targetBranch: props.targetBranch,
targetBranchCommitsPath: props.targetBranchPath,
shouldRemoveSourceBranch: props.shouldRemoveSourceBranch,
forceRemoveSourceBranch: props.shouldRemoveSourceBranch,
userPermissions: {
removeSourceBranch: props.canRemoveSourceBranch,
},
};
}
function factory(propsData) {
let state = {};
if (mergeRequestWidgetGraphqlEnabled) {
state = convertPropsToGraphqlState(propsData);
}
wrapper = extendedWrapper(
shallowMount(autoMergeEnabledComponent, {
propsData: {
mr: propsData,
service: new MRWidgetService({}),
},
data() {
return { state };
},
provide: { glFeatures: { mergeRequestWidgetGraphql: mergeRequestWidgetGraphqlEnabled } },
mocks: {
$apollo: {
queries: {
state: { loading: false },
},
},
},
}),
);
}
const targetBranchPath = '/foo/bar';
const targetBranch = 'foo';
const sha = '1EA2EZ34';
const defaultMrProps = () => ({
shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true,
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
describe('MRWidgetAutoMergeEnabled', () => { describe('MRWidgetAutoMergeEnabled', () => {
let vm;
let oldWindowGl; let oldWindowGl;
const targetBranchPath = '/foo/bar';
const targetBranch = 'foo';
const sha = '1EA2EZ34';
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(autoMergeEnabledComponent);
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
oldWindowGl = window.gl; oldWindowGl = window.gl;
@ -23,216 +84,234 @@ describe('MRWidgetAutoMergeEnabled', () => {
defaultAvatarUrl: 'no_avatar.png', defaultAvatarUrl: 'no_avatar.png',
}, },
}; };
vm = mountComponent(Component, {
mr: {
shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true,
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
autoMergeStrategy: MWPS_MERGE_STRATEGY,
},
service: new MRWidgetService({}),
});
}); });
afterEach(() => { afterEach(() => {
vm.$destroy();
window.gl = oldWindowGl; window.gl = oldWindowGl;
wrapper.destroy();
wrapper = null;
}); });
describe('computed', () => { [true, false].forEach((mergeRequestWidgetGraphql) => {
describe('canRemoveSourceBranch', () => { describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
it('should return true when user is able to remove source branch', () => { beforeEach(() => {
expect(vm.canRemoveSourceBranch).toBeTruthy(); mergeRequestWidgetGraphqlEnabled = mergeRequestWidgetGraphql;
}); });
it('should return false when user id is not the same with who set the MWPS', () => { describe('computed', () => {
vm.mr.mergeUserId = 2; describe('canRemoveSourceBranch', () => {
it('should return true when user is able to remove source branch', () => {
expect(vm.canRemoveSourceBranch).toBeFalsy(); factory({
...defaultMrProps(),
vm.mr.currentUserId = 2;
expect(vm.canRemoveSourceBranch).toBeTruthy();
vm.mr.currentUserId = 3;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
it('should return false when shouldRemoveSourceBranch set to false', () => {
vm.mr.shouldRemoveSourceBranch = true;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
it('should return false if user is not able to remove the source branch', () => {
vm.mr.canRemoveSourceBranch = false;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
});
describe('statusTextBeforeAuthor', () => {
it('should return "Set by" if the MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.statusTextBeforeAuthor).toBe('Set by');
});
});
describe('statusTextAfterAuthor', () => {
it('should return "to be merged automatically..." if MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.statusTextAfterAuthor).toBe(
'to be merged automatically when the pipeline succeeds',
);
});
});
describe('cancelButtonText', () => {
it('should return "Cancel automatic merge" if MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.cancelButtonText).toBe('Cancel automatic merge');
});
});
});
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
it('should set flag and call service then tell main component to update the widget with data', (done) => {
const mrObj = {
is_new_mr_data: true,
};
jest.spyOn(vm.service, 'cancelAutomaticMerge').mockReturnValue(
new Promise((resolve) => {
resolve({
data: mrObj,
}); });
}),
);
vm.cancelAutomaticMerge(); expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
setImmediate(() => { });
expect(vm.isCancellingAutoMerge).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); it.each`
done(); mergeUserId | currentUserId
}); ${2} | ${1}
}); ${1} | ${2}
}); `(
'should return false when user id is not the same with who set the MWPS',
describe('removeSourceBranch', () => { ({ mergeUserId, currentUserId }) => {
it('should set flag and call service then request main component to update the widget', (done) => { factory({
jest.spyOn(vm.service, 'merge').mockReturnValue( ...defaultMrProps(),
Promise.resolve({ mergeUserId,
data: { currentUserId,
status: MWPS_MERGE_STRATEGY, });
},
}), expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
); },
);
vm.removeSourceBranch();
setImmediate(() => { it('should return false when shouldRemoveSourceBranch set to false', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); factory({
expect(vm.service.merge).toHaveBeenCalledWith({ ...defaultMrProps(),
sha, shouldRemoveSourceBranch: true,
auto_merge_strategy: MWPS_MERGE_STRATEGY, });
should_remove_source_branch: true,
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
});
it('should return false if user is not able to remove the source branch', () => {
factory({
...defaultMrProps(),
canRemoveSourceBranch: false,
});
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
});
});
describe('statusTextBeforeAuthor', () => {
it('should return "Set by" if the MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(wrapper.findByTestId('beforeStatusText').text()).toBe('Set by');
});
});
describe('statusTextAfterAuthor', () => {
it('should return "to be merged automatically..." if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(wrapper.findByTestId('afterStatusText').text()).toBe(
'to be merged automatically when the pipeline succeeds',
);
});
});
describe('cancelButtonText', () => {
it('should return "Cancel automatic merge" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe(
'Cancel automatic merge',
);
}); });
done();
}); });
}); });
});
});
describe('template', () => { describe('methods', () => {
it('should have correct elements', () => { describe('cancelAutomaticMerge', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); it('should set flag and call service then tell main component to update the widget with data', (done) => {
expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds'); factory({
...defaultMrProps(),
});
const mrObj = {
is_new_mr_data: true,
};
jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue(
new Promise((resolve) => {
resolve({
data: mrObj,
});
}),
);
expect(vm.$el.innerText).toContain('The changes will be merged into'); wrapper.vm.cancelAutomaticMerge();
expect(vm.$el.innerText).toContain(targetBranch); setImmediate(() => {
expect(vm.$el.innerText).toContain('The source branch will not be deleted'); expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain( expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
'Cancel automatic merge', done();
); });
});
});
expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); describe('removeSourceBranch', () => {
expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain( it('should set flag and call service then request main component to update the widget', (done) => {
'Delete source branch', factory({
); ...defaultMrProps(),
});
jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(
Promise.resolve({
data: {
status: MWPS_MERGE_STRATEGY,
},
}),
);
expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy(); wrapper.vm.removeSourceBranch();
}); setImmediate(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
it('should disable cancel auto merge button when the action is in progress', (done) => { expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
vm.isCancellingAutoMerge = true; sha,
auto_merge_strategy: MWPS_MERGE_STRATEGY,
Vue.nextTick(() => { should_remove_source_branch: true,
expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); });
done(); done();
});
});
});
}); });
});
it('should show source branch will be deleted text when it source branch set to remove', (done) => { describe('template', () => {
vm.mr.shouldRemoveSourceBranch = true; it('should have correct elements', () => {
factory({
...defaultMrProps(),
});
Vue.nextTick(() => { expect(wrapper.element).toMatchSnapshot();
const normalizedText = vm.$el.innerText.replace(/\s+/g, ' '); });
expect(normalizedText).toContain('The source branch will be deleted'); it('should disable cancel auto merge button when the action is in progress', async () => {
expect(normalizedText).not.toContain('The source branch will not be deleted'); factory({
done(); ...defaultMrProps(),
}); });
}); wrapper.setData({
isCancellingAutoMerge: true,
});
it('should not show delete source branch button when user not able to delete source branch', (done) => { await nextTick();
vm.mr.currentUserId = 4;
Vue.nextTick(() => { expect(wrapper.find('.js-cancel-auto-merge').attributes('disabled')).toBe('disabled');
expect(vm.$el.querySelector('.js-remove-source-branch')).toEqual(null); });
done();
});
});
it('should disable delete source branch button when the action is in progress', (done) => { it('should show source branch will be deleted text when it source branch set to remove', () => {
vm.isRemovingSourceBranch = true; factory({
...defaultMrProps(),
shouldRemoveSourceBranch: true,
});
Vue.nextTick(() => { const normalizedText = wrapper.text().replace(/\s+/g, ' ');
expect(
vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled'),
).toBeTruthy();
done();
});
});
it('should render the status text as "...to merged automatically" if MWPS is selected', (done) => { expect(normalizedText).toContain('The source branch will be deleted');
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY); expect(normalizedText).not.toContain('The source branch will not be deleted');
});
Vue.nextTick(() => { it('should not show delete source branch button when user not able to delete source branch', () => {
const statusText = trimText(vm.$el.querySelector('.js-status-text-after-author').innerText); factory({
...defaultMrProps(),
currentUserId: 4,
});
expect(statusText).toBe('to be merged automatically when the pipeline succeeds'); expect(wrapper.find('.js-remove-source-branch').exists()).toBe(false);
done(); });
});
});
it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', (done) => { it('should disable delete source branch button when the action is in progress', async () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY); factory({
...defaultMrProps(),
});
wrapper.setData({
isRemovingSourceBranch: true,
});
Vue.nextTick(() => { await nextTick();
const cancelButtonText = trimText(vm.$el.querySelector('.js-cancel-auto-merge').innerText);
expect(cancelButtonText).toBe('Cancel automatic merge'); expect(wrapper.find('.js-remove-source-branch').attributes('disabled')).toBe('disabled');
done(); });
it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
const statusText = trimText(wrapper.find('.js-status-text-after-author').text());
expect(statusText).toBe('to be merged automatically when the pipeline succeeds');
});
it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
expect(cancelButtonText).toBe('Cancel automatic merge');
});
}); });
}); });
}); });

View File

@ -1,3 +1,4 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
@ -8,43 +9,60 @@ describe('MRWidgetAutoMergeFailed', () => {
const mergeError = 'This is the merge error'; const mergeError = 'This is the merge error';
const findButton = () => wrapper.find(GlButton); const findButton = () => wrapper.find(GlButton);
const createComponent = (props = {}) => { const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(AutoMergeFailedComponent, { wrapper = shallowMount(AutoMergeFailedComponent, {
propsData: { ...props }, propsData: { ...props },
data() {
if (mergeRequestWidgetGraphql) {
return { mergeError: props.mr?.mergeError };
}
return {};
},
provide: {
glFeatures: { mergeRequestWidgetGraphql },
},
}); });
}; };
beforeEach(() => {
createComponent({
mr: { mergeError },
});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders failed message', () => { [true, false].forEach((mergeRequestWidgetGraphql) => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
}); beforeEach(() => {
createComponent(
{
mr: { mergeError },
},
mergeRequestWidgetGraphql,
);
});
it('renders merge error provided', () => { it('renders failed message', () => {
expect(wrapper.text()).toContain(mergeError); expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
}); });
it('render refresh button', () => { it('renders merge error provided', () => {
expect(findButton().text()).toEqual('Refresh'); expect(wrapper.text()).toContain(mergeError);
}); });
it('emits event and shows loading icon when button is clicked', () => { it('render refresh button', () => {
jest.spyOn(eventHub, '$emit'); expect(findButton().text()).toBe('Refresh');
findButton().vm.$emit('click'); });
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested'); it('emits event and shows loading icon when button is clicked', async () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
return wrapper.vm.$nextTick(() => { expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); await nextTick();
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
}); });
}); });
}); });

View File

@ -201,6 +201,10 @@ describe('gfm_autocomplete/utils', () => {
expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / '))); expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / ')));
}); });
it('limits the items in the autocomplete menu to 10', () => {
expect(membersConfig.menuItemLimit).toBe(10);
});
it('shows the avatar, name and username in the menu item for a user', () => { it('shows the avatar, name and username in the menu item for a user', () => {
expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot(); expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot();
}); });

View File

@ -30,6 +30,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
conflicts auto_merge_enabled approved_by source_branch_protected conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
auto_merge_strategy merge_user
] ]
expect(described_class).to have_graphql_fields(*expected_fields).at_least expect(described_class).to have_graphql_fields(*expected_fields).at_least

View File

@ -433,6 +433,7 @@ RSpec.describe ProjectsHelper do
context 'when project has external wiki' do context 'when project has external wiki' do
it 'includes external wiki tab' do it 'includes external wiki tab' do
project.create_external_wiki_service(active: true, properties: { 'external_wiki_url' => 'https://gitlab.com' }) project.create_external_wiki_service(active: true, properties: { 'external_wiki_url' => 'https://gitlab.com' })
project.reload
is_expected.to include(:external_wiki) is_expected.to include(:external_wiki)
end end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupImporter do RSpec.describe BulkImports::Importers::GroupImporter do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:bulk_import) { create(:bulk_import) } let(:bulk_import) { create(:bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, bulk_import: bulk_import) } let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
let(:context) do let(:context) do
BulkImports::Pipeline::Context.new( BulkImports::Pipeline::Context.new(
@ -23,7 +23,6 @@ RSpec.describe BulkImports::Importers::GroupImporter do
describe '#execute' do describe '#execute' do
it 'starts the entity and run its pipelines' do it 'starts the entity and run its pipelines' do
expect(bulk_import_entity).to receive(:start!).and_call_original
expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee? expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context

View File

@ -1,36 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupsImporter do
let_it_be(:bulk_import) { create(:bulk_import) }
subject { described_class.new(bulk_import.id) }
describe '#execute' do
context "when there is entities to be imported" do
let!(:bulk_import_entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
it "starts the bulk_import and imports its entities" do
expect(BulkImports::Importers::GroupImporter).to receive(:new)
.with(bulk_import_entity).and_return(double(execute: true))
expect(BulkImportWorker).to receive(:perform_async).with(bulk_import.id)
subject.execute
expect(bulk_import.reload).to be_started
end
end
context "when there is no entities to be imported" do
it "starts the bulk_import and imports its entities" do
expect(BulkImports::Importers::GroupImporter).not_to receive(:new)
expect(BulkImportWorker).not_to receive(:perform_async)
subject.execute
expect(bulk_import.reload).to be_finished
end
end
end
end

View File

@ -7,6 +7,7 @@ RSpec.describe Gitlab::Database::Reindexing::GrafanaNotifier do
let(:api_key) { "foo" } let(:api_key) { "foo" }
let(:api_url) { "http://bar"} let(:api_url) { "http://bar"}
let(:additional_tag) { "some-tag" }
let(:action) { create(:reindex_action) } let(:action) { create(:reindex_action) }
@ -73,32 +74,66 @@ RSpec.describe Gitlab::Database::Reindexing::GrafanaNotifier do
end end
describe '#notify_start' do describe '#notify_start' do
subject { described_class.new(api_key, api_url).notify_start(action) } context 'additional tag is nil' do
subject { described_class.new(api_key, api_url, nil).notify_start(action) }
let(:payload) do let(:payload) do
{ {
time: (action.action_start.utc.to_f * 1000).to_i, time: (action.action_start.utc.to_f * 1000).to_i,
tags: ['reindex', action.index.tablename, action.index.name], tags: ['reindex', action.index.tablename, action.index.name],
text: "Started reindexing of #{action.index.name} on #{action.index.tablename}" text: "Started reindexing of #{action.index.name} on #{action.index.tablename}"
} }
end
it_behaves_like 'interacting with Grafana annotations API'
end end
it_behaves_like 'interacting with Grafana annotations API' context 'additional tag is not nil' do
subject { described_class.new(api_key, api_url, additional_tag).notify_start(action) }
let(:payload) do
{
time: (action.action_start.utc.to_f * 1000).to_i,
tags: ['reindex', additional_tag, action.index.tablename, action.index.name],
text: "Started reindexing of #{action.index.name} on #{action.index.tablename}"
}
end
it_behaves_like 'interacting with Grafana annotations API'
end
end end
describe '#notify_end' do describe '#notify_end' do
subject { described_class.new(api_key, api_url).notify_end(action) } context 'additional tag is nil' do
subject { described_class.new(api_key, api_url, nil).notify_end(action) }
let(:payload) do let(:payload) do
{ {
time: (action.action_start.utc.to_f * 1000).to_i, time: (action.action_start.utc.to_f * 1000).to_i,
tags: ['reindex', action.index.tablename, action.index.name], tags: ['reindex', action.index.tablename, action.index.name],
text: "Finished reindexing of #{action.index.name} on #{action.index.tablename} (#{action.state})", text: "Finished reindexing of #{action.index.name} on #{action.index.tablename} (#{action.state})",
timeEnd: (action.action_end.utc.to_f * 1000).to_i, timeEnd: (action.action_end.utc.to_f * 1000).to_i,
isRegion: true isRegion: true
} }
end
it_behaves_like 'interacting with Grafana annotations API'
end end
it_behaves_like 'interacting with Grafana annotations API' context 'additional tag is not nil' do
subject { described_class.new(api_key, api_url, additional_tag).notify_end(action) }
let(:payload) do
{
time: (action.action_start.utc.to_f * 1000).to_i,
tags: ['reindex', additional_tag, action.index.tablename, action.index.name],
text: "Finished reindexing of #{action.index.name} on #{action.index.tablename} (#{action.state})",
timeEnd: (action.action_end.utc.to_f * 1000).to_i,
isRegion: true
}
end
it_behaves_like 'interacting with Grafana annotations API'
end
end end
end end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReleaseHighlights::Validator::Entry do
subject(:entry) { described_class.new(document.root.children.first) }
let(:document) { YAML.parse(File.read(yaml_path)) }
let(:yaml_path) { 'spec/fixtures/whats_new/blank.yml' }
describe 'validations' do
before do
allow(entry).to receive(:value_for).and_call_original
end
context 'with a valid entry' do
let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' }
it { is_expected.to be_valid }
end
context 'with an invalid entry' do
let(:yaml_path) { 'spec/fixtures/whats_new/invalid.yml' }
it 'returns line numbers in errors' do
subject.valid?
expect(entry.errors[:packages].first).to match('(line 6)')
end
end
context 'with a blank entry' do
it 'validate presence of title, body and stage' do
subject.valid?
expect(subject.errors[:title]).not_to be_empty
expect(subject.errors[:body]).not_to be_empty
expect(subject.errors[:stage]).not_to be_empty
expect(subject.errors[:packages]).not_to be_empty
end
it 'validates boolean value of "self-managed" and "gitlab-com"' do
allow(entry).to receive(:value_for).with('self-managed').and_return('nope')
allow(entry).to receive(:value_for).with('gitlab-com').and_return('yerp')
subject.valid?
expect(subject.errors[:'self-managed']).to include(/must be a boolean/)
expect(subject.errors[:'gitlab-com']).to include(/must be a boolean/)
end
it 'validates URI of "url" and "image_url"' do
allow(entry).to receive(:value_for).with('image_url').and_return('imgur/gitlab_feature.gif')
allow(entry).to receive(:value_for).with('url').and_return('gitlab/newest_release.html')
subject.valid?
expect(subject.errors[:url]).to include(/must be a URL/)
expect(subject.errors[:image_url]).to include(/must be a URL/)
end
it 'validates release is numerical' do
allow(entry).to receive(:value_for).with('release').and_return('one')
subject.valid?
expect(subject.errors[:release]).to include(/is not a number/)
end
it 'validates published_at is a date' do
allow(entry).to receive(:value_for).with('published_at').and_return('christmas day')
subject.valid?
expect(subject.errors[:published_at]).to include(/must be valid Date/)
end
it 'validates packages are included in list' do
allow(entry).to receive(:value_for).with('packages').and_return(['ALL'])
subject.valid?
expect(subject.errors[:packages].first).to include("must be one of", "Core", "Starter", "Premium", "Ultimate")
end
end
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReleaseHighlights::Validator do
let(:validator) { described_class.new(file: yaml_path) }
let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' }
let(:invalid_yaml_path) { 'spec/fixtures/whats_new/invalid.yml' }
describe '#valid?' do
subject { validator.valid? }
context 'with a valid file' do
it 'passes entries to entry validator and returns true' do
expect(ReleaseHighlights::Validator::Entry).to receive(:new).exactly(:twice).and_call_original
expect(subject).to be true
expect(validator.errors).to be_empty
end
end
context 'with invalid file' do
let(:yaml_path) { invalid_yaml_path }
it 'returns false and has errors' do
expect(subject).to be false
expect(validator.errors).not_to be_empty
end
end
end
describe '.validate_all!' do
subject { described_class.validate_all! }
before do
allow(ReleaseHighlight).to receive(:file_paths).and_return(yaml_paths)
end
context 'with valid files' do
let(:yaml_paths) { [yaml_path, yaml_path] }
it { is_expected.to be true }
end
context 'with an invalid file' do
let(:yaml_paths) { [invalid_yaml_path, yaml_path] }
it { is_expected.to be false }
end
end
describe '.error_message' do
subject do
described_class.validate_all!
described_class.error_message
end
before do
allow(ReleaseHighlight).to receive(:file_paths).and_return([yaml_path])
end
context 'with a valid file' do
it { is_expected.to be_empty }
end
context 'with an invalid file' do
let(:yaml_path) { invalid_yaml_path }
it 'returns a nice error message' do
expect(subject).to eq(<<-MESSAGE.strip_heredoc)
---------------------------------------------------------
Validation failed for spec/fixtures/whats_new/invalid.yml
---------------------------------------------------------
* Packages must be one of ["Core", "Starter", "Premium", "Ultimate"] (line 6)
MESSAGE
end
end
end
describe 'when validating all files' do
it 'they should have no errors' do
expect(described_class.validate_all!).to be_truthy, described_class.error_message
end
end
end

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddHasExternalWikiTrigger do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:services) { table(:services) }
before do
@namespace = namespaces.create!(name: 'foo', path: 'foo')
@project = projects.create!(namespace_id: @namespace.id)
end
describe '#up' do
before do
migrate!
end
describe 'INSERT trigger' do
it 'sets `has_external_wiki` to true when active `ExternalWikiService` is inserted' do
expect do
services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
end.to change { @project.reload.has_external_wiki }.to(true)
end
it 'does not set `has_external_wiki` to true when service is for a different project' do
different_project = projects.create!(namespace_id: @namespace.id)
expect do
services.create!(type: 'ExternalWikiService', active: true, project_id: different_project.id)
end.not_to change { @project.reload.has_external_wiki }
end
it 'does not set `has_external_wiki` to true when inactive `ExternalWikiService` is inserted' do
expect do
services.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
end.not_to change { @project.reload.has_external_wiki }
end
it 'does not set `has_external_wiki` to true when active other service is inserted' do
expect do
services.create!(type: 'MyService', active: true, project_id: @project.id)
end.not_to change { @project.reload.has_external_wiki }
end
end
describe 'UPDATE trigger' do
it 'sets `has_external_wiki` to true when `ExternalWikiService` is made active' do
service = services.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
expect do
service.update!(active: true)
end.to change { @project.reload.has_external_wiki }.to(true)
end
it 'sets `has_external_wiki` to false when `ExternalWikiService` is made inactive' do
service = services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
expect do
service.update!(active: false)
end.to change { @project.reload.has_external_wiki }.to(false)
end
it 'does not change `has_external_wiki` when service is for a different project' do
different_project = projects.create!(namespace_id: @namespace.id)
service = services.create!(type: 'ExternalWikiService', active: false, project_id: different_project.id)
expect do
service.update!(active: true)
end.not_to change { @project.reload.has_external_wiki }
end
end
describe 'DELETE trigger' do
it 'sets `has_external_wiki` to false when `ExternalWikiService` is deleted' do
service = services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
expect do
service.delete
end.to change { @project.reload.has_external_wiki }.to(false)
end
it 'does not change `has_external_wiki` when service is for a different project' do
different_project = projects.create!(namespace_id: @namespace.id)
service = services.create!(type: 'ExternalWikiService', active: true, project_id: different_project.id)
expect do
service.delete
end.not_to change { @project.reload.has_external_wiki }
end
end
end
describe '#down' do
before do
migration.up
migration.down
end
it 'drops the INSERT trigger' do
expect do
services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
end.not_to change { @project.reload.has_external_wiki }
end
it 'drops the UPDATE trigger' do
service = services.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
@project.update!(has_external_wiki: false)
expect do
service.update!(active: true)
end.not_to change { @project.reload.has_external_wiki }
end
it 'drops the DELETE trigger' do
service = services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
@project.update!(has_external_wiki: true)
expect do
service.delete
end.not_to change { @project.reload.has_external_wiki }
end
end
end

View File

@ -33,4 +33,49 @@ RSpec.describe Namespace::PackageSetting do
end end
end end
end end
describe '#duplicates_allowed?' do
using RSpec::Parameterized::TableSyntax
subject { described_class.duplicates_allowed?(package) }
context 'package types with package_settings' do
# As more package types gain settings they will be added to this list
[:maven_package].each do |format|
let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
let_it_be(:package_type) { package.package_type }
let_it_be(:package_setting) { package.project.namespace.package_settings }
where(:duplicates_allowed, :duplicate_exception_regex, :result) do
true | '' | true
false | '' | false
false | '.*' | true
end
with_them do
context "for #{format}" do
before do
package_setting.update!(
"#{package_type}_duplicates_allowed" => duplicates_allowed,
"#{package_type}_duplicate_exception_regex" => duplicate_exception_regex
)
end
it { is_expected.to be(result) }
end
end
end
end
context 'package types without package_settings' do
[:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :generic_package, :golang_package, :debian_package].each do |format|
let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
let_it_be(:package_setting) { package.project.namespace.package_settings }
it 'raises an error' do
expect { subject }.to raise_error(Namespace::PackageSetting::PackageSettingNotImplemented)
end
end
end
end
end end

View File

@ -745,4 +745,14 @@ RSpec.describe Packages::Package, type: :model do
end end
end end
end end
describe '#package_settings' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:package) { create(:maven_package, project: project) }
it 'returns the namespace package_settings' do
expect(package.package_settings).to eq(group.package_settings)
end
end
end end

View File

@ -1067,36 +1067,6 @@ RSpec.describe Project, factory_default: :keep do
end end
end end
describe '#cache_has_external_wiki' do
let_it_be(:project) { create(:project, has_external_wiki: nil) }
it 'stores true if there is any external_wikis' do
services = double(:service, external_wikis: [ExternalWikiService.new])
expect(project).to receive(:services).and_return(services)
expect do
project.cache_has_external_wiki
end.to change { project.has_external_wiki}.to(true)
end
it 'stores false if there is no external_wikis' do
services = double(:service, external_wikis: [])
expect(project).to receive(:services).and_return(services)
expect do
project.cache_has_external_wiki
end.to change { project.has_external_wiki}.to(false)
end
it 'does not cache data when in a read-only GitLab instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect do
project.cache_has_external_wiki
end.not_to change { project.has_external_wiki }
end
end
describe '#has_wiki?' do describe '#has_wiki?' do
let(:no_wiki_project) { create(:project, :wiki_disabled, has_external_wiki: false) } let(:no_wiki_project) { create(:project, :wiki_disabled, has_external_wiki: false) }
let(:wiki_enabled_project) { create(:project) } let(:wiki_enabled_project) { create(:project) }
@ -1136,51 +1106,63 @@ RSpec.describe Project, factory_default: :keep do
describe '#external_wiki' do describe '#external_wiki' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
context 'with an active external wiki' do def subject
before do project.reload.external_wiki
end
it 'returns an active external wiki' do
create(:service, project: project, type: 'ExternalWikiService', active: true)
is_expected.to be_kind_of(ExternalWikiService)
end
it 'does not return an inactive external wiki' do
create(:service, project: project, type: 'ExternalWikiService', active: false)
is_expected.to eq(nil)
end
it 'sets Project#has_external_wiki when it is nil' do
create(:service, project: project, type: 'ExternalWikiService', active: true)
project.update_column(:has_external_wiki, nil)
expect { subject }.to change { project.has_external_wiki }.from(nil).to(true)
end
end
describe '#has_external_wiki' do
let_it_be(:project) { create(:project) }
def subject
project.reload.has_external_wiki
end
specify { is_expected.to eq(false) }
context 'when there is an active external wiki service' do
let!(:service) do
create(:service, project: project, type: 'ExternalWikiService', active: true) create(:service, project: project, type: 'ExternalWikiService', active: true)
project.external_wiki
end end
it 'sets :has_external_wiki as true' do specify { is_expected.to eq(true) }
expect(project.has_external_wiki).to be(true)
it 'becomes false if the external wiki service is destroyed' do
expect do
Service.find(service.id).delete
end.to change { subject }.to(false)
end end
it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do it 'becomes false if the external wiki service becomes inactive' do
expect(project.has_external_wiki).to be(true) expect do
service.update_column(:active, false)
project.services.external_wikis.first.destroy end.to change { subject }.to(false)
expect(project.has_external_wiki).to be(false)
end end
end end
context 'with an inactive external wiki' do it 'is false when external wiki service is not active' do
before do create(:service, project: project, type: 'ExternalWikiService', active: false)
create(:service, project: project, type: 'ExternalWikiService', active: false)
end
it 'sets :has_external_wiki as false' do is_expected.to eq(false)
expect(project.has_external_wiki).to be(false)
end
end
context 'with no external wiki' do
before do
project.external_wiki
end
it 'sets :has_external_wiki as false' do
expect(project.has_external_wiki).to be(false)
end
it 'sets :has_external_wiki as true if an external wiki service is created later' do
expect(project.has_external_wiki).to be(false)
create(:service, project: project, type: 'ExternalWikiService', active: true)
expect(project.has_external_wiki).to be(true)
end
end end
end end

View File

@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ReleaseHighlight do RSpec.describe ReleaseHighlight do
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*\_(\d*\_\d*)\.yml$/) }
before do before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)

View File

@ -53,17 +53,6 @@ RSpec.describe API::Boards do
end end
end end
describe "PUT /projects/:id/boards/:board_id" do
let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" }
it 'updates the issue board' do
put api(url, user), params: { name: 'changed board name' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('changed board name')
end
end
describe "DELETE /projects/:id/boards/:board_id" do describe "DELETE /projects/:id/boards/:board_id" do
let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" } let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" }

View File

@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe API::MavenPackages do RSpec.describe API::MavenPackages do
include WorkhorseHelpers include WorkhorseHelpers
let_it_be(:group) { create(:group) } let_it_be_with_refind(:package_settings) { create(:namespace_package_setting, :group) }
let_it_be(:group) { package_settings.namespace }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
let_it_be(:package, reload: true) { create(:maven_package, project: project, name: project.full_path) } let_it_be(:package, reload: true) { create(:maven_package, project: project, name: project.full_path) }
@ -18,6 +19,7 @@ RSpec.describe API::MavenPackages do
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) } let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
let(:package_name) { 'com/example/my-app' }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) } let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
@ -669,6 +671,35 @@ RSpec.describe API::MavenPackages do
end end
end end
context 'when package duplicates are not allowed' do
let(:package_name) { package.name }
let(:version) { package.version }
before do
package_settings.update!(maven_duplicates_allowed: false)
end
it 'rejects the request', :aggregate_failures do
expect { upload_file_with_token(params: params) }.not_to change { package.package_files.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include('Duplicate package is not allowed')
end
context 'when the package name matches the exception regex' do
before do
package_settings.update!(maven_duplicate_exception_regex: '.*')
end
it 'stores the package file', :aggregate_failures do
expect { upload_file_with_token(params: params) }.to change { package.package_files.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
expect(jar_file.file_name).to eq(file_upload.original_filename)
end
end
end
context 'for sha1 file' do context 'for sha1 file' do
let(:dummy_package) { double(Packages::Package) } let(:dummy_package) { double(Packages::Package) }
@ -698,7 +729,7 @@ RSpec.describe API::MavenPackages do
end end
def upload_file(params: {}, request_headers: headers, file_extension: 'jar') def upload_file(params: {}, request_headers: headers, file_extension: 'jar')
url = "/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/my-app-1.0-20180724.124855-1.#{file_extension}" url = "/projects/#{project.id}/packages/maven/#{package_name}/#{version}/my-app-1.0-20180724.124855-1.#{file_extension}"
workhorse_finalize( workhorse_finalize(
api(url), api(url),
method: :put, method: :put,

View File

@ -11,29 +11,36 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
let(:file_name) { 'test.jar' } let(:file_name) { 'test.jar' }
let(:param_path) { "#{path}/#{version}" } let(:param_path) { "#{path}/#{version}" }
let(:params) { { path: param_path, file_name: file_name } } let(:params) { { path: param_path, file_name: file_name } }
let(:service) { described_class.new(project, user, params) }
describe '#execute' do describe '#execute' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
subject { described_class.new(project, user, params).execute } subject { service.execute }
RSpec.shared_examples 'reuse existing package' do shared_examples 'reuse existing package' do
it { expect { subject}.not_to change { Packages::Package.count } } it { expect { subject }.not_to change { Packages::Package.count } }
it { is_expected.to eq(existing_package) } it 'returns the existing package' do
expect(subject.payload).to eq(package: existing_package)
end
end end
RSpec.shared_examples 'create package' do shared_examples 'create package' do
it { expect { subject }.to change { Packages::Package.count }.by(1) } it { expect { subject }.to change { Packages::Package.count }.by(1) }
it 'sets the proper name and version' do it 'sets the proper name and version', :aggregate_failures do
pkg = subject pkg = subject.payload[:package]
expect(pkg.name).to eq(path) expect(pkg.name).to eq(path)
expect(pkg.version).to eq(version) expect(pkg.version).to eq(version)
end end
it_behaves_like 'assigns build to package' context 'with a build' do
subject { service.execute.payload[:package] }
it_behaves_like 'assigns build to package'
end
end end
context 'path with version' do context 'path with version' do
@ -90,5 +97,27 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
expect { subject }.to change { Packages::BuildInfo.count }.by(1) expect { subject }.to change { Packages::BuildInfo.count }.by(1)
end end
end end
context 'when package duplicates are not allowed' do
let_it_be_with_refind(:package_settings) { create(:namespace_package_setting, :group, maven_duplicates_allowed: false) }
let_it_be_with_refind(:group) { package_settings.namespace }
let_it_be_with_refind(:project) { create(:project, group: group) }
let!(:existing_package) { create(:maven_package, name: path, version: version, project: project) }
it { expect { subject }.not_to change { project.package_files.count } }
it 'returns an error', :aggregate_failures do
expect(subject.payload).to be_empty
expect(subject.errors).to include('Duplicate package is not allowed')
end
context 'when the package name matches the exception regex' do
before do
package_settings.update!(maven_duplicate_exception_regex: '.*')
end
it_behaves_like 'reuse existing package'
end
end
end end
end end

View File

@ -44,16 +44,35 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals
expect_schema_match_for(response, 'public_api/v4/boards', ee) expect_schema_match_for(response, 'public_api/v4/boards', ee)
end end
end
end
describe "GET #{route_definition}/:board_id" do describe "GET #{route_definition}/:board_id" do
let(:url) { "#{root_url}/#{board.id}" } let(:url) { "#{root_url}/#{board.id}" }
it 'get a single board by id' do it 'get a single board by id' do
get api(url, user) get api(url, user)
expect_schema_match_for(response, 'public_api/v4/board', ee) expect_schema_match_for(response, 'public_api/v4/board', ee)
end end
end end
describe "PUT #{route_definition}/:board_id" do
let(:url) { "#{root_url}/#{board.id}" }
it 'updates the board name' do
put api(url, user), params: { name: 'changed board name' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('changed board name')
end
it 'updates the issue board booleans' do
put api(url, user), params: { hide_backlog_list: true, hide_closed_list: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['hide_backlog_list']).to eq(true)
expect(json_response['hide_closed_list']).to eq(true)
end end
end end

View File

@ -4,13 +4,74 @@ require 'spec_helper'
RSpec.describe BulkImportWorker do RSpec.describe BulkImportWorker do
describe '#perform' do describe '#perform' do
it 'executes Group Importer' do before do
bulk_import_id = 1 stub_const("#{described_class}::DEFAULT_BATCH_SIZE", 1)
end
expect(BulkImports::Importers::GroupsImporter) context 'when no bulk import is found' do
.to receive(:new).with(bulk_import_id).and_return(double(execute: true)) it 'does nothing' do
expect(described_class).not_to receive(:perform_in)
described_class.new.perform(bulk_import_id) subject.perform(non_existing_record_id)
end
end
context 'when bulk import is finished' do
it 'does nothing' do
bulk_import = create(:bulk_import, :finished)
expect(described_class).not_to receive(:perform_in)
subject.perform(bulk_import.id)
end
end
context 'when all entities are processed' do
it 'marks bulk import as finished' do
bulk_import = create(:bulk_import, :started)
create(:bulk_import_entity, :finished, bulk_import: bulk_import)
create(:bulk_import_entity, :failed, bulk_import: bulk_import)
subject.perform(bulk_import.id)
expect(bulk_import.reload.finished?).to eq(true)
end
end
context 'when maximum allowed number of import entities in progress' do
it 'reenqueues itself' do
bulk_import = create(:bulk_import, :started)
(described_class::DEFAULT_BATCH_SIZE + 1).times { |_| create(:bulk_import_entity, :started, bulk_import: bulk_import) }
expect(described_class).to receive(:perform_in).with(described_class::PERFORM_DELAY, bulk_import.id)
subject.perform(bulk_import.id)
end
end
context 'when bulk import is created' do
it 'marks bulk import as started' do
bulk_import = create(:bulk_import, :created)
create(:bulk_import_entity, :created, bulk_import: bulk_import)
subject.perform(bulk_import.id)
expect(bulk_import.reload.started?).to eq(true)
end
context 'when there are created entities to process' do
it 'marks a batch of entities as started, enqueues BulkImports::EntityWorker and reenqueues' do
bulk_import = create(:bulk_import, :created)
(described_class::DEFAULT_BATCH_SIZE + 1).times { |_| create(:bulk_import_entity, :created, bulk_import: bulk_import) }
expect(described_class).to receive(:perform_in).with(described_class::PERFORM_DELAY, bulk_import.id)
expect(BulkImports::EntityWorker).to receive(:perform_async)
subject.perform(bulk_import.id)
expect(bulk_import.entities.map(&:status_name)).to contain_exactly(:created, :started)
end
end
end end
end end
end end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::EntityWorker do
describe '#execute' do
let(:bulk_import) { create(:bulk_import) }
context 'when started entity exists' do
let(:entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
it 'executes BulkImports::Importers::GroupImporter' do
expect(BulkImports::Importers::GroupImporter).to receive(:new).with(entity).and_call_original
subject.perform(entity.id)
end
it 'sets jid' do
jid = 'jid'
allow(subject).to receive(:jid).and_return(jid)
subject.perform(entity.id)
expect(entity.reload.jid).to eq(jid)
end
end
context 'when started entity does not exist' do
it 'does not execute BulkImports::Importers::GroupImporter' do
entity = create(:bulk_import_entity, bulk_import: bulk_import)
expect(BulkImports::Importers::GroupImporter).not_to receive(:new)
subject.perform(entity.id)
end
end
end
end