Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									6b9b8a52ba
								
							
						
					
					
						commit
						984357420a
					
				|  | @ -45,6 +45,7 @@ | ||||||
|       "Debian", |       "Debian", | ||||||
|       "DevOps", |       "DevOps", | ||||||
|       "Docker", |       "Docker", | ||||||
|  |       "DockerSlim", | ||||||
|       "Elasticsearch", |       "Elasticsearch", | ||||||
|       "Facebook", |       "Facebook", | ||||||
|       "fastlane", |       "fastlane", | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| #import "./issue.fragment.graphql" | #import "ee_else_ce/boards/queries/issue.fragment.graphql" | ||||||
| 
 | 
 | ||||||
| mutation IssueMoveList( | mutation IssueMoveList( | ||||||
|   $projectPath: ID! |   $projectPath: ID! | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <script> | <script> | ||||||
| /* eslint-disable vue/no-v-html */ | /* eslint-disable vue/no-v-html */ | ||||||
| import { escape } from 'lodash'; | import { escape } from 'lodash'; | ||||||
| import { GlModal, GlButton, GlDeprecatedButton, GlFormInput, GlSprintf } from '@gitlab/ui'; | import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; | ||||||
| import SplitButton from '~/vue_shared/components/split_button.vue'; | import SplitButton from '~/vue_shared/components/split_button.vue'; | ||||||
| import { s__, sprintf } from '~/locale'; | import { s__, sprintf } from '~/locale'; | ||||||
| import csrf from '~/lib/utils/csrf'; | import csrf from '~/lib/utils/csrf'; | ||||||
|  | @ -29,7 +29,6 @@ export default { | ||||||
|     SplitButton, |     SplitButton, | ||||||
|     GlModal, |     GlModal, | ||||||
|     GlButton, |     GlButton, | ||||||
|     GlDeprecatedButton, |  | ||||||
|     GlFormInput, |     GlFormInput, | ||||||
|     GlSprintf, |     GlSprintf, | ||||||
|   }, |   }, | ||||||
|  | @ -175,24 +174,31 @@ export default { | ||||||
|         }}</span> |         }}</span> | ||||||
|       </template> |       </template> | ||||||
|       <template #modal-footer> |       <template #modal-footer> | ||||||
|         <gl-deprecated-button variant="secondary" @click="handleCancel">{{ |         <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button> | ||||||
|           s__('Cancel') |  | ||||||
|         }}</gl-deprecated-button> |  | ||||||
|         <template v-if="confirmCleanup"> |         <template v-if="confirmCleanup"> | ||||||
|           <gl-deprecated-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{ |           <gl-button | ||||||
|             s__('ClusterIntegration|Remove integration') |             :disabled="!canSubmit" | ||||||
|           }}</gl-deprecated-button> |             variant="warning" | ||||||
|           <gl-deprecated-button |             category="primary" | ||||||
|  |             @click="handleSubmit" | ||||||
|  |             >{{ s__('ClusterIntegration|Remove integration') }}</gl-button | ||||||
|  |           > | ||||||
|  |           <gl-button | ||||||
|             :disabled="!canSubmit" |             :disabled="!canSubmit" | ||||||
|             variant="danger" |             variant="danger" | ||||||
|  |             category="primary" | ||||||
|             @click="handleSubmit(true)" |             @click="handleSubmit(true)" | ||||||
|             >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-deprecated-button |             >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-button | ||||||
|           > |           > | ||||||
|         </template> |         </template> | ||||||
|         <template v-else> |         <template v-else> | ||||||
|           <gl-deprecated-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{ |           <gl-button | ||||||
|             s__('ClusterIntegration|Remove integration') |             :disabled="!canSubmit" | ||||||
|           }}</gl-deprecated-button> |             variant="danger" | ||||||
|  |             category="primary" | ||||||
|  |             @click="handleSubmit" | ||||||
|  |             >{{ s__('ClusterIntegration|Remove integration') }}</gl-button | ||||||
|  |           > | ||||||
|         </template> |         </template> | ||||||
|       </template> |       </template> | ||||||
|     </gl-modal> |     </gl-modal> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <script> | <script> | ||||||
| import { mapState, mapActions, mapGetters } from 'vuex'; | import { mapState, mapActions, mapGetters } from 'vuex'; | ||||||
| import { GlModal } from '@gitlab/ui'; | import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui'; | ||||||
| import { n__, __ } from '~/locale'; | import { n__, __ } from '~/locale'; | ||||||
| import LoadingButton from '~/vue_shared/components/loading_button.vue'; | import LoadingButton from '~/vue_shared/components/loading_button.vue'; | ||||||
| import CommitMessageField from './message_field.vue'; | import CommitMessageField from './message_field.vue'; | ||||||
|  | @ -8,6 +8,7 @@ import Actions from './actions.vue'; | ||||||
| import SuccessMessage from './success_message.vue'; | import SuccessMessage from './success_message.vue'; | ||||||
| import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; | import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; | ||||||
| import consts from '../../stores/modules/commit/constants'; | import consts from '../../stores/modules/commit/constants'; | ||||||
|  | import { createUnexpectedCommitError } from '../../lib/errors'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|  | @ -17,15 +18,20 @@ export default { | ||||||
|     SuccessMessage, |     SuccessMessage, | ||||||
|     GlModal, |     GlModal, | ||||||
|   }, |   }, | ||||||
|  |   directives: { | ||||||
|  |     SafeHtml: GlSafeHtmlDirective, | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       isCompact: true, |       isCompact: true, | ||||||
|       componentHeight: null, |       componentHeight: null, | ||||||
|  |       // Keep track of "lastCommitError" so we hold onto the value even when "commitError" is cleared. | ||||||
|  |       lastCommitError: createUnexpectedCommitError(), | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), |     ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), | ||||||
|     ...mapState('commit', ['commitMessage', 'submitCommitLoading']), |     ...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']), | ||||||
|     ...mapGetters(['someUncommittedChanges']), |     ...mapGetters(['someUncommittedChanges']), | ||||||
|     ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), |     ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), | ||||||
|     overviewText() { |     overviewText() { | ||||||
|  | @ -38,11 +44,28 @@ export default { | ||||||
|     currentViewIsCommitView() { |     currentViewIsCommitView() { | ||||||
|       return this.currentActivityView === leftSidebarViews.commit.name; |       return this.currentActivityView === leftSidebarViews.commit.name; | ||||||
|     }, |     }, | ||||||
|  |     commitErrorPrimaryAction() { | ||||||
|  |       if (!this.lastCommitError?.canCreateBranch) { | ||||||
|  |         return undefined; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return { | ||||||
|  |         text: __('Create new branch'), | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     currentActivityView: 'handleCompactState', |     currentActivityView: 'handleCompactState', | ||||||
|     someUncommittedChanges: 'handleCompactState', |     someUncommittedChanges: 'handleCompactState', | ||||||
|     lastCommitMsg: 'handleCompactState', |     lastCommitMsg: 'handleCompactState', | ||||||
|  |     commitError(val) { | ||||||
|  |       if (!val) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.lastCommitError = val; | ||||||
|  |       this.$refs.commitErrorModal.show(); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     ...mapActions(['updateActivityBarView']), |     ...mapActions(['updateActivityBarView']), | ||||||
|  | @ -53,9 +76,7 @@ export default { | ||||||
|       'updateCommitAction', |       'updateCommitAction', | ||||||
|     ]), |     ]), | ||||||
|     commit() { |     commit() { | ||||||
|       return this.commitChanges().catch(() => { |       return this.commitChanges(); | ||||||
|         this.$refs.createBranchModal.show(); |  | ||||||
|       }); |  | ||||||
|     }, |     }, | ||||||
|     forceCreateNewBranch() { |     forceCreateNewBranch() { | ||||||
|       return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit()); |       return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit()); | ||||||
|  | @ -164,17 +185,14 @@ export default { | ||||||
|           </button> |           </button> | ||||||
|         </div> |         </div> | ||||||
|         <gl-modal |         <gl-modal | ||||||
|           ref="createBranchModal" |           ref="commitErrorModal" | ||||||
|           modal-id="ide-create-branch-modal" |           modal-id="ide-commit-error-modal" | ||||||
|           :ok-title="__('Create new branch')" |           :title="lastCommitError.title" | ||||||
|           :title="__('Branch has changed')" |           :action-primary="commitErrorPrimaryAction" | ||||||
|           ok-variant="success" |           :action-cancel="{ text: __('Cancel') }" | ||||||
|           @ok="forceCreateNewBranch" |           @ok="forceCreateNewBranch" | ||||||
|         > |         > | ||||||
|           {{ |           <div v-safe-html="lastCommitError.messageHTML"></div> | ||||||
|             __(`This branch has changed since you started editing. |  | ||||||
|                 Would you like to create a new branch?`) |  | ||||||
|           }} |  | ||||||
|         </gl-modal> |         </gl-modal> | ||||||
|       </form> |       </form> | ||||||
|     </transition> |     </transition> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,39 @@ | ||||||
|  | import { escape } from 'lodash'; | ||||||
|  | import { __ } from '~/locale'; | ||||||
|  | 
 | ||||||
|  | const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; | ||||||
|  | const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; | ||||||
|  | 
 | ||||||
|  | export const createUnexpectedCommitError = () => ({ | ||||||
|  |   title: __('Unexpected error'), | ||||||
|  |   messageHTML: __('Could not commit. An unexpected error occurred.'), | ||||||
|  |   canCreateBranch: false, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const createCodeownersCommitError = message => ({ | ||||||
|  |   title: __('CODEOWNERS rule violation'), | ||||||
|  |   messageHTML: escape(message), | ||||||
|  |   canCreateBranch: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const createBranchChangedCommitError = message => ({ | ||||||
|  |   title: __('Branch changed'), | ||||||
|  |   messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`, | ||||||
|  |   canCreateBranch: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const parseCommitError = e => { | ||||||
|  |   const { message } = e?.response?.data || {}; | ||||||
|  | 
 | ||||||
|  |   if (!message) { | ||||||
|  |     return createUnexpectedCommitError(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (CODEOWNERS_REGEX.test(message)) { | ||||||
|  |     return createCodeownersCommitError(message); | ||||||
|  |   } else if (BRANCH_CHANGED_REGEX.test(message)) { | ||||||
|  |     return createBranchChangedCommitError(message); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return createUnexpectedCommitError(); | ||||||
|  | }; | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import { sprintf, __ } from '~/locale'; | import { sprintf, __ } from '~/locale'; | ||||||
| import { deprecatedCreateFlash as flash } from '~/flash'; | import { deprecatedCreateFlash as flash } from '~/flash'; | ||||||
| import httpStatusCodes from '~/lib/utils/http_status'; |  | ||||||
| import * as rootTypes from '../../mutation_types'; | import * as rootTypes from '../../mutation_types'; | ||||||
| import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; | import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; | ||||||
| import service from '../../../services'; | import service from '../../../services'; | ||||||
|  | @ -8,6 +7,7 @@ import * as types from './mutation_types'; | ||||||
| import consts from './constants'; | import consts from './constants'; | ||||||
| import { leftSidebarViews } from '../../../constants'; | import { leftSidebarViews } from '../../../constants'; | ||||||
| import eventHub from '../../../eventhub'; | import eventHub from '../../../eventhub'; | ||||||
|  | import { parseCommitError } from '../../../lib/errors'; | ||||||
| 
 | 
 | ||||||
| export const updateCommitMessage = ({ commit }, message) => { | export const updateCommitMessage = ({ commit }, message) => { | ||||||
|   commit(types.UPDATE_COMMIT_MESSAGE, message); |   commit(types.UPDATE_COMMIT_MESSAGE, message); | ||||||
|  | @ -113,6 +113,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo | ||||||
|     ? Promise.resolve() |     ? Promise.resolve() | ||||||
|     : dispatch('stageAllChanges', null, { root: true }); |     : dispatch('stageAllChanges', null, { root: true }); | ||||||
| 
 | 
 | ||||||
|  |   commit(types.CLEAR_ERROR); | ||||||
|   commit(types.UPDATE_LOADING, true); |   commit(types.UPDATE_LOADING, true); | ||||||
| 
 | 
 | ||||||
|   return stageFilesPromise |   return stageFilesPromise | ||||||
|  | @ -128,6 +129,12 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo | ||||||
| 
 | 
 | ||||||
|       return service.commit(rootState.currentProjectId, payload); |       return service.commit(rootState.currentProjectId, payload); | ||||||
|     }) |     }) | ||||||
|  |     .catch(e => { | ||||||
|  |       commit(types.UPDATE_LOADING, false); | ||||||
|  |       commit(types.SET_ERROR, parseCommitError(e)); | ||||||
|  | 
 | ||||||
|  |       throw e; | ||||||
|  |     }) | ||||||
|     .then(({ data }) => { |     .then(({ data }) => { | ||||||
|       commit(types.UPDATE_LOADING, false); |       commit(types.UPDATE_LOADING, false); | ||||||
| 
 | 
 | ||||||
|  | @ -214,24 +221,5 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo | ||||||
|             { root: true }, |             { root: true }, | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|     }) |  | ||||||
|     .catch(err => { |  | ||||||
|       commit(types.UPDATE_LOADING, false); |  | ||||||
| 
 |  | ||||||
|       // don't catch bad request errors, let the view handle them
 |  | ||||||
|       if (err.response.status === httpStatusCodes.BAD_REQUEST) throw err; |  | ||||||
| 
 |  | ||||||
|       dispatch( |  | ||||||
|         'setErrorMessage', |  | ||||||
|         { |  | ||||||
|           text: __('An error occurred while committing your changes.'), |  | ||||||
|           action: () => |  | ||||||
|             dispatch('commitChanges').then(() => dispatch('setErrorMessage', null, { root: true })), |  | ||||||
|           actionText: __('Please try again'), |  | ||||||
|         }, |  | ||||||
|         { root: true }, |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       window.dispatchEvent(new Event('resize')); |  | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -3,3 +3,6 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; | ||||||
| export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; | export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; | ||||||
| export const UPDATE_LOADING = 'UPDATE_LOADING'; | export const UPDATE_LOADING = 'UPDATE_LOADING'; | ||||||
| export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR'; | export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR'; | ||||||
|  | 
 | ||||||
|  | export const CLEAR_ERROR = 'CLEAR_ERROR'; | ||||||
|  | export const SET_ERROR = 'SET_ERROR'; | ||||||
|  |  | ||||||
|  | @ -24,4 +24,10 @@ export default { | ||||||
|       shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR, |       shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR, | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |   [types.CLEAR_ERROR](state) { | ||||||
|  |     state.commitError = null; | ||||||
|  |   }, | ||||||
|  |   [types.SET_ERROR](state, error) { | ||||||
|  |     state.commitError = error; | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -4,4 +4,5 @@ export default () => ({ | ||||||
|   newBranchName: '', |   newBranchName: '', | ||||||
|   submitCommitLoading: false, |   submitCommitLoading: false, | ||||||
|   shouldCreateMR: true, |   shouldCreateMR: true, | ||||||
|  |   commitError: null, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | <script> | ||||||
|  | import IssuableForm from './issuable_form.vue'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     IssuableForm, | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     descriptionPreviewPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     descriptionHelpPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     labelsFetchPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     labelsManagePath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div class="issuable-create-container"> | ||||||
|  |     <slot name="title"></slot> | ||||||
|  |     <hr /> | ||||||
|  |     <issuable-form | ||||||
|  |       :description-preview-path="descriptionPreviewPath" | ||||||
|  |       :description-help-path="descriptionHelpPath" | ||||||
|  |       :labels-fetch-path="labelsFetchPath" | ||||||
|  |       :labels-manage-path="labelsManagePath" | ||||||
|  |     > | ||||||
|  |       <template #actions="issuableMeta"> | ||||||
|  |         <slot name="actions" v-bind="issuableMeta"></slot> | ||||||
|  |       </template> | ||||||
|  |     </issuable-form> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1,122 @@ | ||||||
|  | <script> | ||||||
|  | import { GlForm, GlFormInput } from '@gitlab/ui'; | ||||||
|  | import MarkdownField from '~/vue_shared/components/markdown/field.vue'; | ||||||
|  | import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; | ||||||
|  | 
 | ||||||
|  | import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   LabelSelectVariant: DropdownVariant, | ||||||
|  |   components: { | ||||||
|  |     GlForm, | ||||||
|  |     GlFormInput, | ||||||
|  |     MarkdownField, | ||||||
|  |     LabelsSelect, | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     descriptionPreviewPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     descriptionHelpPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     labelsFetchPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     labelsManagePath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       issuableTitle: '', | ||||||
|  |       issuableDescription: '', | ||||||
|  |       selectedLabels: [], | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     handleUpdateSelectedLabels(labels) { | ||||||
|  |       if (labels.length) { | ||||||
|  |         this.selectedLabels = labels; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <gl-form class="common-note-form gfm-form" @submit.stop.prevent> | ||||||
|  |     <div data-testid="issuable-title" class="form-group row"> | ||||||
|  |       <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label> | ||||||
|  |       <div class="col-sm-10"> | ||||||
|  |         <gl-form-input id="issuable-title" v-model="issuableTitle" :placeholder="__('Title')" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div data-testid="issuable-description" class="form-group row"> | ||||||
|  |       <label for="issuable-description" class="col-form-label col-sm-2">{{ | ||||||
|  |         __('Description') | ||||||
|  |       }}</label> | ||||||
|  |       <div class="col-sm-10"> | ||||||
|  |         <markdown-field | ||||||
|  |           :markdown-preview-path="descriptionPreviewPath" | ||||||
|  |           :markdown-docs-path="descriptionHelpPath" | ||||||
|  |           :add-spacing-classes="false" | ||||||
|  |           :show-suggest-popover="true" | ||||||
|  |         > | ||||||
|  |           <textarea | ||||||
|  |             id="issuable-description" | ||||||
|  |             ref="textarea" | ||||||
|  |             slot="textarea" | ||||||
|  |             v-model="issuableDescription" | ||||||
|  |             dir="auto" | ||||||
|  |             class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area" | ||||||
|  |             :aria-label="__('Description')" | ||||||
|  |             :placeholder="__('Write a comment or drag your files here…')" | ||||||
|  |           ></textarea> | ||||||
|  |         </markdown-field> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="row"> | ||||||
|  |       <div class="col-lg-6"> | ||||||
|  |         <div data-testid="issuable-labels" class="form-group row"> | ||||||
|  |           <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{ | ||||||
|  |             __('Labels') | ||||||
|  |           }}</label> | ||||||
|  |           <div class="col-md-8 col-sm-10"> | ||||||
|  |             <div class="issuable-form-select-holder"> | ||||||
|  |               <labels-select | ||||||
|  |                 :allow-label-edit="true" | ||||||
|  |                 :allow-label-create="true" | ||||||
|  |                 :allow-multiselect="true" | ||||||
|  |                 :allow-scoped-labels="true" | ||||||
|  |                 :labels-fetch-path="labelsFetchPath" | ||||||
|  |                 :labels-manage-path="labelsManagePath" | ||||||
|  |                 :selected-labels="selectedLabels" | ||||||
|  |                 :labels-list-title="__('Select label')" | ||||||
|  |                 :footer-create-label-title="__('Create project label')" | ||||||
|  |                 :footer-manage-label-title="__('Manage project labels')" | ||||||
|  |                 :variant="$options.LabelSelectVariant.Embedded" | ||||||
|  |                 @updateSelectedLabels="handleUpdateSelectedLabels" | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div | ||||||
|  |       data-testid="issuable-create-actions" | ||||||
|  |       class="footer-block row-content-block gl-display-flex" | ||||||
|  |     > | ||||||
|  |       <slot | ||||||
|  |         name="actions" | ||||||
|  |         :issuable-title="issuableTitle" | ||||||
|  |         :issuable-description="issuableDescription" | ||||||
|  |         :selected-labels="selectedLabels" | ||||||
|  |       ></slot> | ||||||
|  |     </div> | ||||||
|  |   </gl-form> | ||||||
|  | </template> | ||||||
|  | @ -1,6 +1,14 @@ | ||||||
| <script> | <script> | ||||||
| import { GlLoadingIcon, GlTable } from '@gitlab/ui'; | import { GlLoadingIcon, GlTable } from '@gitlab/ui'; | ||||||
| import { s__ } from '~/locale'; | import { s__ } from '~/locale'; | ||||||
|  | import { | ||||||
|  |   capitalizeFirstCharacter, | ||||||
|  |   convertToSentenceCase, | ||||||
|  |   splitCamelCase, | ||||||
|  | } from '~/lib/utils/text_utility'; | ||||||
|  | 
 | ||||||
|  | const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; | ||||||
|  | const tdClass = 'gl-border-gray-100! gl-p-5!'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|  | @ -18,27 +26,42 @@ export default { | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   tableHeader: { |   fields: [ | ||||||
|     [s__('AlertManagement|Key')]: s__('AlertManagement|Value'), |     { | ||||||
|   }, |       key: 'fieldName', | ||||||
|  |       label: s__('AlertManagement|Key'), | ||||||
|  |       thClass, | ||||||
|  |       tdClass, | ||||||
|  |       formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       key: 'value', | ||||||
|  |       thClass: `${thClass} w-60p`, | ||||||
|  |       tdClass, | ||||||
|  |       label: s__('AlertManagement|Value'), | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|   computed: { |   computed: { | ||||||
|     items() { |     items() { | ||||||
|       if (!this.alert) { |       if (!this.alert) { | ||||||
|         return []; |         return []; | ||||||
|       } |       } | ||||||
|       return [{ ...this.$options.tableHeader, ...this.alert }]; |       return Object.entries(this.alert).map(([fieldName, value]) => ({ | ||||||
|  |         fieldName, | ||||||
|  |         value, | ||||||
|  |       })); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <gl-table |   <gl-table | ||||||
|     class="alert-management-details-table gl-mb-0!" |     class="alert-management-details-table" | ||||||
|     :busy="loading" |     :busy="loading" | ||||||
|     :empty-text="s__('AlertManagement|No alert data to display.')" |     :empty-text="s__('AlertManagement|No alert data to display.')" | ||||||
|     :items="items" |     :items="items" | ||||||
|  |     :fields="$options.fields" | ||||||
|     show-empty |     show-empty | ||||||
|     stacked |  | ||||||
|   > |   > | ||||||
|     <template #table-busy> |     <template #table-busy> | ||||||
|       <gl-loading-icon size="lg" color="dark" class="gl-mt-5" /> |       <gl-loading-icon size="lg" color="dark" class="gl-mt-5" /> | ||||||
|  |  | ||||||
|  | @ -166,7 +166,11 @@ export default { | ||||||
|         !state.showDropdownButton && |         !state.showDropdownButton && | ||||||
|         !state.showDropdownContents |         !state.showDropdownContents | ||||||
|       ) { |       ) { | ||||||
|         this.handleDropdownClose(state.labels.filter(label => label.touched)); |         let filterFn = label => label.touched; | ||||||
|  |         if (this.isDropdownVariantEmbedded) { | ||||||
|  |           filterFn = label => label.set; | ||||||
|  |         } | ||||||
|  |         this.handleDropdownClose(state.labels.filter(filterFn)); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     /** |     /** | ||||||
|  | @ -186,7 +190,7 @@ export default { | ||||||
|       ].some( |       ].some( | ||||||
|         className => |         className => | ||||||
|           target?.classList.contains(className) || |           target?.classList.contains(className) || | ||||||
|           target?.parentElement.classList.contains(className), |           target?.parentElement?.classList.contains(className), | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( |       const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( | ||||||
|  |  | ||||||
|  | @ -1,48 +1,4 @@ | ||||||
| .alert-management-details { | .alert-management-details { | ||||||
|   // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui |  | ||||||
|   table { |  | ||||||
|     tr { |  | ||||||
|       td { |  | ||||||
|         @include gl-border-0; |  | ||||||
|         @include gl-p-5; |  | ||||||
|         border-color: transparent; |  | ||||||
| 
 |  | ||||||
|         &:not(:last-child) { |  | ||||||
|           border-bottom: 1px solid $table-border-color; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         &:first-child { |  | ||||||
|           div { |  | ||||||
|             font-weight: bold; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         &:not(:first-child) { |  | ||||||
|           &::before { |  | ||||||
|             color: $gray-500; |  | ||||||
|             font-weight: normal !important; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           div { |  | ||||||
|             color: $gray-500; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @include media-breakpoint-up(sm) { |  | ||||||
|           div { |  | ||||||
|             text-align: left !important; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       &:last-child { |  | ||||||
|         &::after { |  | ||||||
|           content: none !important; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @include media-breakpoint-down(xs) { |   @include media-breakpoint-down(xs) { | ||||||
|     .alert-details-incident-button { |     .alert-details-incident-button { | ||||||
|       width: 100%; |       width: 100%; | ||||||
|  |  | ||||||
|  | @ -210,6 +210,20 @@ module ObjectStorage | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     class OpenFile | ||||||
|  |       extend Forwardable | ||||||
|  | 
 | ||||||
|  |       # Explicitly exclude :path, because rubyzip uses that to detect "real" files. | ||||||
|  |       def_delegators :@file, *(Zip::File::IO_METHODS - [:path]) | ||||||
|  | 
 | ||||||
|  |       # Even though :size is not in IO_METHODS, we do need it. | ||||||
|  |       def_delegators :@file, :size | ||||||
|  | 
 | ||||||
|  |       def initialize(file) | ||||||
|  |         @file = file | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     # allow to configure and overwrite the filename |     # allow to configure and overwrite the filename | ||||||
|     def filename |     def filename | ||||||
|       @filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables |       @filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables | ||||||
|  | @ -259,6 +273,24 @@ module ObjectStorage | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def use_open_file(&blk) | ||||||
|  |       Tempfile.open(path) do |file| | ||||||
|  |         file.unlink | ||||||
|  |         file.binmode | ||||||
|  | 
 | ||||||
|  |         if file_storage? | ||||||
|  |           IO.copy_stream(path, file) | ||||||
|  |         else | ||||||
|  |           streamer = lambda { |chunk, _, _| file.write(chunk) } | ||||||
|  |           Excon.get(url, response_block: streamer) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         file.seek(0, IO::SEEK_SET) | ||||||
|  | 
 | ||||||
|  |         yield OpenFile.new(file) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     # |     # | ||||||
|     # Move the file to another store |     # Move the file to another store | ||||||
|     # |     # | ||||||
|  |  | ||||||
|  | @ -3,5 +3,5 @@ | ||||||
|     = custom_icon('dev_ops_report_no_data') |     = custom_icon('dev_ops_report_no_data') | ||||||
|     %h4= _('Data is still calculating...') |     %h4= _('Data is still calculating...') | ||||||
|     %p |     %p | ||||||
|       = _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.') |       = _('It may be several days before you see feature usage data.') | ||||||
|       = link_to _('Learn more'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank' |       = link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank' | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ module Analytics | ||||||
|       idempotent! |       idempotent! | ||||||
| 
 | 
 | ||||||
|       def perform |       def perform | ||||||
|         return if Feature.disabled?(:store_instance_statistics_measurements) |         return if Feature.disabled?(:store_instance_statistics_measurements, default_enabled: true) | ||||||
| 
 | 
 | ||||||
|         recorded_at = Time.zone.now |         recorded_at = Time.zone.now | ||||||
|         measurement_identifiers = Analytics::InstanceStatistics::Measurement.identifiers |         measurement_identifiers = Analytics::InstanceStatistics::Measurement.identifiers | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Fix error reporting for Web IDE commits | ||||||
|  | merge_request: 42383 | ||||||
|  | author: | ||||||
|  | type: fixed | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Replace LoadingButton with GlButton for the comment dismissal modal | ||||||
|  | merge_request: 40882 | ||||||
|  | author: | ||||||
|  | type: performance | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Store object counts periodically for instance statistics | ||||||
|  | merge_request: 42433 | ||||||
|  | author: | ||||||
|  | type: changed | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Modify DevOps Score UI Text | ||||||
|  | merge_request: 42256 | ||||||
|  | author: | ||||||
|  | type: other | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | --- | ||||||
|  | name: ci_new_artifact_file_reader | ||||||
|  | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40268 | ||||||
|  | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/249588 | ||||||
|  | group: group::pipeline authoring | ||||||
|  | type: development | ||||||
|  | default_enabled: false | ||||||
|  | @ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41300 | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247871 | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247871 | ||||||
| group: group::analytics | group: group::analytics | ||||||
| type: development | type: development | ||||||
| default_enabled: false | default_enabled: true | ||||||
|  |  | ||||||
|  | @ -76,6 +76,7 @@ exceptions: | ||||||
|   - SCSS |   - SCSS | ||||||
|   - SDK |   - SDK | ||||||
|   - SHA |   - SHA | ||||||
|  |   - SLA | ||||||
|   - SMTP |   - SMTP | ||||||
|   - SQL |   - SQL | ||||||
|   - SSH |   - SSH | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ Note the following when promoting a secondary: | ||||||
| 
 | 
 | ||||||
| - If replication was paused on the secondary node, for example as a part of upgrading, | - If replication was paused on the secondary node, for example as a part of upgrading, | ||||||
|   while you were running a version of GitLab lower than 13.4, you _must_ |   while you were running a version of GitLab lower than 13.4, you _must_ | ||||||
|   [enable the node via the database](#while-promoting-the-secondary-i-got-an-error-activerecordrecordinvalid) |   [enable the node via the database](../replication/troubleshooting.md#while-promoting-the-secondary-i-got-an-error-activerecordrecordinvalid) | ||||||
|   before proceeding. |   before proceeding. | ||||||
| - A new **secondary** should not be added at this time. If you want to add a new | - A new **secondary** should not be added at this time. If you want to add a new | ||||||
|   **secondary**, do this after you have completed the entire process of promoting |   **secondary**, do this after you have completed the entire process of promoting | ||||||
|  | @ -129,28 +129,20 @@ Note the following when promoting a secondary: | ||||||
|    ``` |    ``` | ||||||
| 
 | 
 | ||||||
| 1. Promote the **secondary** node to the **primary** node. | 1. Promote the **secondary** node to the **primary** node. | ||||||
| 
 |     | ||||||
|    Before promoting a secondary node to primary, preflight checks should be run. They can be run separately or along with the promotion script. |  | ||||||
| 
 |  | ||||||
|    To promote the secondary node to primary along with preflight checks: |    To promote the secondary node to primary along with preflight checks: | ||||||
| 
 | 
 | ||||||
|    ```shell |    ```shell | ||||||
|    gitlab-ctl promote-to-primary-node |    gitlab-ctl promote-to-primary-node | ||||||
|    ``` |    ``` | ||||||
| 
 | 
 | ||||||
|    If you have already run the [preflight checks](planned_failover.md#preflight-checks) or don't want to run them, you can skip preflight checks with: |    If you have already run the [preflight checks](planned_failover.md#preflight-checks) separately or don't want to run them, you can skip preflight checks with: | ||||||
| 
 | 
 | ||||||
|    ```shell |    ```shell | ||||||
|    gitlab-ctl promote-to-primary-node --skip-preflight-check |    gitlab-ctl promote-to-primary-node --skip-preflight-check | ||||||
|    ``` |    ``` | ||||||
| 
 | 
 | ||||||
|    You can also run preflight checks separately: |    You can also promote the secondary node to primary **without any further confirmation**, even when preflight checks fail: | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    gitlab-ctl promotion-preflight-checks |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
|    After all the checks are run, you will be asked for a final confirmation before the promotion to primary. To skip this confirmation, run: |  | ||||||
| 
 | 
 | ||||||
|    ```shell |    ```shell | ||||||
|    gitlab-ctl promote-to-primary-node --force |    gitlab-ctl promote-to-primary-node --force | ||||||
|  | @ -421,33 +413,4 @@ for another **primary** node. All the old replication settings will be overwritt | ||||||
| 
 | 
 | ||||||
| ## Troubleshooting | ## Troubleshooting | ||||||
| 
 | 
 | ||||||
| ### I followed the disaster recovery instructions and now two-factor auth is broken | This section was moved to [another location](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-node). | ||||||
| 
 |  | ||||||
| The setup instructions for Geo prior to 10.5 failed to replicate the |  | ||||||
| `otp_key_base` secret, which is used to encrypt the two-factor authentication |  | ||||||
| secrets stored in the database. If it differs between **primary** and **secondary** |  | ||||||
| nodes, users with two-factor authentication enabled won't be able to log in |  | ||||||
| after a failover. |  | ||||||
| 
 |  | ||||||
| If you still have access to the old **primary** node, you can follow the |  | ||||||
| instructions in the |  | ||||||
| [Upgrading to GitLab 10.5](../replication/version_specific_updates.md#updating-to-gitlab-105) |  | ||||||
| section to resolve the error. Otherwise, the secret is lost and you'll need to |  | ||||||
| [reset two-factor authentication for all users](../../../security/two_factor_authentication.md#disabling-2fa-for-everyone). |  | ||||||
| 
 |  | ||||||
| ### While Promoting the secondary, I got an error `ActiveRecord::RecordInvalid` |  | ||||||
| 
 |  | ||||||
| If you disabled a secondary node, either with the [replication pause task](../index.md#pausing-and-resuming-replication) |  | ||||||
| (13.2) or via the UI (13.1 and earlier), you must first re-enable the |  | ||||||
| node before you can continue. This is fixed in 13.4. |  | ||||||
| 
 |  | ||||||
| From `gitlab-psql`, execute the following, replacing  `<your secondary url>` |  | ||||||
| with the URL for your secondary server starting with `http` or `https` and ending with a `/`. |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| SECONDARY_URL="https://<secondary url>/" |  | ||||||
| DATABASE_NAME="gitlabhq_production" |  | ||||||
| sudo gitlab-psql -d "$DATABASE_NAME" -c "UPDATE geo_nodes SET enabled = true WHERE url = '$SECONDARY_URL';" |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| This should update 1 row. |  | ||||||
|  |  | ||||||
|  | @ -51,12 +51,6 @@ Run this command to list out all preflight checks and automatically check if rep | ||||||
| gitlab-ctl promotion-preflight-checks | gitlab-ctl promotion-preflight-checks | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| You can run this command in `force` mode to promote to primary even if preflight checks fail: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| sudo gitlab-ctl promote-to-primary-node --force |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Each step is described in more detail below. | Each step is described in more detail below. | ||||||
| 
 | 
 | ||||||
| ### Object storage | ### Object storage | ||||||
|  |  | ||||||
|  | @ -632,6 +632,23 @@ To double check this, you can do the following: | ||||||
|   UPDATE geo_nodes SET enabled = 't' WHERE id = ID_FROM_ABOVE; |   UPDATE geo_nodes SET enabled = 't' WHERE id = ID_FROM_ABOVE; | ||||||
|   ``` |   ``` | ||||||
| 
 | 
 | ||||||
|  | ### While Promoting the secondary, I got an error `ActiveRecord::RecordInvalid` | ||||||
|  | 
 | ||||||
|  | If you disabled a secondary node, either with the [replication pause task](../index.md#pausing-and-resuming-replication) | ||||||
|  | (13.2) or via the UI (13.1 and earlier), you must first re-enable the | ||||||
|  | node before you can continue. This is fixed in 13.4. | ||||||
|  | 
 | ||||||
|  | From `gitlab-psql`, execute the following, replacing  `<your secondary url>` | ||||||
|  | with the URL for your secondary server starting with `http` or `https` and ending with a `/`. | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | SECONDARY_URL="https://<secondary url>/" | ||||||
|  | DATABASE_NAME="gitlabhq_production" | ||||||
|  | sudo gitlab-psql -d "$DATABASE_NAME" -c "UPDATE geo_nodes SET enabled = true WHERE url = '$SECONDARY_URL';" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | This should update 1 row. | ||||||
|  | 
 | ||||||
| ### Message: ``NoMethodError: undefined method `secondary?' for nil:NilClass`` | ### Message: ``NoMethodError: undefined method `secondary?' for nil:NilClass`` | ||||||
| 
 | 
 | ||||||
| When [promoting a **secondary** node](../disaster_recovery/index.md#step-3-promoting-a-secondary-node), | When [promoting a **secondary** node](../disaster_recovery/index.md#step-3-promoting-a-secondary-node), | ||||||
|  | @ -674,6 +691,20 @@ sudo /opt/gitlab/embedded/bin/gitlab-pg-ctl promote | ||||||
| 
 | 
 | ||||||
| GitLab 12.9 and later are [unaffected by this error](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5147). | GitLab 12.9 and later are [unaffected by this error](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5147). | ||||||
| 
 | 
 | ||||||
|  | ### Two-factor authentication is broken after a failover | ||||||
|  | 
 | ||||||
|  | The setup instructions for Geo prior to 10.5 failed to replicate the | ||||||
|  | `otp_key_base` secret, which is used to encrypt the two-factor authentication | ||||||
|  | secrets stored in the database. If it differs between **primary** and **secondary** | ||||||
|  | nodes, users with two-factor authentication enabled won't be able to log in | ||||||
|  | after a failover. | ||||||
|  | 
 | ||||||
|  | If you still have access to the old **primary** node, you can follow the | ||||||
|  | instructions in the | ||||||
|  | [Upgrading to GitLab 10.5](../replication/version_specific_updates.md#updating-to-gitlab-105) | ||||||
|  | section to resolve the error. Otherwise, the secret is lost and you'll need to | ||||||
|  | [reset two-factor authentication for all users](../../../security/two_factor_authentication.md#disabling-2fa-for-everyone). | ||||||
|  | 
 | ||||||
| ## Expired artifacts | ## Expired artifacts | ||||||
| 
 | 
 | ||||||
| If you notice for some reason there are more artifacts on the Geo | If you notice for some reason there are more artifacts on the Geo | ||||||
|  |  | ||||||
|  | @ -314,7 +314,7 @@ sudo gitlab-ctl reconfigure | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| If you do not perform this step, you may find that two-factor authentication | If you do not perform this step, you may find that two-factor authentication | ||||||
| [is broken following DR](../disaster_recovery/index.md#i-followed-the-disaster-recovery-instructions-and-now-two-factor-auth-is-broken). | [is broken following DR](troubleshooting.md#two-factor-authentication-is-broken-after-a-failover). | ||||||
| 
 | 
 | ||||||
| To prevent SSH requests to the newly promoted **primary** node from failing | To prevent SSH requests to the newly promoted **primary** node from failing | ||||||
| due to SSH host key mismatch when updating the **primary** node domain's DNS record | due to SSH host key mismatch when updating the **primary** node domain's DNS record | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 121 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 236 KiB | 
|  | @ -0,0 +1,251 @@ | ||||||
|  | --- | ||||||
|  | stage: Verify | ||||||
|  | group: Continuous Integration | ||||||
|  | info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||||
|  | type: reference | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | # Pipeline Efficiency | ||||||
|  | 
 | ||||||
|  | [CI/CD Pipelines](index.md) are the fundamental building blocks for [GitLab CI/CD](../README.md). | ||||||
|  | Making pipelines more efficient helps you save developer time, which: | ||||||
|  | 
 | ||||||
|  | - Speeds up your DevOps processes | ||||||
|  | - Reduces costs | ||||||
|  | - Shortens the development feedback loop | ||||||
|  | 
 | ||||||
|  | It's common that new teams or projects start with slow and inefficient pipelines, | ||||||
|  | and improve their configuration over time through trial and error. A better process is | ||||||
|  | to use pipeline features that improve efficiency right away, and get a faster software | ||||||
|  | development lifecycle earlier. | ||||||
|  | 
 | ||||||
|  | First ensure you are familiar with [GitLab CI/CD fundamentals](../introduction/index.md) | ||||||
|  | and understand the [quick start guide](../quick_start/README.md). | ||||||
|  | 
 | ||||||
|  | ## Identify bottlenecks and common failures | ||||||
|  | 
 | ||||||
|  | The easiest indicators to check for inefficient pipelines are the runtimes of the jobs, | ||||||
|  | stages, and the total runtime of the pipeline itself. The total pipeline duration is | ||||||
|  | heavily influenced by the: | ||||||
|  | 
 | ||||||
|  | - Total number of stages and jobs | ||||||
|  | - Dependencies between jobs | ||||||
|  | - The ["critical path"](#directed-acyclic-graphs-dag-visualization), which represents | ||||||
|  |   the minimum and maximum pipeline duration | ||||||
|  | 
 | ||||||
|  | Additional points to pay attention relate to [GitLab Runners](../runners/README.md): | ||||||
|  | 
 | ||||||
|  | - Availability of the runners and the resources they are provisioned with | ||||||
|  | - Build dependencies and their installation time | ||||||
|  | - [Container image size](#docker-images) | ||||||
|  | - Network latency and slow connections | ||||||
|  | 
 | ||||||
|  | Pipelines frequently failing unnecessarily also causes slowdowns in the development | ||||||
|  | lifecycle. You should look for problematic patterns with failed jobs: | ||||||
|  | 
 | ||||||
|  | - Flaky unit tests which fail randomly, or produce unreliable test results. | ||||||
|  | - Test coverage drops and code quality correlated to that behavior. | ||||||
|  | - Failures that can be safely ignored, but that halt the pipeline instead. | ||||||
|  | - Tests that fail at the end of a long pipeline, but could be in an earlier stage, | ||||||
|  |   causing delayed feedback. | ||||||
|  | 
 | ||||||
|  | ## Pipeline analysis | ||||||
|  | 
 | ||||||
|  | Analyze the performance of your pipeline to find ways to improve efficiency. Analysis | ||||||
|  | can help identify possible blockers in the CI/CD infrastructure. This includes analyzing: | ||||||
|  | 
 | ||||||
|  | - Job workloads | ||||||
|  | - Bottlenecks in the execution times | ||||||
|  | - The overall pipeline architecture | ||||||
|  | 
 | ||||||
|  | It's important to understand and document the pipeline workflows, and discuss possible | ||||||
|  | actions and changes. Refactoring pipelines may need careful interaction between teams | ||||||
|  | in the DevSecOps lifecycle. | ||||||
|  | 
 | ||||||
|  | Pipeline analysis can help identify issues with cost efficiency. For example, [runners](../runners/README.md) | ||||||
|  | hosted with a paid cloud service may be provisioned with: | ||||||
|  | 
 | ||||||
|  | - More resources than needed for CI/CD pipelines, wasting money. | ||||||
|  | - Not enough resources, causing slow runtimes and wasting time. | ||||||
|  | 
 | ||||||
|  | ### Pipeline Insights | ||||||
|  | 
 | ||||||
|  | The [Pipeline success and duration charts](index.md#pipeline-success-and-duration-charts) | ||||||
|  | give information about pipeline runtime and failed job counts. | ||||||
|  | 
 | ||||||
|  | Tests like [unit tests](../unit_test_reports.md), integration tests, end-to-end tests, | ||||||
|  | [code quality](../../user/project/merge_requests/code_quality.md) tests, and others | ||||||
|  | ensure that problems are automatically found by the CI/CD pipeline. There could be many | ||||||
|  | pipeline stages involved causing long runtimes. | ||||||
|  | 
 | ||||||
|  | You can improve runtimes by running jobs that test different things in parallel, in | ||||||
|  | the same stage, reducing overall runtime. The downside is that you need more runners | ||||||
|  | running simultaneously to support the parallel jobs. | ||||||
|  | 
 | ||||||
|  | The [testing levels for GitLab](../../development/testing_guide/testing_levels.md) | ||||||
|  | provide an example of a complex testing strategy with many components involved. | ||||||
|  | 
 | ||||||
|  | ### Directed Acyclic Graphs (DAG) visualization | ||||||
|  | 
 | ||||||
|  | The [Directed Acyclic Graph](../directed_acyclic_graph/index.md) (DAG) visualization can help analyze the critical path in | ||||||
|  | the pipeline and understand possible blockers. | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
|  | ### Pipeline Monitoring | ||||||
|  | 
 | ||||||
|  | Global pipeline health is a key indicator to monitor along with job and pipeline duration. | ||||||
|  | [CI/CD analytics](index.md#pipeline-success-and-duration-charts) give a visual | ||||||
|  | representation of pipeline health. | ||||||
|  | 
 | ||||||
|  | Instance administrators have access to additional [performance metrics and self-monitoring](../../administration/monitoring/index.md). | ||||||
|  | 
 | ||||||
|  | You can fetch specific pipeline health metrics from the [API](../../api/README.md). | ||||||
|  | External monitoring tools can poll the API and verify pipeline health or collect | ||||||
|  | metrics for long term SLA analytics. | ||||||
|  | 
 | ||||||
|  | For example, the [GitLab CI Pipelines Exporter](https://github.com/mvisonneau/gitlab-ci-pipelines-exporter) | ||||||
|  | for Prometheus fetches metrics from the API. It can check branches in projects automatically | ||||||
|  | and get the pipeline status and duration. In combination with a Grafana dashboard, | ||||||
|  | this helps build an actionable view for your operations team. Metric graphs can also | ||||||
|  | be embedded into incidents making problem resolving easier. | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
|  | Alternatively, you can use a monitoring tool that can execute scripts, like | ||||||
|  | [`check_gitlab`](https://gitlab.com/6uellerBpanda/check_gitlab) for example. | ||||||
|  | 
 | ||||||
|  | #### Runner monitoring | ||||||
|  | 
 | ||||||
|  | You can also [monitor CI runners](https://docs.gitlab.com/runner/monitoring/) on | ||||||
|  | their host systems, or in clusters like Kubernetes. This includes checking: | ||||||
|  | 
 | ||||||
|  | - Disk and disk IO | ||||||
|  | - CPU usage | ||||||
|  | - Memory | ||||||
|  | - Runner process resources | ||||||
|  | 
 | ||||||
|  | The [Prometheus Node Exporter](https://prometheus.io/docs/guides/node-exporter/) | ||||||
|  | can monitor runners on Linux hosts, and [`kube-state-metrics`](https://github.com/kubernetes/kube-state-metrics) | ||||||
|  | runs in a Kubernetes cluster. | ||||||
|  | 
 | ||||||
|  | You can also test [GitLab Runner auto-scaling](https://docs.gitlab.com/runner/configuration/autoscale.html) | ||||||
|  | with cloud providers, and define offline times to reduce costs. | ||||||
|  | 
 | ||||||
|  | #### Dashboards and incident management | ||||||
|  | 
 | ||||||
|  | Use your existing monitoring tools and dashboards to integrate CI/CD pipeline monitoring, | ||||||
|  | or build them from scratch. Ensure that the runtime data is actionable and useful | ||||||
|  | in teams, and operations/SREs are able to identify problems early enough. | ||||||
|  | [Incident management](../../operations/incident_management/index.md) can help here too, | ||||||
|  | with embedded metric charts and all valuable details to analyze the problem. | ||||||
|  | 
 | ||||||
|  | ### Storage usage | ||||||
|  | 
 | ||||||
|  | Review the storage use of the following to help analyze costs and efficiency: | ||||||
|  | 
 | ||||||
|  | - [Job artifacts](job_artifacts.md) and their [`expire_in`](../yaml/README.md#artifactsexpire_in) | ||||||
|  |   configuration. If kept for too long, storage usage grows and could slow pipelines down. | ||||||
|  | - [Container registry](../../user/packages/container_registry/index.md) usage. | ||||||
|  | - [Package registry](../../user/packages/package_registry/index.md) usage. | ||||||
|  | 
 | ||||||
|  | ## Pipeline configuration | ||||||
|  | 
 | ||||||
|  | Make careful choices when configuring pipelines to speed up pipelines and reduce | ||||||
|  | resource usage. This includes making use of GitLab CI/CD's built-in features that | ||||||
|  | make pipelines run faster and more efficiently. | ||||||
|  | 
 | ||||||
|  | ### Reduce how often jobs run | ||||||
|  | 
 | ||||||
|  | Try to find which jobs don't need to run in all situations, and use pipeline configuration | ||||||
|  | to stop them from running: | ||||||
|  | 
 | ||||||
|  | - Use the [`interruptible`](../yaml/README.md#interruptible) keyword to stop old pipelines | ||||||
|  |   when they are superceded by a newer pipeline. | ||||||
|  | - Use [`rules`](../yaml/README.md#rules) to skip tests that aren't needed. For example, | ||||||
|  |   skip backend tests when only the frontend code is changed. | ||||||
|  | - Run non-essential [scheduled pipelines](schedules.md) less frequently. | ||||||
|  | 
 | ||||||
|  | ### Fail fast | ||||||
|  | 
 | ||||||
|  | Ensure that errors are detected early in the CI/CD pipeline. A job that takes a very long | ||||||
|  | time to complete keeps a pipeline from returning a failed status until the job completes. | ||||||
|  | 
 | ||||||
|  | Design pipelines so that jobs that can [fail fast](../../user/project/merge_requests/fail_fast_testing.md) | ||||||
|  | run earlier. For example, add an early stage and move the syntax, style linting, | ||||||
|  | Git commit message verification, and similar jobs in there. | ||||||
|  | 
 | ||||||
|  | Decide if it's important for long jobs to run early, before fast feedback from | ||||||
|  | faster jobs. The initial failures may make it clear that the rest of the pipeline | ||||||
|  | shouldn't run, saving pipeline resources. | ||||||
|  | 
 | ||||||
|  | ### Directed Acyclic Graphs (DAG) | ||||||
|  | 
 | ||||||
|  | In a basic configuration, jobs always wait for all other jobs in earlier stages to complete | ||||||
|  | before running. This is the simplest configuration, but it's also the slowest in most | ||||||
|  | cases. [Directed Acyclic Graphs](../directed_acyclic_graph/index.md) and | ||||||
|  | [parent/child pipelines](../parent_child_pipelines.md) are more flexible and can | ||||||
|  | be more efficient, but can also make pipelines harder to understand and analyze. | ||||||
|  | 
 | ||||||
|  | ### Caching | ||||||
|  | 
 | ||||||
|  | Another optimization method is to use [caching](../caching/index.md) between jobs and stages, | ||||||
|  | for example [`/node_modules` for NodeJS](../caching/index.md#caching-nodejs-dependencies). | ||||||
|  | 
 | ||||||
|  | ### Docker Images | ||||||
|  | 
 | ||||||
|  | Downloading and initializing Docker images can be a large part of the overall runtime | ||||||
|  | of jobs. | ||||||
|  | 
 | ||||||
|  | If a Docker image is slowing down job execution, analyze the base image size and network | ||||||
|  | connection to the registry. If GitLab is running in the cloud, look for a cloud container | ||||||
|  | registry offered by the vendor. In addition to that, you can make use of the | ||||||
|  | [GitLab container registry](../../user/packages/container_registry/index.md) which can be accessed | ||||||
|  | by the GitLab instance faster than other registries. | ||||||
|  | 
 | ||||||
|  | #### Optimize Docker images | ||||||
|  | 
 | ||||||
|  | Build optimized Docker images because large Docker images use up a lot of space and | ||||||
|  | take a long time to download with slower connection speeds. If possible, avoid using | ||||||
|  | one large image for all jobs. Use multiple smaller images, each for a specific task, | ||||||
|  | that download and run faster. | ||||||
|  | 
 | ||||||
|  | Try to use custom Docker images with the software pre-installed. It's usually much | ||||||
|  | faster to download a larger pre-configured image than to use a common image and install | ||||||
|  | software on it each time. | ||||||
|  | 
 | ||||||
|  | Methods to reduce Docker image size: | ||||||
|  | 
 | ||||||
|  | - Use a small base image, for example `debian-slim`. | ||||||
|  | - Do not install convenience tools like vim, curl, and so on, if they aren't strictly needed. | ||||||
|  | - Create a dedicated development image. | ||||||
|  | - Disable man pages and docs installed by packages to save space. | ||||||
|  | - Reduce the `RUN` layers and combine software installation steps. | ||||||
|  | - If using `apt`, add `--no-install-recommends` to avoid unnecessary packages. | ||||||
|  | - Clean up caches and files that are no longer needed at the end. For example | ||||||
|  |   `rm -rf /var/lib/apt/lists/*` for Debian and Ubuntu, or `yum clean all` for RHEL and CentOS. | ||||||
|  | - Use tools like [dive](https://github.com/wagoodman/dive) or [DockerSlim](https://github.com/docker-slim/docker-slim) | ||||||
|  |   to analyze and shrink images. | ||||||
|  | 
 | ||||||
|  | To simplify Docker image management, you can create a dedicated group for managing | ||||||
|  | [Docker images](../docker/README.md) and test, build and publish them with CI/CD pipelines. | ||||||
|  | 
 | ||||||
|  | ## Test, document, and learn | ||||||
|  | 
 | ||||||
|  | Improving pipelines is an iterative process. Make small changes, monitor the effect, | ||||||
|  | then iterate again. Many small improvements can add up to a large increase in pipeline | ||||||
|  | efficiency. | ||||||
|  | 
 | ||||||
|  | It can help to document the pipeline design and architecture. You can do this with | ||||||
|  | [Mermaid charts in Markdown](../../user/markdown.md#mermaid) directly in the GitLab | ||||||
|  | repository. | ||||||
|  | 
 | ||||||
|  | Document CI/CD pipeline problems and incidents in issues, including research done | ||||||
|  | and solutions found. This helps onboarding new team members, and also helps | ||||||
|  | identify recurring problems with CI pipeline efficiency. | ||||||
|  | 
 | ||||||
|  | ### Learn More | ||||||
|  | 
 | ||||||
|  | - [CI Monitoring Webcast Slides](https://docs.google.com/presentation/d/1ONwIIzRB7GWX-WOSziIIv8fz1ngqv77HO1yVfRooOHM/edit?usp=sharing) | ||||||
|  | - [GitLab.com Monitoring Handbook](https://about.gitlab.com/handbook/engineering/monitoring/) | ||||||
|  | - [Buildings dashboards for operational visibility](https://aws.amazon.com/builders-library/building-dashboards-for-operational-visibility/) | ||||||
|  | @ -86,6 +86,11 @@ If you would like to contribute to GitLab: | ||||||
| - Issues with the | - Issues with the | ||||||
|   [`~Accepting merge requests` label](issue_workflow.md#label-for-community-contributors) |   [`~Accepting merge requests` label](issue_workflow.md#label-for-community-contributors) | ||||||
|   are a great place to start. |   are a great place to start. | ||||||
|  | - Optimizing our tests is another great opportunity to contribute. You can use | ||||||
|  |   [RSpec profiling statistics](https://gitlab-org.gitlab.io/rspec_profiling_stats/) to identify | ||||||
|  |   slowest tests. These tests are good candidates for improving and checking if any of | ||||||
|  |   [best practices](../testing_guide/best_practices.md) | ||||||
|  |   could speed them up. | ||||||
| - Consult the [Contribution Flow](#contribution-flow) section to learn the process. | - Consult the [Contribution Flow](#contribution-flow) section to learn the process. | ||||||
| 
 | 
 | ||||||
| If you have any questions or need help visit [Getting Help](https://about.gitlab.com/get-help/) to | If you have any questions or need help visit [Getting Help](https://about.gitlab.com/get-help/) to | ||||||
|  |  | ||||||
|  | @ -47,13 +47,13 @@ Full details can be found in the [Elasticsearch documentation](https://www.elast | ||||||
| here's a quick guide: | here's a quick guide: | ||||||
| 
 | 
 | ||||||
| - Searches look for all the words in a query, in any order - e.g.: searching | - Searches look for all the words in a query, in any order - e.g.: searching | ||||||
|   issues for `display bug` will return all issues matching both those words, in any order. |   issues for [`display bug`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=display+bug&group_id=9970&project_id=278964) and [`bug display`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+Display&group_id=9970&project_id=278964) will return the same results. | ||||||
| - To find the exact phrase (stemming still applies), use double quotes: `"display bug"` | - To find the exact phrase (stemming still applies), use double quotes: [`"display bug"`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=%22display+bug%22&group_id=9970&project_id=278964) | ||||||
| - To find bugs not mentioning display, use `-`: `bug -display` | - To find bugs not mentioning display, use `-`: [`bug -display`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+-display&group_id=9970&project_id=278964) | ||||||
| - To find a bug in display or sound, use `|`: `bug display | sound` | - To find a bug in display or banner, use `|`: [`bug display | banner`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+display+%7C+banner&group_id=9970&project_id=278964) | ||||||
| - To group terms together, use parentheses: `bug | (display +sound)` | - To group terms together, use parentheses: [`bug | (display +banner)`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+%7C+%28display+%2Bbanner%29&group_id=9970&project_id=278964) | ||||||
| - To match a partial word, use `*`: `bug find_by_*` | - To match a partial word, use `*`. In this example, I want to find bugs with any 500 errors. : [`bug error 50*`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+error+50*&group_id=9970&project_id=278964) | ||||||
| - To find a term containing one of these symbols, use `\`: `argument \-last` | - To use one of symbols above literally, escape the symbol with a preceding `\`: [`argument \-last`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=argument+%5C-last&group_id=9970&project_id=278964) | ||||||
| 
 | 
 | ||||||
| ### Syntax search filters | ### Syntax search filters | ||||||
| 
 | 
 | ||||||
|  | @ -68,11 +68,11 @@ To use them, simply add them to your query in the format `<filter_name>:<value>` | ||||||
| 
 | 
 | ||||||
| Examples: | Examples: | ||||||
| 
 | 
 | ||||||
| - Finding a file with any content named `hello_world.rb`: `* filename:hello_world.rb` | - Finding a file with any content named `search_results.rb`: [`* filename:search_results.rb`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=*+filename%3Asearch_results.rb&group_id=9970&project_id=278964) | ||||||
| - Finding a file named `hello_world` with the text `whatever` inside of it: `whatever filename:hello_world` | - Finding a file named `found_blob_spec.rb` with the text `CHANGELOG` inside of it: [`CHANGELOG filename:found_blob_spec.rb](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=CHANGELOG+filename%3Afound_blob_spec.rb&group_id=9970&project_id=278964) | ||||||
| - Finding the text 'def create' inside files with the `.rb` extension: `def create extension:rb` | - Finding the text `EpicLinks` inside files with the `.rb` extension: [`EpicLinks extension:rb`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=EpicLinks+extension%3Arb&group_id=9970&project_id=278964) | ||||||
| - Finding the text `sha` inside files in a folder called `encryption`: `sha path:encryption` | - Finding the text `Sidekiq` in a file, when that file is in a path that includes `elastic`: [`Sidekiq path:elastic`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=Sidekiq+path%3Aelastic&group_id=9970&project_id=278964) | ||||||
| - Finding any file starting with `hello` containing `world` and with the `.js` extension: `world filename:hello* extension:js` | - Syntax filters can be combined for complex filtering. Finding any file starting with `search` containing `eventHub` and with the `.js` extension: [`eventHub filename:search* extension:js`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=eventHub+filename%3Asearch*+extension%3Ajs&group_id=9970&project_id=278964) | ||||||
| 
 | 
 | ||||||
| #### Excluding filters | #### Excluding filters | ||||||
| 
 | 
 | ||||||
|  | @ -86,7 +86,7 @@ Filters can be inversed to **filter out** results from the result set, by prefix | ||||||
| 
 | 
 | ||||||
| Examples: | Examples: | ||||||
| 
 | 
 | ||||||
| - Finding `rails` in all files but `Gemfile.lock`: `rails -filename:Gemfile.lock` | - Finding `rails` in all files but `Gemfile.lock`: [`rails -filename:Gemfile.lock`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=rails+-filename%3AGemfile.lock&group_id=9970&project_id=278964) | ||||||
| - Finding `success` in all files excluding `.po|pot` files: `success -filename:*.po*` | - Finding `success` in all files excluding `.po|pot` files: [`success -filename:*.po*`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=success+-filename%3A*.po*&group_id=9970&project_id=278964) | ||||||
| - Finding `import` excluding minified JavaScript (`.min.js`) files: `import -extension:min.js` | - Finding `import` excluding minified JavaScript (`.min.js`) files: [`import -extension:min.js`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=import+-extension%3Amin.js&group_id=9970&project_id=278964) | ||||||
| - Finding `docs` for all files outside the `docs/` folder: `docs -path:docs/` | - Finding `docs` for all files outside the `docs/` folder: [`docs -path:docs/`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=docs+-path%3Adocs%2F&group_id=9970&project_id=278964) | ||||||
|  |  | ||||||
|  | @ -45,6 +45,31 @@ module Gitlab | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def read_zip_file!(file_path) |       def read_zip_file!(file_path) | ||||||
|  |         if ::Gitlab::Ci::Features.new_artifact_file_reader_enabled?(job.project) | ||||||
|  |           read_with_new_artifact_file_reader(file_path) | ||||||
|  |         else | ||||||
|  |           read_with_legacy_artifact_file_reader(file_path) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def read_with_new_artifact_file_reader(file_path) | ||||||
|  |         job.artifacts_file.use_open_file do |file| | ||||||
|  |           zip_file = Zip::File.new(file, false, true) | ||||||
|  |           entry = zip_file.find_entry(file_path) | ||||||
|  | 
 | ||||||
|  |           unless entry | ||||||
|  |             raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!" | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           if entry.name_is_directory? | ||||||
|  |             raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!" | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           zip_file.read(entry) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def read_with_legacy_artifact_file_reader(file_path) | ||||||
|         job.artifacts_file.use_file do |archive_path| |         job.artifacts_file.use_file do |archive_path| | ||||||
|           Zip::File.open(archive_path) do |zip_file| |           Zip::File.open(archive_path) do |zip_file| | ||||||
|             entry = zip_file.find_entry(file_path) |             entry = zip_file.find_entry(file_path) | ||||||
|  |  | ||||||
|  | @ -78,6 +78,10 @@ module Gitlab | ||||||
|         ::Feature.enabled?(:ci_enable_live_trace, project) && |         ::Feature.enabled?(:ci_enable_live_trace, project) && | ||||||
|           ::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: false) |           ::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: false) | ||||||
|       end |       end | ||||||
|  | 
 | ||||||
|  |       def self.new_artifact_file_reader_enabled?(project) | ||||||
|  |         ::Feature.enabled?(:ci_new_artifact_file_reader, project, default_enabled: false) | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -3,8 +3,6 @@ | ||||||
| module Gitlab | module Gitlab | ||||||
|   module UsageDataCounters |   module UsageDataCounters | ||||||
|     module HLLRedisCounter |     module HLLRedisCounter | ||||||
|       include Gitlab::Utils::UsageData |  | ||||||
| 
 |  | ||||||
|       DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH = 6.weeks |       DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH = 6.weeks | ||||||
|       DEFAULT_DAILY_KEY_EXPIRY_LENGTH = 29.days |       DEFAULT_DAILY_KEY_EXPIRY_LENGTH = 29.days | ||||||
|       DEFAULT_REDIS_SLOT = ''.freeze |       DEFAULT_REDIS_SLOT = ''.freeze | ||||||
|  | @ -33,6 +31,8 @@ module Gitlab | ||||||
|       # * Track event: Gitlab::UsageDataCounters::HLLRedisCounter.track_event(user_id, 'g_compliance_dashboard') |       # * Track event: Gitlab::UsageDataCounters::HLLRedisCounter.track_event(user_id, 'g_compliance_dashboard') | ||||||
|       # * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current) |       # * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current) | ||||||
|       class << self |       class << self | ||||||
|  |         include Gitlab::Utils::UsageData | ||||||
|  | 
 | ||||||
|         def track_event(entity_id, event_name, time = Time.zone.now) |         def track_event(entity_id, event_name, time = Time.zone.now) | ||||||
|           return unless Gitlab::CurrentSettings.usage_ping_enabled? |           return unless Gitlab::CurrentSettings.usage_ping_enabled? | ||||||
| 
 | 
 | ||||||
|  | @ -54,7 +54,7 @@ module Gitlab | ||||||
| 
 | 
 | ||||||
|           keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date) |           keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date) | ||||||
| 
 | 
 | ||||||
|           Gitlab::Redis::HLL.count(keys: keys) |           redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) } | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def categories |         def categories | ||||||
|  |  | ||||||
|  | @ -2678,9 +2678,6 @@ msgstr "" | ||||||
| msgid "An error occurred while checking group path. Please refresh and try again." | msgid "An error occurred while checking group path. Please refresh and try again." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "An error occurred while committing your changes." |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "An error occurred while creating the issue. Please try again." | msgid "An error occurred while creating the issue. Please try again." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -4114,7 +4111,7 @@ msgstr "" | ||||||
| msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" | msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "Branch has changed" | msgid "Branch changed" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "Branch is already taken" | msgid "Branch is already taken" | ||||||
|  | @ -4420,6 +4417,9 @@ msgstr "" | ||||||
| msgid "CLOSED (MOVED)" | msgid "CLOSED (MOVED)" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "CODEOWNERS rule violation" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "CONTRIBUTING" | msgid "CONTRIBUTING" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -7134,6 +7134,9 @@ msgstr "" | ||||||
| msgid "Could not change HEAD: branch '%{branch}' does not exist" | msgid "Could not change HEAD: branch '%{branch}' does not exist" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Could not commit. An unexpected error occurred." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Could not connect to FogBugz, check your URL" | msgid "Could not connect to FogBugz, check your URL" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -13324,9 +13327,6 @@ msgstr "" | ||||||
| msgid "In order to enable Service Desk for your instance, you must first set up incoming email." | msgid "In order to enable Service Desk for your instance, you must first set up incoming email." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index." |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "In order to personalize your experience with GitLab%{br_tag}we would like to know a bit more about you." | msgid "In order to personalize your experience with GitLab%{br_tag}we would like to know a bit more about you." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -14013,6 +14013,9 @@ msgstr "" | ||||||
| msgid "It looks like you have some draft commits in this branch." | msgid "It looks like you have some draft commits in this branch." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "It may be several days before you see feature usage data." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected." | msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -17757,6 +17760,9 @@ msgstr "" | ||||||
| msgid "Other visibility settings have been disabled by the administrator." | msgid "Other visibility settings have been disabled by the administrator." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Our documentation includes an example DevOps Score report." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Out-of-compliance with this project's policies and should be removed" | msgid "Out-of-compliance with this project's policies and should be removed" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -25705,9 +25711,6 @@ msgstr "" | ||||||
| msgid "This board's scope is reduced" | msgid "This board's scope is reduced" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "This branch has changed since you started editing. Would you like to create a new branch?" |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "This chart could not be displayed" | msgid "This chart could not be displayed" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -27056,6 +27059,9 @@ msgstr "" | ||||||
| msgid "Undo ignore" | msgid "Undo ignore" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Unexpected error" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Unfortunately, your email message to GitLab could not be processed." | msgid "Unfortunately, your email message to GitLab could not be processed." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -28715,6 +28721,9 @@ msgstr "" | ||||||
| msgid "Workflow Help" | msgid "Workflow Help" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Would you like to create a new branch?" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Write" | msgid "Write" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -110,7 +110,7 @@ function get_job_id() { | ||||||
|     let "page++" |     let "page++" | ||||||
|   done |   done | ||||||
| 
 | 
 | ||||||
|   if [[ "${job_id}" == "" ]]; then |   if [[ "${job_id}" == "null" ]]; then # jq prints "null" for non-existent attribute | ||||||
|     echoerr "The '${job_name}' job ID couldn't be retrieved!" |     echoerr "The '${job_name}' job ID couldn't be retrieved!" | ||||||
|   else |   else | ||||||
|     echoinfo "The '${job_name}' job ID is ${job_id}" |     echoinfo "The '${job_name}' job ID is ${job_id}" | ||||||
|  | @ -142,7 +142,7 @@ function fail_pipeline_early() { | ||||||
|   local dont_interrupt_me_job_id |   local dont_interrupt_me_job_id | ||||||
|   dont_interrupt_me_job_id=$(get_job_id 'dont-interrupt-me' 'scope=success') |   dont_interrupt_me_job_id=$(get_job_id 'dont-interrupt-me' 'scope=success') | ||||||
| 
 | 
 | ||||||
|   if [[ "${dont_interrupt_me_job_id}" != "" ]]; then |   if [[ -n "${dont_interrupt_me_job_id}" ]]; then | ||||||
|     echoinfo "This pipeline cannot be interrupted due to \`dont-interrupt-me\` job ${dont_interrupt_me_job_id}" |     echoinfo "This pipeline cannot be interrupted due to \`dont-interrupt-me\` job ${dont_interrupt_me_job_id}" | ||||||
|   else |   else | ||||||
|     echoinfo "Failing pipeline early for fast feedback due to test failures in rspec fail-fast." |     echoinfo "Failing pipeline early for fast feedback due to test failures in rspec fail-fast." | ||||||
|  |  | ||||||
|  | @ -1,10 +1,13 @@ | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  | import { getByText } from '@testing-library/dom'; | ||||||
| import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; | import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; | ||||||
| import { projectData } from 'jest/ide/mock_data'; | import { projectData } from 'jest/ide/mock_data'; | ||||||
| import waitForPromises from 'helpers/wait_for_promises'; | import waitForPromises from 'helpers/wait_for_promises'; | ||||||
| import { createStore } from '~/ide/stores'; | import { createStore } from '~/ide/stores'; | ||||||
|  | import consts from '~/ide/stores/modules/commit/constants'; | ||||||
| import CommitForm from '~/ide/components/commit_sidebar/form.vue'; | import CommitForm from '~/ide/components/commit_sidebar/form.vue'; | ||||||
| import { leftSidebarViews } from '~/ide/constants'; | import { leftSidebarViews } from '~/ide/constants'; | ||||||
|  | import { createCodeownersCommitError, createUnexpectedCommitError } from '~/ide/lib/errors'; | ||||||
| 
 | 
 | ||||||
| describe('IDE commit form', () => { | describe('IDE commit form', () => { | ||||||
|   const Component = Vue.extend(CommitForm); |   const Component = Vue.extend(CommitForm); | ||||||
|  | @ -259,21 +262,47 @@ describe('IDE commit form', () => { | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('opens new branch modal if commitChanges throws an error', () => { |       it.each` | ||||||
|         vm.commitChanges.mockRejectedValue({ success: false }); |         createError                                          | props | ||||||
|  |         ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }} | ||||||
|  |         ${createUnexpectedCommitError}                       | ${{ actionPrimary: null }} | ||||||
|  |       `('opens error modal if commitError with $error', async ({ createError, props }) => {
 | ||||||
|  |         jest.spyOn(vm.$refs.commitErrorModal, 'show'); | ||||||
| 
 | 
 | ||||||
|         jest.spyOn(vm.$refs.createBranchModal, 'show').mockImplementation(); |         const error = createError(); | ||||||
|  |         store.state.commit.commitError = error; | ||||||
| 
 | 
 | ||||||
|         return vm |         await vm.$nextTick(); | ||||||
|           .$nextTick() |  | ||||||
|           .then(() => { |  | ||||||
|             vm.$el.querySelector('.btn-success').click(); |  | ||||||
| 
 | 
 | ||||||
|             return vm.$nextTick(); |         expect(vm.$refs.commitErrorModal.show).toHaveBeenCalled(); | ||||||
|           }) |         expect(vm.$refs.commitErrorModal).toMatchObject({ | ||||||
|           .then(() => { |           actionCancel: { text: 'Cancel' }, | ||||||
|             expect(vm.$refs.createBranchModal.show).toHaveBeenCalled(); |           ...props, | ||||||
|           }); |         }); | ||||||
|  |         // Because of the legacy 'mountComponent' approach here, the only way to
 | ||||||
|  |         // test the text of the modal is by viewing the content of the modal added to the document.
 | ||||||
|  |         expect(document.body).toHaveText(error.messageHTML); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('with error modal with primary', () => { | ||||||
|  |       beforeEach(() => { | ||||||
|  |         jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve()); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('updates commit action and commits', async () => { | ||||||
|  |         store.state.commit.commitError = createCodeownersCommitError('test message'); | ||||||
|  | 
 | ||||||
|  |         await vm.$nextTick(); | ||||||
|  | 
 | ||||||
|  |         getByText(document.body, 'Create new branch').click(); | ||||||
|  | 
 | ||||||
|  |         await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |         expect(vm.$store.dispatch.mock.calls).toEqual([ | ||||||
|  |           ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH], | ||||||
|  |           ['commit/commitChanges', undefined], | ||||||
|  |         ]); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,70 @@ | ||||||
|  | import { | ||||||
|  |   createUnexpectedCommitError, | ||||||
|  |   createCodeownersCommitError, | ||||||
|  |   createBranchChangedCommitError, | ||||||
|  |   parseCommitError, | ||||||
|  | } from '~/ide/lib/errors'; | ||||||
|  | 
 | ||||||
|  | const TEST_SPECIAL = '&special<'; | ||||||
|  | const TEST_SPECIAL_ESCAPED = '&special<'; | ||||||
|  | const TEST_MESSAGE = 'Test message.'; | ||||||
|  | const CODEOWNERS_MESSAGE = | ||||||
|  |   'Push to protected branches that contain changes to files matching CODEOWNERS is not allowed'; | ||||||
|  | const CHANGED_MESSAGE = 'Things changed since you started editing'; | ||||||
|  | 
 | ||||||
|  | describe('~/ide/lib/errors', () => { | ||||||
|  |   const createResponseError = message => ({ | ||||||
|  |     response: { | ||||||
|  |       data: { | ||||||
|  |         message, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('createCodeownersCommitError', () => { | ||||||
|  |     it('uses given message', () => { | ||||||
|  |       expect(createCodeownersCommitError(TEST_MESSAGE)).toEqual({ | ||||||
|  |         title: 'CODEOWNERS rule violation', | ||||||
|  |         messageHTML: TEST_MESSAGE, | ||||||
|  |         canCreateBranch: true, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('escapes special chars', () => { | ||||||
|  |       expect(createCodeownersCommitError(TEST_SPECIAL)).toEqual({ | ||||||
|  |         title: 'CODEOWNERS rule violation', | ||||||
|  |         messageHTML: TEST_SPECIAL_ESCAPED, | ||||||
|  |         canCreateBranch: true, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('createBranchChangedCommitError', () => { | ||||||
|  |     it.each` | ||||||
|  |       message         | expectedMessage | ||||||
|  |       ${TEST_MESSAGE} | ${`${TEST_MESSAGE}<br/><br/>Would you like to create a new branch?`} | ||||||
|  |       ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}<br/><br/>Would you like to create a new branch?`} | ||||||
|  |     `('uses given message="$message"', ({ message, expectedMessage }) => {
 | ||||||
|  |       expect(createBranchChangedCommitError(message)).toEqual({ | ||||||
|  |         title: 'Branch changed', | ||||||
|  |         messageHTML: expectedMessage, | ||||||
|  |         canCreateBranch: true, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('parseCommitError', () => { | ||||||
|  |     it.each` | ||||||
|  |       message                                    | expectation | ||||||
|  |       ${null}                                    | ${createUnexpectedCommitError()} | ||||||
|  |       ${{}}                                      | ${createUnexpectedCommitError()} | ||||||
|  |       ${{ response: {} }}                        | ${createUnexpectedCommitError()} | ||||||
|  |       ${{ response: { data: {} } }}              | ${createUnexpectedCommitError()} | ||||||
|  |       ${createResponseError('test')}             | ${createUnexpectedCommitError()} | ||||||
|  |       ${createResponseError(CODEOWNERS_MESSAGE)} | ${createCodeownersCommitError(CODEOWNERS_MESSAGE)} | ||||||
|  |       ${createResponseError(CHANGED_MESSAGE)}    | ${createBranchChangedCommitError(CHANGED_MESSAGE)} | ||||||
|  |     `('parses message into error object with "$message"', ({ message, expectation }) => {
 | ||||||
|  |       expect(parseCommitError(message)).toEqual(expectation); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -9,6 +9,7 @@ import eventHub from '~/ide/eventhub'; | ||||||
| import consts from '~/ide/stores/modules/commit/constants'; | import consts from '~/ide/stores/modules/commit/constants'; | ||||||
| import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; | import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; | ||||||
| import * as actions from '~/ide/stores/modules/commit/actions'; | import * as actions from '~/ide/stores/modules/commit/actions'; | ||||||
|  | import { createUnexpectedCommitError } from '~/ide/lib/errors'; | ||||||
| import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants'; | import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants'; | ||||||
| import testAction from '../../../../helpers/vuex_action_helper'; | import testAction from '../../../../helpers/vuex_action_helper'; | ||||||
| 
 | 
 | ||||||
|  | @ -510,7 +511,7 @@ describe('IDE commit module actions', () => { | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('failed', () => { |     describe('success response with failed message', () => { | ||||||
|       beforeEach(() => { |       beforeEach(() => { | ||||||
|         jest.spyOn(service, 'commit').mockResolvedValue({ |         jest.spyOn(service, 'commit').mockResolvedValue({ | ||||||
|           data: { |           data: { | ||||||
|  | @ -533,6 +534,25 @@ describe('IDE commit module actions', () => { | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     describe('failed response', () => { | ||||||
|  |       beforeEach(() => { | ||||||
|  |         jest.spyOn(service, 'commit').mockRejectedValue({}); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('commits error updates', async () => { | ||||||
|  |         jest.spyOn(store, 'commit'); | ||||||
|  | 
 | ||||||
|  |         await store.dispatch('commit/commitChanges').catch(() => {}); | ||||||
|  | 
 | ||||||
|  |         expect(store.commit.mock.calls).toEqual([ | ||||||
|  |           ['commit/CLEAR_ERROR', undefined, undefined], | ||||||
|  |           ['commit/UPDATE_LOADING', true, undefined], | ||||||
|  |           ['commit/UPDATE_LOADING', false, undefined], | ||||||
|  |           ['commit/SET_ERROR', createUnexpectedCommitError(), undefined], | ||||||
|  |         ]); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     describe('first commit of a branch', () => { |     describe('first commit of a branch', () => { | ||||||
|       const COMMIT_RESPONSE = { |       const COMMIT_RESPONSE = { | ||||||
|         id: '123456', |         id: '123456', | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import commitState from '~/ide/stores/modules/commit/state'; | import commitState from '~/ide/stores/modules/commit/state'; | ||||||
| import mutations from '~/ide/stores/modules/commit/mutations'; | import mutations from '~/ide/stores/modules/commit/mutations'; | ||||||
|  | import * as types from '~/ide/stores/modules/commit/mutation_types'; | ||||||
| 
 | 
 | ||||||
| describe('IDE commit module mutations', () => { | describe('IDE commit module mutations', () => { | ||||||
|   let state; |   let state; | ||||||
|  | @ -62,4 +63,24 @@ describe('IDE commit module mutations', () => { | ||||||
|       expect(state.shouldCreateMR).toBe(false); |       expect(state.shouldCreateMR).toBe(false); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   describe(types.CLEAR_ERROR, () => { | ||||||
|  |     it('should clear commitError', () => { | ||||||
|  |       state.commitError = {}; | ||||||
|  | 
 | ||||||
|  |       mutations[types.CLEAR_ERROR](state); | ||||||
|  | 
 | ||||||
|  |       expect(state.commitError).toBeNull(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe(types.SET_ERROR, () => { | ||||||
|  |     it('should set commitError', () => { | ||||||
|  |       const error = { title: 'foo' }; | ||||||
|  | 
 | ||||||
|  |       mutations[types.SET_ERROR](state, error); | ||||||
|  | 
 | ||||||
|  |       expect(state.commitError).toBe(error); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,64 @@ | ||||||
|  | import { mount } from '@vue/test-utils'; | ||||||
|  | 
 | ||||||
|  | import IssuableCreateRoot from '~/issuable_create/components/issuable_create_root.vue'; | ||||||
|  | import IssuableForm from '~/issuable_create/components/issuable_form.vue'; | ||||||
|  | 
 | ||||||
|  | const createComponent = ({ | ||||||
|  |   descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown', | ||||||
|  |   descriptionHelpPath = '/help/user/markdown', | ||||||
|  |   labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json', | ||||||
|  |   labelsManagePath = '/gitlab-org/gitlab-shell/-/labels', | ||||||
|  | } = {}) => { | ||||||
|  |   return mount(IssuableCreateRoot, { | ||||||
|  |     propsData: { | ||||||
|  |       descriptionPreviewPath, | ||||||
|  |       descriptionHelpPath, | ||||||
|  |       labelsFetchPath, | ||||||
|  |       labelsManagePath, | ||||||
|  |     }, | ||||||
|  |     slots: { | ||||||
|  |       title: ` | ||||||
|  |         <h1 class="js-create-title">New Issuable</h1> | ||||||
|  |       `,
 | ||||||
|  |       actions: ` | ||||||
|  |         <button class="js-issuable-save">Submit issuable</button> | ||||||
|  |       `,
 | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | describe('IssuableCreateRoot', () => { | ||||||
|  |   let wrapper; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     wrapper = createComponent(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   afterEach(() => { | ||||||
|  |     wrapper.destroy(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('template', () => { | ||||||
|  |     it('renders component container element with class "issuable-create-container"', () => { | ||||||
|  |       expect(wrapper.classes()).toContain('issuable-create-container'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders contents for slot "title"', () => { | ||||||
|  |       const titleEl = wrapper.find('h1.js-create-title'); | ||||||
|  | 
 | ||||||
|  |       expect(titleEl.exists()).toBe(true); | ||||||
|  |       expect(titleEl.text()).toBe('New Issuable'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders issuable-form component', () => { | ||||||
|  |       expect(wrapper.find(IssuableForm).exists()).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders contents for slot "actions" within issuable-form component', () => { | ||||||
|  |       const buttonEl = wrapper.find(IssuableForm).find('button.js-issuable-save'); | ||||||
|  | 
 | ||||||
|  |       expect(buttonEl.exists()).toBe(true); | ||||||
|  |       expect(buttonEl.text()).toBe('Submit issuable'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -0,0 +1,118 @@ | ||||||
|  | import { shallowMount } from '@vue/test-utils'; | ||||||
|  | import { GlFormInput } from '@gitlab/ui'; | ||||||
|  | 
 | ||||||
|  | import MarkdownField from '~/vue_shared/components/markdown/field.vue'; | ||||||
|  | import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; | ||||||
|  | 
 | ||||||
|  | import IssuableForm from '~/issuable_create/components/issuable_form.vue'; | ||||||
|  | 
 | ||||||
|  | const createComponent = ({ | ||||||
|  |   descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown', | ||||||
|  |   descriptionHelpPath = '/help/user/markdown', | ||||||
|  |   labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json', | ||||||
|  |   labelsManagePath = '/gitlab-org/gitlab-shell/-/labels', | ||||||
|  | } = {}) => { | ||||||
|  |   return shallowMount(IssuableForm, { | ||||||
|  |     propsData: { | ||||||
|  |       descriptionPreviewPath, | ||||||
|  |       descriptionHelpPath, | ||||||
|  |       labelsFetchPath, | ||||||
|  |       labelsManagePath, | ||||||
|  |     }, | ||||||
|  |     slots: { | ||||||
|  |       actions: ` | ||||||
|  |         <button class="js-issuable-save">Submit issuable</button> | ||||||
|  |       `,
 | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | describe('IssuableForm', () => { | ||||||
|  |   let wrapper; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     wrapper = createComponent(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   afterEach(() => { | ||||||
|  |     wrapper.destroy(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('methods', () => { | ||||||
|  |     describe('handleUpdateSelectedLabels', () => { | ||||||
|  |       it('sets provided `labels` param to prop `selectedLabels`', () => { | ||||||
|  |         const labels = [ | ||||||
|  |           { | ||||||
|  |             id: 1, | ||||||
|  |             color: '#BADA55', | ||||||
|  |             text_color: '#ffffff', | ||||||
|  |             title: 'Documentation', | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         wrapper.vm.handleUpdateSelectedLabels(labels); | ||||||
|  | 
 | ||||||
|  |         expect(wrapper.vm.selectedLabels).toBe(labels); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('template', () => { | ||||||
|  |     it('renders issuable title input field', () => { | ||||||
|  |       const titleFieldEl = wrapper.find('[data-testid="issuable-title"]'); | ||||||
|  | 
 | ||||||
|  |       expect(titleFieldEl.exists()).toBe(true); | ||||||
|  |       expect(titleFieldEl.find('label').text()).toBe('Title'); | ||||||
|  |       expect(titleFieldEl.find(GlFormInput).exists()).toBe(true); | ||||||
|  |       expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders issuable description input field', () => { | ||||||
|  |       const descriptionFieldEl = wrapper.find('[data-testid="issuable-description"]'); | ||||||
|  | 
 | ||||||
|  |       expect(descriptionFieldEl.exists()).toBe(true); | ||||||
|  |       expect(descriptionFieldEl.find('label').text()).toBe('Description'); | ||||||
|  |       expect(descriptionFieldEl.find(MarkdownField).exists()).toBe(true); | ||||||
|  |       expect(descriptionFieldEl.find(MarkdownField).props()).toMatchObject({ | ||||||
|  |         markdownPreviewPath: wrapper.vm.descriptionPreviewPath, | ||||||
|  |         markdownDocsPath: wrapper.vm.descriptionHelpPath, | ||||||
|  |         addSpacingClasses: false, | ||||||
|  |         showSuggestPopover: true, | ||||||
|  |       }); | ||||||
|  |       expect(descriptionFieldEl.find('textarea').exists()).toBe(true); | ||||||
|  |       expect(descriptionFieldEl.find('textarea').attributes('placeholder')).toBe( | ||||||
|  |         'Write a comment or drag your files here…', | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders labels select field', () => { | ||||||
|  |       const labelsSelectEl = wrapper.find('[data-testid="issuable-labels"]'); | ||||||
|  | 
 | ||||||
|  |       expect(labelsSelectEl.exists()).toBe(true); | ||||||
|  |       expect(labelsSelectEl.find('label').text()).toBe('Labels'); | ||||||
|  |       expect(labelsSelectEl.find(LabelsSelect).exists()).toBe(true); | ||||||
|  |       expect(labelsSelectEl.find(LabelsSelect).props()).toMatchObject({ | ||||||
|  |         allowLabelEdit: true, | ||||||
|  |         allowLabelCreate: true, | ||||||
|  |         allowMultiselect: true, | ||||||
|  |         allowScopedLabels: true, | ||||||
|  |         labelsFetchPath: wrapper.vm.labelsFetchPath, | ||||||
|  |         labelsManagePath: wrapper.vm.labelsManagePath, | ||||||
|  |         selectedLabels: wrapper.vm.selectedLabels, | ||||||
|  |         labelsListTitle: 'Select label', | ||||||
|  |         footerCreateLabelTitle: 'Create project label', | ||||||
|  |         footerManageLabelTitle: 'Manage project labels', | ||||||
|  |         variant: 'embedded', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders contents for slot "actions"', () => { | ||||||
|  |       const buttonEl = wrapper | ||||||
|  |         .find('[data-testid="issuable-create-actions"]') | ||||||
|  |         .find('button.js-issuable-save'); | ||||||
|  | 
 | ||||||
|  |       expect(buttonEl.exists()).toBe(true); | ||||||
|  |       expect(buttonEl.text()).toBe('Submit issuable'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -65,6 +65,33 @@ describe('LabelsSelectRoot', () => { | ||||||
|           ]), |           ]), | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|  | 
 | ||||||
|  |       it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => { | ||||||
|  |         wrapper = createComponent({ | ||||||
|  |           ...mockConfig, | ||||||
|  |           variant: 'embedded', | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation(); | ||||||
|  | 
 | ||||||
|  |         wrapper.vm.handleVuexActionDispatch( | ||||||
|  |           { type: 'toggleDropdownContents' }, | ||||||
|  |           { | ||||||
|  |             showDropdownButton: false, | ||||||
|  |             showDropdownContents: false, | ||||||
|  |             labels: [{ id: 1 }, { id: 2, set: true }], | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith( | ||||||
|  |           expect.arrayContaining([ | ||||||
|  |             { | ||||||
|  |               id: 2, | ||||||
|  |               set: true, | ||||||
|  |             }, | ||||||
|  |           ]), | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('handleDropdownClose', () => { |     describe('handleDropdownClose', () => { | ||||||
|  |  | ||||||
|  | @ -18,6 +18,17 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do | ||||||
|         expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') |         expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       context 'when FF ci_new_artifact_file_reader is disabled' do | ||||||
|  |         before do | ||||||
|  |           stub_feature_flags(ci_new_artifact_file_reader: false) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'returns the content at the path' do | ||||||
|  |           is_expected.to be_present | ||||||
|  |           expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       context 'when path does not exist' do |       context 'when path does not exist' do | ||||||
|         let(:path) { 'file/does/not/exist.txt' } |         let(:path) { 'file/does/not/exist.txt' } | ||||||
|         let(:expected_error) do |         let(:expected_error) do | ||||||
|  |  | ||||||
|  | @ -210,6 +210,27 @@ RSpec.describe ObjectStorage do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       describe '#use_open_file' do | ||||||
|  |         context 'when file is stored locally' do | ||||||
|  |           it "returns the file" do | ||||||
|  |             expect { |b| uploader.use_open_file(&b) }.to yield_with_args(an_instance_of(ObjectStorage::Concern::OpenFile)) | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when file is stored remotely' do | ||||||
|  |           let(:store) { described_class::Store::REMOTE } | ||||||
|  | 
 | ||||||
|  |           before do | ||||||
|  |             stub_artifacts_object_storage | ||||||
|  |             stub_request(:get, %r{s3.amazonaws.com/#{uploader.path}}).to_return(status: 200, body: '') | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it "returns the file" do | ||||||
|  |             expect { |b| uploader.use_open_file(&b) }.to yield_with_args(an_instance_of(ObjectStorage::Concern::OpenFile)) | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       describe '#migrate!' do |       describe '#migrate!' do | ||||||
|         subject { uploader.migrate!(new_store) } |         subject { uploader.migrate!(new_store) } | ||||||
| 
 | 
 | ||||||
|  | @ -844,4 +865,19 @@ RSpec.describe ObjectStorage do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe 'OpenFile' do | ||||||
|  |     subject { ObjectStorage::Concern::OpenFile.new(file) } | ||||||
|  | 
 | ||||||
|  |     let(:file) { double(read: true, size: true, path: true) } | ||||||
|  | 
 | ||||||
|  |     it 'delegates read and size methods' do | ||||||
|  |       expect(subject.read).to eq(true) | ||||||
|  |       expect(subject.size).to eq(true) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'does not delegate path method' do | ||||||
|  |       expect { subject.path }.to raise_error(NoMethodError) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue