Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									9c07ab8c69
								
							
						
					
					
						commit
						bbfd13e575
					
				|  | @ -11,6 +11,11 @@ export default { | |||
|     BoardListHeader, | ||||
|     BoardList, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|  | @ -27,11 +32,6 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       detailIssue: boardsStore.detail, | ||||
|  |  | |||
|  | @ -9,6 +9,11 @@ export default { | |||
|     BoardListHeader, | ||||
|     BoardList, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|  | @ -25,11 +30,6 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(['filterParams']), | ||||
|     ...mapGetters(['getIssuesByList']), | ||||
|  |  | |||
|  | @ -49,6 +49,14 @@ export default { | |||
|     GlModal, | ||||
|     BoardConfigurationOptions, | ||||
|   }, | ||||
|   inject: { | ||||
|     fullPath: { | ||||
|       default: '', | ||||
|     }, | ||||
|     rootPath: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     canAdminBoard: { | ||||
|       type: Boolean, | ||||
|  | @ -92,14 +100,6 @@ export default { | |||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   inject: { | ||||
|     fullPath: { | ||||
|       default: '', | ||||
|     }, | ||||
|     rootPath: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       board: { ...boardDefaults, ...this.currentBoard }, | ||||
|  |  | |||
|  | @ -31,6 +31,11 @@ export default { | |||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|  | @ -47,11 +52,6 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       weightFeatureAvailable: false, | ||||
|  |  | |||
|  | @ -37,6 +37,20 @@ export default { | |||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|     weightFeatureAvailable: { | ||||
|       default: false, | ||||
|     }, | ||||
|     scopedLabelsAvailable: { | ||||
|       default: false, | ||||
|     }, | ||||
|     currentUserId: { | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|  | @ -53,20 +67,6 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: { | ||||
|     boardId: { | ||||
|       default: '', | ||||
|     }, | ||||
|     weightFeatureAvailable: { | ||||
|       default: false, | ||||
|     }, | ||||
|     scopedLabelsAvailable: { | ||||
|       default: false, | ||||
|     }, | ||||
|     currentUserId: { | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(['activeId']), | ||||
|     isLoggedIn() { | ||||
|  |  | |||
|  | @ -16,13 +16,13 @@ export default { | |||
|     GlButton, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagMixin()], | ||||
|   inject: ['groupId'], | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['groupId'], | ||||
|   data() { | ||||
|     return { | ||||
|       title: '', | ||||
|  |  | |||
|  | @ -18,13 +18,13 @@ export default { | |||
|     GlButton, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagMixin()], | ||||
|   inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'], | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'], | ||||
|   data() { | ||||
|     return { | ||||
|       title: '', | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ export default { | |||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   mixins: [issueCardInner], | ||||
|   inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'], | ||||
|   props: { | ||||
|     issue: { | ||||
|       type: Object, | ||||
|  | @ -43,7 +44,6 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'], | ||||
|   data() { | ||||
|     return { | ||||
|       limitBeforeCounter: 2, | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ export default { | |||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   mixins: [issueCardInner], | ||||
|   inject: ['groupId', 'rootPath'], | ||||
|   props: { | ||||
|     issue: { | ||||
|       type: Object, | ||||
|  | @ -41,7 +42,6 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['groupId', 'rootPath'], | ||||
|   data() { | ||||
|     return { | ||||
|       limitBeforeCounter: 2, | ||||
|  |  | |||
|  | @ -11,13 +11,13 @@ export default { | |||
|     GlIcon, | ||||
|     GlTooltip, | ||||
|   }, | ||||
|   inject: ['timeTrackingLimitToHours'], | ||||
|   props: { | ||||
|     estimate: { | ||||
|       type: Number, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['timeTrackingLimitToHours'], | ||||
|   computed: { | ||||
|     title() { | ||||
|       return stringifyTime( | ||||
|  |  | |||
|  | @ -33,13 +33,13 @@ export default { | |||
|     GlDropdownText, | ||||
|     GlSearchBoxByType, | ||||
|   }, | ||||
|   inject: ['groupId'], | ||||
|   props: { | ||||
|     list: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['groupId'], | ||||
|   data() { | ||||
|     return { | ||||
|       initialLoading: true, | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; | |||
| 
 | ||||
| export default { | ||||
|   components: { GlButton, GlLoadingIcon }, | ||||
|   inject: ['canUpdate'], | ||||
|   props: { | ||||
|     title: { | ||||
|       type: String, | ||||
|  | @ -25,7 +26,6 @@ export default { | |||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['canUpdate'], | ||||
|   data() { | ||||
|     return { | ||||
|       edit: false, | ||||
|  |  | |||
|  | @ -14,12 +14,12 @@ export default { | |||
|     LabelsSelect, | ||||
|     GlLabel, | ||||
|   }, | ||||
|   inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], | ||||
|   data() { | ||||
|     return { | ||||
|       loading: false, | ||||
|     }; | ||||
|   }, | ||||
|   inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], | ||||
|   computed: { | ||||
|     ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), | ||||
|     selectedLabels() { | ||||
|  |  | |||
|  | @ -18,6 +18,10 @@ export default (params = {}) => { | |||
|       BoardsSelector, | ||||
|     }, | ||||
|     apolloProvider, | ||||
|     provide: { | ||||
|       fullPath: params.fullPath, | ||||
|       rootPath: params.rootPath, | ||||
|     }, | ||||
|     data() { | ||||
|       const { dataset } = boardsSwitcherElement; | ||||
| 
 | ||||
|  | @ -35,10 +39,6 @@ export default (params = {}) => { | |||
| 
 | ||||
|       return { boardsSelectorProps }; | ||||
|     }, | ||||
|     provide: { | ||||
|       fullPath: params.fullPath, | ||||
|       rootPath: params.rootPath, | ||||
|     }, | ||||
|     render(createElement) { | ||||
|       return createElement(BoardsSelector, { | ||||
|         props: this.boardsSelectorProps, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
| import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui'; | ||||
| import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui'; | ||||
| import { __, s__, sprintf } from '~/locale'; | ||||
| import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; | ||||
| import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
|  | @ -28,12 +28,13 @@ const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; | |||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     CommitForm, | ||||
|     CiLint, | ||||
|     CommitForm, | ||||
|     EditorTab, | ||||
|     GlAlert, | ||||
|     GlLoadingIcon, | ||||
|     GlTabs, | ||||
|     GlTab, | ||||
|     PipelineGraph, | ||||
|     TextEditor, | ||||
|     ValidationSegment, | ||||
|  | @ -317,16 +318,15 @@ export default { | |||
|               :commit-sha="lastCommitSha" | ||||
|             /> | ||||
|           </editor-tab> | ||||
|           <editor-tab | ||||
|           <gl-tab | ||||
|             v-if="glFeatures.ciConfigVisualizationTab" | ||||
|             :lazy="true" | ||||
|             :title="$options.i18n.tabGraph" | ||||
|             :title-link-attributes="{ 'data-testid': 'visualization-tab-btn' }" | ||||
|             data-testid="visualization-tab" | ||||
|           > | ||||
|             <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> | ||||
|             <pipeline-graph v-else :pipeline-data="ciConfigData" /> | ||||
|           </editor-tab> | ||||
|           </gl-tab> | ||||
| 
 | ||||
|           <editor-tab :title="$options.i18n.tabLint"> | ||||
|             <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> | ||||
|  |  | |||
|  | @ -1,21 +1,37 @@ | |||
| <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 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 statusIcon from '../mr_widget_status_icon.vue'; | ||||
| import MrWidgetAuthor from '../mr_widget_author.vue'; | ||||
| import eventHub from '../../event_hub'; | ||||
| import { AUTO_MERGE_STRATEGIES } from '../../constants'; | ||||
| import { __ } from '~/locale'; | ||||
| import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'MRWidgetAutoMergeEnabled', | ||||
|   apollo: { | ||||
|     state: { | ||||
|       query: autoMergeEnabledQuery, | ||||
|       skip() { | ||||
|         return !this.glFeatures.mergeRequestWidgetGraphql; | ||||
|       }, | ||||
|       variables() { | ||||
|         return this.mergeRequestQueryVariables; | ||||
|       }, | ||||
|       update: (data) => data.project?.mergeRequest, | ||||
|     }, | ||||
|   }, | ||||
|   components: { | ||||
|     MrWidgetAuthor, | ||||
|     statusIcon, | ||||
|     GlLoadingIcon, | ||||
|     GlSkeletonLoader, | ||||
|   }, | ||||
|   mixins: [autoMergeMixin], | ||||
|   mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], | ||||
|   props: { | ||||
|     mr: { | ||||
|       type: Object, | ||||
|  | @ -30,20 +46,47 @@ export default { | |||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       state: {}, | ||||
|       isCancellingAutoMerge: false, | ||||
|       isRemovingSourceBranch: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     canRemoveSourceBranch() { | ||||
|       const { | ||||
|         shouldRemoveSourceBranch, | ||||
|         canRemoveSourceBranch, | ||||
|         mergeUserId, | ||||
|         currentUserId, | ||||
|       } = this.mr; | ||||
|     loading() { | ||||
|       return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading; | ||||
|     }, | ||||
|     mergeUser() { | ||||
|       if (this.glFeatures.mergeRequestWidgetGraphql) { | ||||
|         return this.state.mergeUser; | ||||
|       } | ||||
| 
 | ||||
|       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: { | ||||
|  | @ -63,7 +106,7 @@ export default { | |||
|     removeSourceBranch() { | ||||
|       const options = { | ||||
|         sha: this.mr.sha, | ||||
|         auto_merge_strategy: this.mr.autoMergeStrategy, | ||||
|         auto_merge_strategy: this.autoMergeStrategy, | ||||
|         should_remove_source_branch: true, | ||||
|       }; | ||||
| 
 | ||||
|  | @ -86,49 +129,64 @@ export default { | |||
| </script> | ||||
| <template> | ||||
|   <div class="mr-widget-body media"> | ||||
|     <status-icon status="success" /> | ||||
|     <div class="media-body"> | ||||
|       <h4 class="d-flex align-items-start"> | ||||
|         <span class="gl-mr-3"> | ||||
|           <span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span> | ||||
|           <mr-widget-author :author="mr.setToAutoMergeBy" /> | ||||
|           <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 v-if="loading" class="gl-w-full mr-conflict-loader"> | ||||
|       <gl-skeleton-loader :width="334" :height="30"> | ||||
|         <rect x="0" y="3" width="24" height="24" rx="4" /> | ||||
|         <rect x="32" y="7" width="150" height="16" rx="4" /> | ||||
|         <rect x="190" y="7" width="144" height="16" rx="4" /> | ||||
|       </gl-skeleton-loader> | ||||
|     </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> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,7 +1,10 @@ | |||
| <script> | ||||
| import { GlLoadingIcon, GlButton } from '@gitlab/ui'; | ||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
| import eventHub from '../../event_hub'; | ||||
| 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 { | ||||
|   name: 'MRWidgetAutoMergeFailed', | ||||
|  | @ -10,6 +13,19 @@ export default { | |||
|     GlLoadingIcon, | ||||
|     GlButton, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], | ||||
|   apollo: { | ||||
|     mergeError: { | ||||
|       query: autoMergeFailedQuery, | ||||
|       skip() { | ||||
|         return !this.glFeatures.mergeRequestWidgetGraphql; | ||||
|       }, | ||||
|       variables() { | ||||
|         return this.mergeRequestQueryVariables; | ||||
|       }, | ||||
|       update: (data) => data.project?.mergeRequest?.mergeError, | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     mr: { | ||||
|       type: Object, | ||||
|  | @ -18,6 +34,7 @@ export default { | |||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError, | ||||
|       isRefreshing: false, | ||||
|     }; | ||||
|   }, | ||||
|  | @ -36,7 +53,7 @@ export default { | |||
|     <status-icon status="warning" /> | ||||
|     <div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center"> | ||||
|       <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') }} | ||||
|       </span> | ||||
|       <gl-button | ||||
|  |  | |||
|  | @ -0,0 +1,15 @@ | |||
| fragment autoMergeEnabled on MergeRequest { | ||||
|   autoMergeStrategy | ||||
|   mergeUser { | ||||
|     name | ||||
|     username | ||||
|     webUrl | ||||
|     avatarUrl | ||||
|   } | ||||
|   targetBranch | ||||
|   shouldRemoveSourceBranch | ||||
|   forceRemoveSourceBranch | ||||
|   userPermissions { | ||||
|     removeSourceBranch | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| #import "./auto_merge_enabled.fragment.graphql" | ||||
| 
 | ||||
| query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) { | ||||
|   project(fullPath: $projectPath) { | ||||
|     mergeRequest(iid: $iid) { | ||||
|       ...autoMergeEnabled | ||||
|       mergeTrainsCount | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| query autoMergeFailedQuery($projectPath: ID!, $iid: String!) { | ||||
|   project(fullPath: $projectPath) { | ||||
|     mergeRequest(iid: $iid) { | ||||
|       mergeError | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -4,6 +4,9 @@ import { spriteIcon } from '~/lib/utils/common_utils'; | |||
| 
 | ||||
| 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+$/; | ||||
| 
 | ||||
| export const GfmAutocompleteType = { | ||||
|  | @ -74,6 +77,7 @@ export const tributeConfig = { | |||
|       fillAttr: 'username', | ||||
|       lookup: (value) => | ||||
|         value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`, | ||||
|       menuItemLimit: memberLimit, | ||||
|       menuItemTemplate: ({ original }) => { | ||||
|         const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0'; | ||||
|         const noAvatarClasses = `${commonClasses} gl-rounded-small
 | ||||
|  |  | |||
|  | @ -5,6 +5,10 @@ | |||
|     min-width: auto; | ||||
|   } | ||||
| 
 | ||||
|   .filtered-search-box .form-control { | ||||
|     min-width: unset; | ||||
|   } | ||||
| 
 | ||||
|   .sort-control { | ||||
|     .btn { | ||||
|       padding-right: 2rem; | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ module Types | |||
|       graphql_name 'AlertManagementDomainFilter' | ||||
|       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' | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -175,6 +175,10 @@ module Types | |||
|           calls_gitaly: true, description: 'Merge request commits excluding merge commits' | ||||
|     field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true, | ||||
|           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 | ||||
|       object.approved_by_users | ||||
|  |  | |||
|  | @ -4,9 +4,25 @@ class Namespace::PackageSetting < ApplicationRecord | |||
|   self.primary_key = :namespace_id | ||||
|   self.table_name = 'namespace_package_settings' | ||||
| 
 | ||||
|   PackageSettingNotImplemented = Class.new(StandardError) | ||||
| 
 | ||||
|   PACKAGES_WITH_SETTINGS = %w[maven].freeze | ||||
| 
 | ||||
|   belongs_to :namespace, inverse_of: :package_setting_relation | ||||
| 
 | ||||
|   validates :namespace, presence: true | ||||
|   validates :maven_duplicates_allowed, inclusion: { in: [true, false] } | ||||
|   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 | ||||
|  |  | |||
|  | @ -200,6 +200,12 @@ class Packages::Package < ApplicationRecord | |||
|     debian? && !version.nil? | ||||
|   end | ||||
| 
 | ||||
|   def package_settings | ||||
|     strong_memoize(:package_settings) do | ||||
|       project.namespace.package_settings | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def composer_tag_version? | ||||
|  |  | |||
|  | @ -1333,19 +1333,11 @@ class Project < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   def external_wiki | ||||
|     if has_external_wiki.nil? | ||||
|       cache_has_external_wiki | ||||
|     end | ||||
|     cache_has_external_wiki if has_external_wiki.nil? | ||||
| 
 | ||||
|     if has_external_wiki | ||||
|       @external_wiki ||= services.external_wikis.first | ||||
|     else | ||||
|       nil | ||||
|     end | ||||
|   end | ||||
|     return unless has_external_wiki? | ||||
| 
 | ||||
|   def cache_has_external_wiki | ||||
|     update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? | ||||
|     @external_wiki ||= services.external_wikis.first | ||||
|   end | ||||
| 
 | ||||
|   def find_or_initialize_services | ||||
|  | @ -2707,6 +2699,10 @@ class Project < ApplicationRecord | |||
|       objects.each_batch { |relation| out.concat(relation.pluck(:oid)) } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def cache_has_external_wiki | ||||
|     update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| Project.prepend_if_ee('EE::Project') | ||||
|  |  | |||
|  | @ -48,7 +48,6 @@ class Service < ApplicationRecord | |||
| 
 | ||||
|   after_commit :reset_updated_properties | ||||
|   after_commit :cache_project_has_external_issue_tracker | ||||
|   after_commit :cache_project_has_external_wiki | ||||
| 
 | ||||
|   belongs_to :project, inverse_of: :services | ||||
|   belongs_to :group, inverse_of: :services | ||||
|  | @ -469,12 +468,6 @@ class Service < ApplicationRecord | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def cache_project_has_external_wiki | ||||
|     if project && !project.destroyed? | ||||
|       project.cache_has_external_wiki | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def valid_recipients? | ||||
|     activated? && !importing? | ||||
|   end | ||||
|  |  | |||
|  | @ -38,10 +38,6 @@ class BulkCreateIntegrationService | |||
|     if integration.external_issue_tracker? | ||||
|       Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true) | ||||
|     end | ||||
| 
 | ||||
|     if integration.external_wiki? | ||||
|       Project.where(id: batch.select(:id)).update_all(has_external_wiki: true) | ||||
|     end | ||||
|   end | ||||
|   # rubocop: enable CodeReuse/ActiveRecord | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,6 +10,10 @@ module Packages | |||
|           ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project) | ||||
|                                           .execute | ||||
| 
 | ||||
|         unless Namespace::PackageSetting.duplicates_allowed?(package) | ||||
|           return ServiceResponse.error(message: 'Duplicate package is not allowed') | ||||
|         end | ||||
| 
 | ||||
|         unless package | ||||
|           # Maven uploads several files during `mvn deploy` in next order: | ||||
|           #   - 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 | ||||
|         ServiceResponse.success(payload: { package: package }) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| %li{ class: "branch-item js-branch-item js-branch-#{branch.name}", data: { name: branch.name } } | ||||
|   .branch-info | ||||
|     .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 | ||||
|         = branch.name | ||||
|       - if branch.name == @repository.root_ref | ||||
|  |  | |||
|  | @ -1433,6 +1433,14 @@ | |||
|   :idempotent: | ||||
|   :tags: [] | ||||
| - :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 | ||||
|   :has_external_dependencies: true | ||||
|   :urgency: :low | ||||
|  |  | |||
|  | @ -7,9 +7,58 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker | |||
| 
 | ||||
|   sidekiq_options retry: false, dead: false | ||||
| 
 | ||||
|   worker_has_external_dependencies! | ||||
|   PERFORM_DELAY = 5.seconds | ||||
|   DEFAULT_BATCH_SIZE = 5 | ||||
| 
 | ||||
|   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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Check namespace package settings when creating Maven packages | ||||
| merge_request: 50691 | ||||
| author: | ||||
| type: changed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Fix mobile layout Error Tracking details page | ||||
| merge_request: 50970 | ||||
| author: Kev @KevSlashNull | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add PostgreSQL trigger to maintain projects.has_external_wiki | ||||
| merge_request: 49916 | ||||
| author: | ||||
| type: changed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Fix fork icon shrinks if branch name is very long | ||||
| merge_request: 50915 | ||||
| author: Kev @KevSlashNull | ||||
| type: fixed | ||||
|  | @ -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 | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Move Group Migration entities import to individual sidekiq jobs | ||||
| merge_request: 50781 | ||||
| author: | ||||
| type: changed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Skip secret_detection on tags | ||||
| merge_request: 51129 | ||||
| author: | ||||
| type: changed | ||||
|  | @ -50,6 +50,8 @@ | |||
|   - 1 | ||||
| - - bulk_import | ||||
|   - 1 | ||||
| - - bulk_imports_entity | ||||
|   - 1 | ||||
| - - chaos | ||||
|   - 2 | ||||
| - - chat_notification | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ | |||
|   stage: Release | ||||
|   self-managed: true | ||||
|   gitlab-com: true | ||||
|   packages: [starter, premium, ultimate] | ||||
|   packages: [Starter, Premium, Ultimate] | ||||
|   url: https://www.youtube.com/embed/1FBRaBQTQZk | ||||
|   image_url: https://img.youtube.com/vi/1FBRaBQTQZk/hqdefault.jpg | ||||
|   published_at: 2020-09-22 | ||||
|  |  | |||
|  | @ -47,4 +47,5 @@ | |||
|   stage: Verify | ||||
|   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. | ||||
| 
 | ||||
|   published_at: 2020-12-22 | ||||
|   release: 13.7 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -0,0 +1 @@ | |||
| db23b5315386ad5d5fec5a14958769cc1e62a0a89ec3246edb9fc024607e917b | ||||
|  | @ -10,6 +10,17 @@ CREATE EXTENSION IF NOT EXISTS btree_gist; | |||
| 
 | ||||
| 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 | ||||
|     LANGUAGE plpgsql | ||||
|     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 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 | ||||
|     ADD CONSTRAINT fk_00797a2bf9 FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE; | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ before/after the brackets. Also, some shells (for example, `zsh`) can interpret | |||
| 
 | ||||
| ## 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. | ||||
| 
 | ||||
| ## Importing multiple projects | ||||
|  |  | |||
|  | @ -121,6 +121,9 @@ to the default installation: | |||
| - Enable zero-downtime upgrades. | ||||
| - 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)** | ||||
| 
 | ||||
| > - Level of complexity: **Medium** | ||||
|  |  | |||
|  | @ -157,7 +157,7 @@ See current settings with: | |||
| 
 | ||||
| ```shell | ||||
| 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 ;" | ||||
| ``` | ||||
| 
 | ||||
|  | @ -165,9 +165,19 @@ It may take a little while to respond. | |||
| 
 | ||||
| ```ruby | ||||
| {"statement_timeout"=>"1min"} | ||||
| {"lock_timeout"=>"0"} | ||||
| {"deadlock_timeout"=>"0"} | ||||
| {"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: | ||||
| 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. | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ documentation for some popular browsers. | |||
| - [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/) | ||||
| - [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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -600,7 +600,7 @@ Filters the alerts based on given domain | |||
| """ | ||||
| enum AlertManagementDomainFilter { | ||||
|   """ | ||||
|   Alerts for operations domain  | ||||
|   Alerts for operations domain | ||||
|   """ | ||||
|   operations | ||||
| 
 | ||||
|  | @ -13836,6 +13836,11 @@ type MergeRequest implements CurrentUserTodos & Noteable { | |||
|   """ | ||||
|   autoMergeEnabled: Boolean! | ||||
| 
 | ||||
|   """ | ||||
|   Selected auto merge strategy | ||||
|   """ | ||||
|   autoMergeStrategy: String | ||||
| 
 | ||||
|   """ | ||||
|   Array of available auto merge strategies | ||||
|   """ | ||||
|  | @ -14075,6 +14080,11 @@ type MergeRequest implements CurrentUserTodos & Noteable { | |||
|   """ | ||||
|   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) | ||||
|   """ | ||||
|  |  | |||
|  | @ -1500,7 +1500,7 @@ | |||
|           "enumValues": [ | ||||
|             { | ||||
|               "name": "operations", | ||||
|               "description": "Alerts for operations domain ", | ||||
|               "description": "Alerts for operations domain", | ||||
|               "isDeprecated": false, | ||||
|               "deprecationReason": null | ||||
|             }, | ||||
|  | @ -37994,6 +37994,20 @@ | |||
|               "isDeprecated": false, | ||||
|               "deprecationReason": null | ||||
|             }, | ||||
|             { | ||||
|               "name": "autoMergeStrategy", | ||||
|               "description": "Selected auto merge strategy", | ||||
|               "args": [ | ||||
| 
 | ||||
|               ], | ||||
|               "type": { | ||||
|                 "kind": "SCALAR", | ||||
|                 "name": "String", | ||||
|                 "ofType": null | ||||
|               }, | ||||
|               "isDeprecated": false, | ||||
|               "deprecationReason": null | ||||
|             }, | ||||
|             { | ||||
|               "name": "availableAutoMergeStrategies", | ||||
|               "description": "Array of available auto merge strategies", | ||||
|  | @ -38645,6 +38659,20 @@ | |||
|               "isDeprecated": false, | ||||
|               "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", | ||||
|               "description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)", | ||||
|  |  | |||
|  | @ -2095,6 +2095,7 @@ Autogenerated return type of MarkAsSpamSnippet. | |||
| | `assignees` | UserConnection | Assignees of the merge request | | ||||
| | `author` | User | User who created this 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 | | ||||
| | `commitCount` | Int | Number of commits in the merge request | | ||||
| | `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 | | ||||
| | `mergeStatus` | String | Status of the merge request | | ||||
| | `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) | | ||||
| | `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 | | ||||
|  | @ -4162,7 +4164,7 @@ Filters the alerts based on given domain. | |||
| 
 | ||||
| | Value | Description | | ||||
| | ----- | ----------- | | ||||
| | `operations` | Alerts for operations domain  | | ||||
| | `operations` | Alerts for operations domain | | ||||
| | `threat_monitoring` | Alerts for threat monitoring domain | | ||||
| 
 | ||||
| ### AlertManagementIntegrationType | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  | @ -289,15 +289,17 @@ Updates a Group Issue Board. | |||
| PUT /groups/:id/boards/:board_id | ||||
| ``` | ||||
| 
 | ||||
| | 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 | | ||||
| | `board_id`          | integer        | yes      | The ID of a board | | ||||
| | `name`              | string         | no       | The new name of the board | | ||||
| | `assignee_id`       | integer        | no       | The assignee the board should be scoped to | | ||||
| | `milestone_id`      | integer        | no       | The milestone the board should be scoped to | | ||||
| | `labels`            | string         | no       | Comma-separated list of label names which the board should be scoped to | | ||||
| | `weight`            | integer        | no       | The weight range from 0 to 9, to which the board should be scoped to | | ||||
| | 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 | | ||||
| | `board_id`                   | integer        | yes      | The ID of a board | | ||||
| | `name`                       | string         | no       | The new name of the board | | ||||
| | `hide_backlog_list`          | boolean        | no       | Hide the Open list | | ||||
| | `hide_closed_list`           | boolean        | no       | Hide the Closed list | | ||||
| | `assignee_id` **(PREMIUM)**  | integer        | no       | The assignee 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 | ||||
| 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" | ||||
|  |  | |||
|  | @ -7,10 +7,10 @@ module API | |||
| 
 | ||||
|     prepend_if_ee('EE::API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule | ||||
| 
 | ||||
|     before { authenticate! } | ||||
| 
 | ||||
|     feature_category :boards | ||||
| 
 | ||||
|     before { authenticate! } | ||||
| 
 | ||||
|     helpers do | ||||
|       def board_parent | ||||
|         user_project | ||||
|  |  | |||
|  | @ -80,10 +80,20 @@ module API | |||
|           requires :label_id, type: Integer, desc: 'The ID of an existing label' | ||||
|         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. | ||||
|           # 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 | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ module API | |||
|     class Board < Grape::Entity | ||||
|       expose :id | ||||
|       expose :name | ||||
|       expose :hide_backlog_list | ||||
|       expose :hide_closed_list | ||||
|       expose :project, using: Entities::BasicProjectDetails | ||||
| 
 | ||||
|       expose :lists, using: Entities::List do |board| | ||||
|  |  | |||
|  | @ -9,9 +9,7 @@ module API | |||
| 
 | ||||
|     feature_category :boards | ||||
| 
 | ||||
|     before do | ||||
|       authenticate! | ||||
|     end | ||||
|     before { authenticate! } | ||||
| 
 | ||||
|     helpers do | ||||
|       def board_parent | ||||
|  | @ -22,18 +20,8 @@ module API | |||
|     params do | ||||
|       requires :id, type: String, desc: 'The ID of a group' | ||||
|     end | ||||
| 
 | ||||
|     resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS 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 | ||||
|           detail 'This feature was introduced in 10.6' | ||||
|           success Entities::Board | ||||
|  | @ -45,6 +33,28 @@ module API | |||
|           authorize!(:read_board, user_group) | ||||
|           present paginate(board_parent.boards.with_associations), with: Entities::Board | ||||
|         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 | ||||
| 
 | ||||
|       params do | ||||
|  |  | |||
|  | @ -220,9 +220,13 @@ module API | |||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|         bad_request!(result.errors.first) if result.error? | ||||
| 
 | ||||
|         package = result.payload[:package] | ||||
| 
 | ||||
|         case format | ||||
|         when 'sha1' | ||||
|           # After uploading a file, Maven tries to upload a sha1 and md5 version of it. | ||||
|  |  | |||
|  | @ -8,7 +8,6 @@ module BulkImports | |||
|       end | ||||
| 
 | ||||
|       def execute | ||||
|         entity.start! | ||||
|         bulk_import = entity.bulk_import | ||||
|         configuration = bulk_import.configuration | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -37,6 +37,7 @@ secret_detection: | |||
|       when: never | ||||
|     - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH | ||||
|   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 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 | ||||
|  |  | |||
|  | @ -5,9 +5,10 @@ module Gitlab | |||
|     module Reindexing | ||||
|       # This can be used to send annotations for reindexing to a Grafana API | ||||
|       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_url = api_url | ||||
|           @additional_tag = additional_tag | ||||
|         end | ||||
| 
 | ||||
|         def notify_start(action) | ||||
|  | @ -37,7 +38,7 @@ module Gitlab | |||
|         def base_payload(action) | ||||
|           { | ||||
|             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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -34,7 +34,8 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do | |||
|     let(:experiment_active) { 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') | ||||
| 
 | ||||
|       find('.js-gitlab-ci-syntax-yml-selector').click | ||||
|  |  | |||
|  | @ -0,0 +1,9 @@ | |||
| - title: | ||||
|   body: | ||||
|   stage: | ||||
|   self-managed: | ||||
|   gitlab-com: | ||||
|   url: | ||||
|   image_url: | ||||
|   published_at: | ||||
|   release: | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -198,21 +198,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { | |||
|           expect(findLoadingIcon().exists()).toBe(true); | ||||
|           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', () => { | ||||
|  |  | |||
|  | @ -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> | ||||
| `; | ||||
|  | @ -1,20 +1,81 @@ | |||
| import Vue from 'vue'; | ||||
| import mountComponent from 'helpers/vue_mount_component_helper'; | ||||
| import { nextTick } from 'vue'; | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| 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 MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; | ||||
| import eventHub from '~/vue_merge_request_widget/event_hub'; | ||||
| 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', () => { | ||||
|   let vm; | ||||
|   let oldWindowGl; | ||||
|   const targetBranchPath = '/foo/bar'; | ||||
|   const targetBranch = 'foo'; | ||||
|   const sha = '1EA2EZ34'; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     const Component = Vue.extend(autoMergeEnabledComponent); | ||||
|     jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); | ||||
| 
 | ||||
|     oldWindowGl = window.gl; | ||||
|  | @ -23,216 +84,234 @@ describe('MRWidgetAutoMergeEnabled', () => { | |||
|         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(() => { | ||||
|     vm.$destroy(); | ||||
|     window.gl = oldWindowGl; | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('computed', () => { | ||||
|     describe('canRemoveSourceBranch', () => { | ||||
|       it('should return true when user is able to remove source branch', () => { | ||||
|         expect(vm.canRemoveSourceBranch).toBeTruthy(); | ||||
|   [true, false].forEach((mergeRequestWidgetGraphql) => { | ||||
|     describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => { | ||||
|       beforeEach(() => { | ||||
|         mergeRequestWidgetGraphqlEnabled = mergeRequestWidgetGraphql; | ||||
|       }); | ||||
| 
 | ||||
|       it('should return false when user id is not the same with who set the MWPS', () => { | ||||
|         vm.mr.mergeUserId = 2; | ||||
| 
 | ||||
|         expect(vm.canRemoveSourceBranch).toBeFalsy(); | ||||
| 
 | ||||
|         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, | ||||
|       describe('computed', () => { | ||||
|         describe('canRemoveSourceBranch', () => { | ||||
|           it('should return true when user is able to remove source branch', () => { | ||||
|             factory({ | ||||
|               ...defaultMrProps(), | ||||
|             }); | ||||
|           }), | ||||
|         ); | ||||
| 
 | ||||
|         vm.cancelAutomaticMerge(); | ||||
|         setImmediate(() => { | ||||
|           expect(vm.isCancellingAutoMerge).toBeTruthy(); | ||||
|           expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); | ||||
|           done(); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('removeSourceBranch', () => { | ||||
|       it('should set flag and call service then request main component to update the widget', (done) => { | ||||
|         jest.spyOn(vm.service, 'merge').mockReturnValue( | ||||
|           Promise.resolve({ | ||||
|             data: { | ||||
|               status: MWPS_MERGE_STRATEGY, | ||||
|             }, | ||||
|           }), | ||||
|         ); | ||||
| 
 | ||||
|         vm.removeSourceBranch(); | ||||
|         setImmediate(() => { | ||||
|           expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); | ||||
|           expect(vm.service.merge).toHaveBeenCalledWith({ | ||||
|             sha, | ||||
|             auto_merge_strategy: MWPS_MERGE_STRATEGY, | ||||
|             should_remove_source_branch: true, | ||||
|             expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true); | ||||
|           }); | ||||
| 
 | ||||
|           it.each` | ||||
|             mergeUserId | currentUserId | ||||
|             ${2}        | ${1} | ||||
|             ${1}        | ${2} | ||||
|           `(
 | ||||
|             'should return false when user id is not the same with who set the MWPS', | ||||
|             ({ mergeUserId, currentUserId }) => { | ||||
|               factory({ | ||||
|                 ...defaultMrProps(), | ||||
|                 mergeUserId, | ||||
|                 currentUserId, | ||||
|               }); | ||||
| 
 | ||||
|               expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false); | ||||
|             }, | ||||
|           ); | ||||
| 
 | ||||
|           it('should return false when shouldRemoveSourceBranch set to false', () => { | ||||
|             factory({ | ||||
|               ...defaultMrProps(), | ||||
|               shouldRemoveSourceBranch: 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', () => { | ||||
|     it('should have correct elements', () => { | ||||
|       expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); | ||||
|       expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds'); | ||||
|       describe('methods', () => { | ||||
|         describe('cancelAutomaticMerge', () => { | ||||
|           it('should set flag and call service then tell main component to update the widget with data', (done) => { | ||||
|             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'); | ||||
|       expect(vm.$el.innerText).toContain(targetBranch); | ||||
|       expect(vm.$el.innerText).toContain('The source branch will not be deleted'); | ||||
|       expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain( | ||||
|         'Cancel automatic merge', | ||||
|       ); | ||||
|             wrapper.vm.cancelAutomaticMerge(); | ||||
|             setImmediate(() => { | ||||
|               expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy(); | ||||
|               expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); | ||||
|               done(); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|       expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); | ||||
|       expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain( | ||||
|         'Delete source branch', | ||||
|       ); | ||||
|         describe('removeSourceBranch', () => { | ||||
|           it('should set flag and call service then request main component to update the widget', (done) => { | ||||
|             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(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should disable cancel auto merge button when the action is in progress', (done) => { | ||||
|       vm.isCancellingAutoMerge = true; | ||||
| 
 | ||||
|       Vue.nextTick(() => { | ||||
|         expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); | ||||
|         done(); | ||||
|             wrapper.vm.removeSourceBranch(); | ||||
|             setImmediate(() => { | ||||
|               expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); | ||||
|               expect(wrapper.vm.service.merge).toHaveBeenCalledWith({ | ||||
|                 sha, | ||||
|                 auto_merge_strategy: MWPS_MERGE_STRATEGY, | ||||
|                 should_remove_source_branch: true, | ||||
|               }); | ||||
|               done(); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should show source branch will be deleted text when it source branch set to remove', (done) => { | ||||
|       vm.mr.shouldRemoveSourceBranch = true; | ||||
|       describe('template', () => { | ||||
|         it('should have correct elements', () => { | ||||
|           factory({ | ||||
|             ...defaultMrProps(), | ||||
|           }); | ||||
| 
 | ||||
|       Vue.nextTick(() => { | ||||
|         const normalizedText = vm.$el.innerText.replace(/\s+/g, ' '); | ||||
|           expect(wrapper.element).toMatchSnapshot(); | ||||
|         }); | ||||
| 
 | ||||
|         expect(normalizedText).toContain('The source branch will be deleted'); | ||||
|         expect(normalizedText).not.toContain('The source branch will not be deleted'); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|         it('should disable cancel auto merge button when the action is in progress', async () => { | ||||
|           factory({ | ||||
|             ...defaultMrProps(), | ||||
|           }); | ||||
|           wrapper.setData({ | ||||
|             isCancellingAutoMerge: true, | ||||
|           }); | ||||
| 
 | ||||
|     it('should not show delete source branch button when user not able to delete source branch', (done) => { | ||||
|       vm.mr.currentUserId = 4; | ||||
|           await nextTick(); | ||||
| 
 | ||||
|       Vue.nextTick(() => { | ||||
|         expect(vm.$el.querySelector('.js-remove-source-branch')).toEqual(null); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|           expect(wrapper.find('.js-cancel-auto-merge').attributes('disabled')).toBe('disabled'); | ||||
|         }); | ||||
| 
 | ||||
|     it('should disable delete source branch button when the action is in progress', (done) => { | ||||
|       vm.isRemovingSourceBranch = true; | ||||
|         it('should show source branch will be deleted text when it source branch set to remove', () => { | ||||
|           factory({ | ||||
|             ...defaultMrProps(), | ||||
|             shouldRemoveSourceBranch: true, | ||||
|           }); | ||||
| 
 | ||||
|       Vue.nextTick(() => { | ||||
|         expect( | ||||
|           vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled'), | ||||
|         ).toBeTruthy(); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|           const normalizedText = wrapper.text().replace(/\s+/g, ' '); | ||||
| 
 | ||||
|     it('should render the status text as "...to merged automatically" if MWPS is selected', (done) => { | ||||
|       Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY); | ||||
|           expect(normalizedText).toContain('The source branch will be deleted'); | ||||
|           expect(normalizedText).not.toContain('The source branch will not be deleted'); | ||||
|         }); | ||||
| 
 | ||||
|       Vue.nextTick(() => { | ||||
|         const statusText = trimText(vm.$el.querySelector('.js-status-text-after-author').innerText); | ||||
|         it('should not show delete source branch button when user not able to delete source branch', () => { | ||||
|           factory({ | ||||
|             ...defaultMrProps(), | ||||
|             currentUserId: 4, | ||||
|           }); | ||||
| 
 | ||||
|         expect(statusText).toBe('to be merged automatically when the pipeline succeeds'); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|           expect(wrapper.find('.js-remove-source-branch').exists()).toBe(false); | ||||
|         }); | ||||
| 
 | ||||
|     it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', (done) => { | ||||
|       Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY); | ||||
|         it('should disable delete source branch button when the action is in progress', async () => { | ||||
|           factory({ | ||||
|             ...defaultMrProps(), | ||||
|           }); | ||||
|           wrapper.setData({ | ||||
|             isRemovingSourceBranch: true, | ||||
|           }); | ||||
| 
 | ||||
|       Vue.nextTick(() => { | ||||
|         const cancelButtonText = trimText(vm.$el.querySelector('.js-cancel-auto-merge').innerText); | ||||
|           await nextTick(); | ||||
| 
 | ||||
|         expect(cancelButtonText).toBe('Cancel automatic merge'); | ||||
|         done(); | ||||
|           expect(wrapper.find('.js-remove-source-branch').attributes('disabled')).toBe('disabled'); | ||||
|         }); | ||||
| 
 | ||||
|         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'); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import { nextTick } from 'vue'; | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import { GlLoadingIcon, GlButton } from '@gitlab/ui'; | ||||
| 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 findButton = () => wrapper.find(GlButton); | ||||
| 
 | ||||
|   const createComponent = (props = {}) => { | ||||
|   const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => { | ||||
|     wrapper = shallowMount(AutoMergeFailedComponent, { | ||||
|       propsData: { ...props }, | ||||
|       data() { | ||||
|         if (mergeRequestWidgetGraphql) { | ||||
|           return { mergeError: props.mr?.mergeError }; | ||||
|         } | ||||
| 
 | ||||
|         return {}; | ||||
|       }, | ||||
|       provide: { | ||||
|         glFeatures: { mergeRequestWidgetGraphql }, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     createComponent({ | ||||
|       mr: { mergeError }, | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders failed message', () => { | ||||
|     expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); | ||||
|   }); | ||||
|   [true, false].forEach((mergeRequestWidgetGraphql) => { | ||||
|     describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => { | ||||
|       beforeEach(() => { | ||||
|         createComponent( | ||||
|           { | ||||
|             mr: { mergeError }, | ||||
|           }, | ||||
|           mergeRequestWidgetGraphql, | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|   it('renders merge error provided', () => { | ||||
|     expect(wrapper.text()).toContain(mergeError); | ||||
|   }); | ||||
|       it('renders failed message', () => { | ||||
|         expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); | ||||
|       }); | ||||
| 
 | ||||
|   it('render refresh button', () => { | ||||
|     expect(findButton().text()).toEqual('Refresh'); | ||||
|   }); | ||||
|       it('renders merge error provided', () => { | ||||
|         expect(wrapper.text()).toContain(mergeError); | ||||
|       }); | ||||
| 
 | ||||
|   it('emits event and shows loading icon when button is clicked', () => { | ||||
|     jest.spyOn(eventHub, '$emit'); | ||||
|     findButton().vm.$emit('click'); | ||||
|       it('render refresh button', () => { | ||||
|         expect(findButton().text()).toBe('Refresh'); | ||||
|       }); | ||||
| 
 | ||||
|     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(findButton().attributes('disabled')).toBe('true'); | ||||
|       expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); | ||||
|         expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested'); | ||||
| 
 | ||||
|         await nextTick(); | ||||
| 
 | ||||
|         expect(findButton().attributes('disabled')).toBe('true'); | ||||
|         expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -201,6 +201,10 @@ describe('gfm_autocomplete/utils', () => { | |||
|       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', () => { | ||||
|       expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot(); | ||||
|     }); | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do | |||
|       conflicts auto_merge_enabled approved_by source_branch_protected | ||||
|       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 | ||||
|       auto_merge_strategy merge_user | ||||
|     ] | ||||
| 
 | ||||
|     expect(described_class).to have_graphql_fields(*expected_fields).at_least | ||||
|  |  | |||
|  | @ -433,6 +433,7 @@ RSpec.describe ProjectsHelper do | |||
|     context 'when project has external wiki' do | ||||
|       it 'includes external wiki tab' do | ||||
|         project.create_external_wiki_service(active: true, properties: { 'external_wiki_url' => 'https://gitlab.com' }) | ||||
|         project.reload | ||||
| 
 | ||||
|         is_expected.to include(:external_wiki) | ||||
|       end | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ require 'spec_helper' | |||
| RSpec.describe BulkImports::Importers::GroupImporter do | ||||
|   let(:user) { create(:user) } | ||||
|   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(:context) do | ||||
|     BulkImports::Pipeline::Context.new( | ||||
|  | @ -23,7 +23,6 @@ RSpec.describe BulkImports::Importers::GroupImporter do | |||
| 
 | ||||
|   describe '#execute' 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('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee? | ||||
|       expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -7,6 +7,7 @@ RSpec.describe Gitlab::Database::Reindexing::GrafanaNotifier do | |||
| 
 | ||||
|   let(:api_key) { "foo" } | ||||
|   let(:api_url) { "http://bar"} | ||||
|   let(:additional_tag) { "some-tag" } | ||||
| 
 | ||||
|   let(:action) { create(:reindex_action) } | ||||
| 
 | ||||
|  | @ -73,32 +74,66 @@ RSpec.describe Gitlab::Database::Reindexing::GrafanaNotifier do | |||
|   end | ||||
| 
 | ||||
|   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 | ||||
|       { | ||||
|         time: (action.action_start.utc.to_f * 1000).to_i, | ||||
|         tags: ['reindex', action.index.tablename, action.index.name], | ||||
|         text: "Started reindexing of #{action.index.name} on #{action.index.tablename}" | ||||
|       } | ||||
|       let(:payload) do | ||||
|         { | ||||
|           time: (action.action_start.utc.to_f * 1000).to_i, | ||||
|           tags: ['reindex', 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 | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|   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 | ||||
|       { | ||||
|         time: (action.action_start.utc.to_f * 1000).to_i, | ||||
|         tags: ['reindex', 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 | ||||
|       } | ||||
|       let(:payload) do | ||||
|         { | ||||
|           time: (action.action_start.utc.to_f * 1000).to_i, | ||||
|           tags: ['reindex', 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 | ||||
| 
 | ||||
|     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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -33,4 +33,49 @@ RSpec.describe Namespace::PackageSetting do | |||
|       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 | ||||
|  |  | |||
|  | @ -745,4 +745,14 @@ RSpec.describe Packages::Package, type: :model do | |||
|       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 | ||||
|  |  | |||
|  | @ -1067,36 +1067,6 @@ RSpec.describe Project, factory_default: :keep do | |||
|     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 | ||||
|     let(:no_wiki_project)       { create(:project, :wiki_disabled, has_external_wiki: false) } | ||||
|     let(:wiki_enabled_project)  { create(:project) } | ||||
|  | @ -1136,51 +1106,63 @@ RSpec.describe Project, factory_default: :keep do | |||
|   describe '#external_wiki' do | ||||
|     let_it_be(:project) { create(:project) } | ||||
| 
 | ||||
|     context 'with an active external wiki' do | ||||
|       before do | ||||
|     def subject | ||||
|       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) | ||||
|         project.external_wiki | ||||
|       end | ||||
| 
 | ||||
|       it 'sets :has_external_wiki as true' do | ||||
|         expect(project.has_external_wiki).to be(true) | ||||
|       specify { is_expected.to eq(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 | ||||
| 
 | ||||
|       it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do | ||||
|         expect(project.has_external_wiki).to be(true) | ||||
| 
 | ||||
|         project.services.external_wikis.first.destroy | ||||
| 
 | ||||
|         expect(project.has_external_wiki).to be(false) | ||||
|       it 'becomes false if the external wiki service becomes inactive' do | ||||
|         expect do | ||||
|           service.update_column(:active, false) | ||||
|         end.to change { subject }.to(false) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with an inactive external wiki' do | ||||
|       before do | ||||
|         create(:service, project: project, type: 'ExternalWikiService', active: false) | ||||
|       end | ||||
|     it 'is false when external wiki service is not active' do | ||||
|       create(:service, project: project, type: 'ExternalWikiService', active: false) | ||||
| 
 | ||||
|       it 'sets :has_external_wiki as false' do | ||||
|         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 | ||||
|       is_expected.to eq(false) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| 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 | ||||
|     allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) | ||||
|  |  | |||
|  | @ -53,17 +53,6 @@ RSpec.describe API::Boards do | |||
|     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 | ||||
|     let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,8 @@ require 'spec_helper' | |||
| RSpec.describe API::MavenPackages do | ||||
|   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(:project, reload: true) { create(:project, :public, namespace: group) } | ||||
|   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(: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(: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) } | ||||
|  | @ -669,6 +671,35 @@ RSpec.describe API::MavenPackages do | |||
|         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 | ||||
|         let(:dummy_package) { double(Packages::Package) } | ||||
| 
 | ||||
|  | @ -698,7 +729,7 @@ RSpec.describe API::MavenPackages do | |||
|     end | ||||
| 
 | ||||
|     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( | ||||
|         api(url), | ||||
|         method: :put, | ||||
|  |  | |||
|  | @ -11,29 +11,36 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do | |||
|   let(:file_name) { 'test.jar' } | ||||
|   let(:param_path) { "#{path}/#{version}" } | ||||
|   let(:params) { { path: param_path, file_name: file_name } } | ||||
|   let(:service) { described_class.new(project, user, params) } | ||||
| 
 | ||||
|   describe '#execute' do | ||||
|     using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|     subject { described_class.new(project, user, params).execute } | ||||
|     subject { service.execute } | ||||
| 
 | ||||
|     RSpec.shared_examples 'reuse existing package' do | ||||
|       it { expect { subject}.not_to change { Packages::Package.count } } | ||||
|     shared_examples 'reuse existing package' do | ||||
|       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 | ||||
| 
 | ||||
|     RSpec.shared_examples 'create package' do | ||||
|     shared_examples 'create package' do | ||||
|       it { expect { subject }.to change { Packages::Package.count }.by(1) } | ||||
| 
 | ||||
|       it 'sets the proper name and version' do | ||||
|         pkg = subject | ||||
|       it 'sets the proper name and version', :aggregate_failures do | ||||
|         pkg = subject.payload[:package] | ||||
| 
 | ||||
|         expect(pkg.name).to eq(path) | ||||
|         expect(pkg.version).to eq(version) | ||||
|       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 | ||||
| 
 | ||||
|     context 'path with version' do | ||||
|  | @ -90,5 +97,27 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do | |||
|         expect { subject }.to change { Packages::BuildInfo.count }.by(1) | ||||
|       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 | ||||
|  |  | |||
|  | @ -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) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|       describe "GET #{route_definition}/:board_id" do | ||||
|         let(:url) { "#{root_url}/#{board.id}" } | ||||
|   describe "GET #{route_definition}/:board_id" do | ||||
|     let(:url) { "#{root_url}/#{board.id}" } | ||||
| 
 | ||||
|         it 'get a single board by id' do | ||||
|           get api(url, user) | ||||
|     it 'get a single board by id' do | ||||
|       get api(url, user) | ||||
| 
 | ||||
|           expect_schema_match_for(response, 'public_api/v4/board', ee) | ||||
|         end | ||||
|       end | ||||
|       expect_schema_match_for(response, 'public_api/v4/board', ee) | ||||
|     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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,13 +4,74 @@ require 'spec_helper' | |||
| 
 | ||||
| RSpec.describe BulkImportWorker do | ||||
|   describe '#perform' do | ||||
|     it 'executes Group Importer' do | ||||
|       bulk_import_id = 1 | ||||
|     before do | ||||
|       stub_const("#{described_class}::DEFAULT_BATCH_SIZE", 1) | ||||
|     end | ||||
| 
 | ||||
|       expect(BulkImports::Importers::GroupsImporter) | ||||
|         .to receive(:new).with(bulk_import_id).and_return(double(execute: true)) | ||||
|     context 'when no bulk import is found' do | ||||
|       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 | ||||
|  |  | |||
|  | @ -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 | ||||
		Loading…
	
		Reference in New Issue