diff options
81 files changed, 957 insertions, 379 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5d3bd38a003..14d9b7ca9f3 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -ab6438664b9303ea430ff511c5af66ab41cb127a +1418db66513d291e36fb5877b032e109763ec733 diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue new file mode 100644 index 00000000000..28f4a267077 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_app.vue @@ -0,0 +1,29 @@ +<script> +import { mapActions, mapGetters } from 'vuex'; +import BoardContent from '~/boards/components/board_content.vue'; +import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; + +export default { + components: { + BoardContent, + BoardSettingsSidebar, + }, + inject: ['disabled'], + computed: { + ...mapGetters(['isSidebarOpen']), + }, + mounted() { + this.performSearch(); + }, + methods: { + ...mapActions(['performSearch']), + }, +}; +</script> + +<template> + <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }"> + <board-content :disabled="disabled" /> + <board-settings-sidebar /> + </div> +</template> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 66f99b47732..21c1bb23dc6 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -2,12 +2,11 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import PortalVue from 'portal-vue'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { mapActions } from 'vuex'; import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes'; import toggleLabels from 'ee_else_ce/boards/toggle_labels'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; -import BoardContent from '~/boards/components/board_content.vue'; +import BoardApp from '~/boards/components/board_app.vue'; import '~/boards/filters/due_date_filters'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; @@ -41,10 +40,75 @@ const apolloProvider = new VueApollo({ ), }); -let issueBoardsApp; +function mountBoardApp(el) { + const { boardId, groupId, fullPath, rootPath } = el.dataset; + + store.dispatch('setInitialBoardData', { + boardId, + fullBoardId: fullBoardId(boardId), + fullPath, + boardType: el.dataset.parent, + disabled: parseBoolean(el.dataset.disabled) || true, + issuableType: issuableTypes.issue, + boardConfig: { + milestoneId: parseInt(el.dataset.boardMilestoneId, 10), + milestoneTitle: el.dataset.boardMilestoneTitle || '', + iterationId: parseInt(el.dataset.boardIterationId, 10), + iterationTitle: el.dataset.boardIterationTitle || '', + assigneeId: el.dataset.boardAssigneeId, + assigneeUsername: el.dataset.boardAssigneeUsername, + labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [], + labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [], + weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null, + }, + }); + + if (!gon?.features?.issueBoardsFilteredSearch) { + // Warning: FilteredSearchBoards has an implicit dependency on the Vuex state 'boardConfig' + // Improve this situation in the future. + const filterManager = new FilteredSearchBoards({ path: '' }, true, []); + filterManager.setup(); + + eventHub.$on('updateTokens', () => { + filterManager.updateTokens(); + }); + } + + // eslint-disable-next-line no-new + new Vue({ + el, + store, + apolloProvider, + provide: { + disabled: parseBoolean(el.dataset.disabled), + boardId, + groupId: Number(groupId), + rootPath, + currentUserId: gon.current_user_id || null, + canUpdate: parseBoolean(el.dataset.canUpdate), + canAdminList: parseBoolean(el.dataset.canAdminList), + labelsManagePath: el.dataset.labelsManagePath, + labelsFilterBasePath: el.dataset.labelsFilterBasePath, + timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours), + multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable), + epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable), + iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable), + weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable), + boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null, + scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels), + milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable), + assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable), + iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable), + issuableType: issuableTypes.issue, + emailsDisabled: parseBoolean(el.dataset.emailsDisabled), + }, + render: (createComponent) => createComponent(BoardApp), + }); +} export default () => { - const $boardApp = document.getElementById('board-app'); + const $boardApp = document.getElementById('js-issuable-board-app'); + // check for browser back and trigger a hard reload to circumvent browser caching. window.addEventListener('pageshow', (event) => { const isNavTypeBackForward = @@ -55,106 +119,11 @@ export default () => { } }); - if (issueBoardsApp) { - issueBoardsApp.$destroy(true); - } - if (gon?.features?.issueBoardsFilteredSearch) { initBoardsFilteredSearch(apolloProvider); } - // eslint-disable-next-line @gitlab/no-runtime-template-compiler - issueBoardsApp = new Vue({ - el: $boardApp, - components: { - BoardContent, - BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'), - }, - provide: { - boardId: $boardApp.dataset.boardId, - groupId: Number($boardApp.dataset.groupId), - rootPath: $boardApp.dataset.rootPath, - currentUserId: gon.current_user_id || null, - canUpdate: parseBoolean($boardApp.dataset.canUpdate), - canAdminList: parseBoolean($boardApp.dataset.canAdminList), - labelsManagePath: $boardApp.dataset.labelsManagePath, - labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath, - timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours), - multipleAssigneesFeatureAvailable: parseBoolean( - $boardApp.dataset.multipleAssigneesFeatureAvailable, - ), - epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable), - iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable), - weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable), - boardWeight: $boardApp.dataset.boardWeight - ? parseInt($boardApp.dataset.boardWeight, 10) - : null, - scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels), - milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable), - assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable), - iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable), - issuableType: issuableTypes.issue, - emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled), - }, - store, - apolloProvider, - data() { - return { - loading: 0, - recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, - disabled: parseBoolean($boardApp.dataset.disabled), - parent: $boardApp.dataset.parent, - detailIssueVisible: false, - }; - }, - created() { - this.setInitialBoardData({ - boardId: $boardApp.dataset.boardId, - fullBoardId: fullBoardId($boardApp.dataset.boardId), - fullPath: $boardApp.dataset.fullPath, - boardType: this.parent, - disabled: this.disabled, - issuableType: issuableTypes.issue, - boardConfig: { - milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10), - milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '', - iterationId: parseInt($boardApp.dataset.boardIterationId, 10), - iterationTitle: $boardApp.dataset.boardIterationTitle || '', - assigneeId: $boardApp.dataset.boardAssigneeId, - assigneeUsername: $boardApp.dataset.boardAssigneeUsername, - labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [], - labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [], - weight: $boardApp.dataset.boardWeight - ? parseInt($boardApp.dataset.boardWeight, 10) - : null, - }, - }); - - eventHub.$on('updateTokens', this.updateTokens); - eventHub.$on('toggleDetailIssue', this.toggleDetailIssue); - }, - beforeDestroy() { - eventHub.$off('updateTokens', this.updateTokens); - eventHub.$off('toggleDetailIssue', this.toggleDetailIssue); - }, - mounted() { - if (!gon?.features?.issueBoardsFilteredSearch) { - this.filterManager = new FilteredSearchBoards({ path: '' }, true, []); - this.filterManager.setup(); - } - - this.performSearch(); - }, - methods: { - ...mapActions(['setInitialBoardData', 'performSearch', 'setError']), - updateTokens() { - this.filterManager.updateTokens(); - }, - toggleDetailIssue(hasSidebar) { - this.detailIssueVisible = hasSidebar; - }, - }, - }); + mountBoardApp($boardApp); const createColumnTriggerEl = document.querySelector('.js-create-column-trigger'); if (createColumnTriggerEl) { diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 476cf2e4c73..402205334c8 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -18,7 +18,6 @@ import { } from 'ee_else_ce/boards/constants'; import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; -import eventHub from '~/boards/eventhub'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; @@ -62,12 +61,10 @@ export default { setActiveId({ commit }, { id, sidebarType }) { commit(types.SET_ACTIVE_ID, { id, sidebarType }); - eventHub.$emit('toggleDetailIssue', true); }, unsetActiveId({ dispatch }) { dispatch('setActiveId', { id: inactiveId, sidebarType: '' }); - eventHub.$emit('toggleDetailIssue', false); }, setFilters: ({ commit, state: { issuableType } }, filters) => { diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue index f04caa8498f..004c2e26c4e 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue @@ -1,8 +1,7 @@ <script> import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui'; -import { escape } from 'lodash'; import { mapState, mapActions } from 'vuex'; -import { sprintf, s__, __ } from '~/locale'; +import { s__, __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { DEFAULT_REGION } from '../constants'; @@ -37,6 +36,9 @@ export default { regionHelpText: s__( 'ClusterIntegration|Select the region you want to create the new cluster in. Make sure you have access to this region for your role to be able to authenticate. If no region is selected, we will use %{codeStart}DEFAULT_REGION%{codeEnd}. Learn more about %{linkStart}Regions%{linkEnd}.', ), + accountAndExternalIdsHelpText: s__( + 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{awsLinkStart}Amazon Web Services %{awsLinkEnd} using the above account and external IDs. %{moreInfoStart}More information%{moreInfoEnd}', + ), regionHelpTextDefaultRegion: DEFAULT_REGION, }, data() { @@ -55,39 +57,8 @@ export default { ? __('Authenticating') : s__('ClusterIntegration|Authenticate with AWS'); }, - accountAndExternalIdsHelpText() { - const escapedUrl = escape(this.accountAndExternalIdsHelpPath); - - return sprintf( - s__( - 'ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}', - ), - { - startAwsLink: - '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">', - startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`, - externalLinkIcon: this.externalLinkIcon, - endLink: '</a>', - }, - false, - ); - }, - provisionRoleArnHelpText() { - const escapedUrl = escape(this.createRoleArnHelpPath); - - return sprintf( - s__( - 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}', - ), - { - startAwsLink: - '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">', - startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`, - externalLinkIcon: this.externalLinkIcon, - endLink: '</a>', - }, - false, - ); + awsHelpLink() { + return 'https://console.aws.amazon.com/iam/home?#roles'; }, }, methods: { @@ -141,19 +112,41 @@ export default { </div> </div> <div class="col-12 mb-3 mt-n3"> - <p - class="form-text text-muted" - v-html="accountAndExternalIdsHelpText /* eslint-disable-line vue/no-v-html */" - ></p> + <p class="form-text text-muted"> + <gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText"> + <template #awsLink="{ content }"> + <gl-link :href="awsHelpLink" target="_blank"> + {{ content }} + <gl-icon name="external-link" class="gl-vertical-align-middle" /> + </gl-link> + </template> + <template #moreInfo="{ content }"> + <gl-link :href="accountAndExternalIdsHelpPath" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> </div> </div> <div class="form-group"> <label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label> <gl-form-input id="eks-provision-role-arn" v-model="roleArn" /> - <p - class="form-text text-muted" - v-html="provisionRoleArnHelpText /* eslint-disable-line vue/no-v-html */" - ></p> + <p class="form-text text-muted"> + <gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText"> + <template #awsLink="{ content }"> + <gl-link :href="awsHelpLink" target="_blank"> + {{ content }} + <gl-icon name="external-link" class="gl-vertical-align-middle" /> + </gl-link> + </template> + <template #moreInfo="{ content }"> + <gl-link :href="accountAndExternalIdsHelpPath" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> </div> <gl-form-group :label="$options.i18n.regionInputLabel"> diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index 5f769e8d0b3..5bdc1bd9f90 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -20,6 +20,7 @@ const MILESTONE_DUE = 'milestone_due'; const POPULARITY = 'popularity'; const WEIGHT = 'weight'; const LABEL_PRIORITY = 'label_priority'; +const TITLE = 'title'; export const RELATIVE_POSITION = 'relative_position'; export const LOADING_LIST_ITEMS_LENGTH = 8; export const PAGE_SIZE = 20; @@ -41,6 +42,8 @@ export const sortOrderMap = { relative_position: { order_by: RELATIVE_POSITION, sort: ASC }, weight_desc: { order_by: WEIGHT, sort: DESC }, weight: { order_by: WEIGHT, sort: ASC }, + title: { order_by: TITLE, sort: ASC }, + title_desc: { order_by: TITLE, sort: DESC }, }; export const availableSortOptionsJira = [ @@ -144,6 +147,8 @@ export const POPULARITY_DESC = 'POPULARITY_DESC'; export const PRIORITY_ASC = 'PRIORITY_ASC'; export const PRIORITY_DESC = 'PRIORITY_DESC'; export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC'; +export const TITLE_ASC = 'TITLE_ASC'; +export const TITLE_DESC = 'TITLE_DESC'; export const UPDATED_ASC = 'UPDATED_ASC'; export const UPDATED_DESC = 'UPDATED_DESC'; export const WEIGHT_ASC = 'WEIGHT_ASC'; @@ -161,6 +166,7 @@ const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc'; const POPULARITY_ASC_SORT = 'popularity_asc'; const WEIGHT_DESC_SORT = 'weight_desc'; const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc'; +const TITLE_DESC_SORT = 'title_desc'; export const urlSortParams = { [PRIORITY_ASC]: PRIORITY_ASC_SORT, @@ -181,6 +187,8 @@ export const urlSortParams = { [WEIGHT_ASC]: WEIGHT, [WEIGHT_DESC]: WEIGHT_DESC_SORT, [BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT, + [TITLE_ASC]: TITLE, + [TITLE_DESC]: TITLE_DESC_SORT, }; export const MAX_LIST_SIZE = 10; diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index de988e28db9..f204f0ebfaa 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -1,4 +1,5 @@ <script> +import { GlSafeHtmlDirective } from '@gitlab/ui'; import { glEmojiTag } from '~/emoji'; import { s__ } from '~/locale'; @@ -12,6 +13,9 @@ export default { DetailedMetric, RequestSelector, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, props: { store: { type: Object, @@ -128,6 +132,7 @@ export default { this.currentRequest = newRequestId; }, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> <template> @@ -143,7 +148,7 @@ export default { class="current-host" :class="{ canary: currentRequest.details.host.canary }" > - <span v-html="birdEmoji /* eslint-disable-line vue/no-v-html */"></span> + <span v-safe-html:[$options.safeHtmlConfig]="birdEmoji"></span> {{ currentRequest.details.host.hostname }} </span> </div> diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue index 677efec85dc..a46ac620f48 100644 --- a/app/assets/javascripts/performance_bar/components/request_selector.vue +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -1,5 +1,5 @@ <script> -import { GlPopover } from '@gitlab/ui'; +import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui'; import { glEmojiTag } from '~/emoji'; import { n__ } from '~/locale'; @@ -7,6 +7,9 @@ export default { components: { GlPopover, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, props: { currentRequest: { type: Object, @@ -42,6 +45,7 @@ export default { methods: { glEmojiTag, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> <template> @@ -60,7 +64,7 @@ export default { <span v-if="requestsWithWarnings.length" class="gl-cursor-default"> <span id="performance-bar-request-selector-warning" - v-html="glEmojiTag('warning') /* eslint-disable-line vue/no-v-html */" + v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('warning')" ></span> <gl-popover placement="bottom" diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue index 01d6e68e3b0..3ebd222029b 100644 --- a/app/assets/javascripts/performance_bar/components/request_warning.vue +++ b/app/assets/javascripts/performance_bar/components/request_warning.vue @@ -1,11 +1,14 @@ <script> -import { GlPopover } from '@gitlab/ui'; +import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui'; import { glEmojiTag } from '~/emoji'; export default { components: { GlPopover, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, props: { htmlId: { type: String, @@ -31,14 +34,12 @@ export default { methods: { glEmojiTag, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> <template> <span v-if="hasWarnings" class="gl-cursor-default"> - <span - :id="htmlId" - v-html="glEmojiTag('warning') /* eslint-disable-line vue/no-v-html */" - ></span> + <span :id="htmlId" v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('warning')"></span> <gl-popover placement="bottom" :target="htmlId" :content="warningMessage" /> </span> </template> diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 576cc0996bd..b1c8f6ef22e 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -7,6 +7,7 @@ import { GlFormCheckbox, GlDropdown, GlDropdownItem, + GlSafeHtmlDirective, } from '@gitlab/ui'; import $ from 'jquery'; import Vue from 'vue'; @@ -48,6 +49,7 @@ export default { }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, mixins: [glFeatureFlagsMixin()], props: { @@ -233,6 +235,7 @@ export default { }, }, statusTimeRanges, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> @@ -266,7 +269,7 @@ export default { @click="setEmoji" > <template #button-content> - <span v-html="emojiTag /* eslint-disable-line vue/no-v-html */"></span> + <span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span> <span v-show="noEmoji" class="js-no-emoji-placeholder no-emoji-placeholder position-relative" @@ -288,7 +291,7 @@ export default { class="js-toggle-emoji-menu emoji-menu-toggle-button btn" @click="showEmojiMenu" > - <span v-html="emojiTag /* eslint-disable-line vue/no-v-html */"></span> + <span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span> <span v-show="noEmoji" class="js-no-emoji-placeholder no-emoji-placeholder position-relative" diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 5b316c7a835..82a28d4cb5f 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui'; import { groupBy } from 'lodash'; import EmojiPicker from '~/emoji/components/picker.vue'; import { __, sprintf } from '~/locale'; @@ -17,6 +17,7 @@ export default { }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, mixins: [glFeatureFlagsMixin()], props: { @@ -163,6 +164,7 @@ export default { this.isMenuOpen = menuOpen; }, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> @@ -180,9 +182,9 @@ export default { > <template #emoji> <span + v-safe-html:[$options.safeHtmlConfig]="awardList.html" class="award-emoji-block" data-testid="award-html" - v-html="awardList.html /* eslint-disable-line vue/no-v-html */" ></span> </template> <span class="js-counter">{{ awardList.list.length }}</span> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 02c9cd94ecb..9e334f944a0 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlLink, GlButton, GlTooltip } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlButton, GlTooltip, GlSafeHtmlDirective } from '@gitlab/ui'; import { glEmojiTag } from '../../emoji'; import { __, sprintf } from '../../locale'; import CiIconBadge from './ci_badge_link.vue'; @@ -24,6 +24,7 @@ export default { }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, EMOJI_REF: 'EMOJI_REF', props: { @@ -92,6 +93,7 @@ export default { this.$emit('clickedSidebarButton'); }, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> @@ -129,8 +131,8 @@ export default { <span v-if="statusTooltipHTML" :ref="$options.EMOJI_REF" + v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML" :data-testid="message" - v-html="statusTooltipHTML /* eslint-disable-line vue/no-v-html */" ></span> </template> </section> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 42095c36a13..97ec1140853 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -1,5 +1,5 @@ <script> -import { GlPopover, GlLink, GlSkeletonLoader, GlIcon } from '@gitlab/ui'; +import { GlPopover, GlLink, GlSkeletonLoader, GlIcon, GlSafeHtmlDirective } from '@gitlab/ui'; import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; import { glEmojiTag } from '../../../emoji'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; @@ -17,6 +17,9 @@ export default { UserAvatarImage, UserNameWithStatus, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, props: { target: { type: HTMLElement, @@ -49,6 +52,7 @@ export default { return this.user?.status?.availability || ''; }, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> @@ -94,7 +98,7 @@ export default { <span class="gl-ml-2">{{ user.location }}</span> </div> <div v-if="statusHtml" class="js-user-status gl-mt-3"> - <span v-html="statusHtml /* eslint-disable-line vue/no-v-html */"></span> + <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span> </div> <div v-if="user.bot" class="gl-text-blue-500"> <gl-icon name="question" /> diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 9ad342f3936..a2312484a9b 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -191,7 +191,8 @@ class Projects::PipelinesController < Projects::ApplicationController def config_variables respond_to do |format| format.json do - result = Ci::ListConfigVariablesService.new(@project, current_user).execute(params[:sha]) + project = @project.uses_external_project_ci_config? ? @project.ci_config_external_project : @project + result = Ci::ListConfigVariablesService.new(project, current_user).execute(params[:sha]) result.nil? ? head(:no_content) : render(json: result) end diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index a2390ff01fe..f8825ff6c46 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -10,6 +10,8 @@ module Types value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order.', value: :relative_position_asc value 'SEVERITY_ASC', 'Severity from less critical to more critical.', value: :severity_asc value 'SEVERITY_DESC', 'Severity from more critical to less critical.', value: :severity_desc + value 'TITLE_ASC', 'Title by ascending order.', value: :title_asc + value 'TITLE_DESC', 'Title by descending order.', value: :title_desc value 'POPULARITY_ASC', 'Number of upvotes (awarded "thumbs up" emoji) by ascending order.', value: :popularity_asc value 'POPULARITY_DESC', 'Number of upvotes (awarded "thumbs up" emoji) by descending order.', value: :popularity_desc end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 7fa85d143f7..b28e5ff39b2 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -37,7 +37,8 @@ module SortingHelper sort_value_contacted_date => sort_title_contacted_date, sort_value_relative_position => sort_title_relative_position, sort_value_size => sort_title_size, - sort_value_expire_date => sort_title_expire_date + sort_value_expire_date => sort_title_expire_date, + sort_value_title => sort_title_title } end # rubocop: enable Metrics/AbcSize @@ -188,7 +189,8 @@ module SortingHelper sort_value_due_date_later => sort_value_due_date, sort_value_merged_recently => sort_value_merged_date, sort_value_closed_recently => sort_value_closed_date, - sort_value_least_popular => sort_value_popularity + sort_value_least_popular => sort_value_popularity, + sort_value_title_desc => sort_value_title } end @@ -205,7 +207,8 @@ module SortingHelper sort_value_closed_date => sort_value_closed_recently, sort_value_closed_earlier => sort_value_closed_recently, sort_value_popularity => sort_value_least_popular, - sort_value_most_popular => sort_value_least_popular + sort_value_most_popular => sort_value_least_popular, + sort_value_title => sort_value_title_desc }.merge(issuable_sort_option_overrides) end diff --git a/app/helpers/sorting_titles_values_helper.rb b/app/helpers/sorting_titles_values_helper.rb index f4117d690f3..75ba6e8a153 100644 --- a/app/helpers/sorting_titles_values_helper.rb +++ b/app/helpers/sorting_titles_values_helper.rb @@ -138,6 +138,10 @@ module SortingTitlesValuesHelper s_('SortOptions|Start soon') end + def sort_title_title + s_('SortOptions|Title') + end + def sort_title_upvotes s_('SortOptions|Most popular') end @@ -307,6 +311,14 @@ module SortingTitlesValuesHelper 'start_date_asc' end + def sort_value_title + 'title_asc' + end + + def sort_value_title_desc + 'title_desc' + end + def sort_value_upvotes 'upvotes_desc' end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8d0f8b01d64..48acec58dc3 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -26,6 +26,7 @@ module Issuable include UpdatedAtFilterable include ClosedAtFilterable include VersionedDescription + include SortableTitle TITLE_LENGTH_MAX = 255 TITLE_HTML_LENGTH_MAX = 800 @@ -293,6 +294,8 @@ module Issuable when 'popularity', 'popularity_desc', 'upvotes_desc' then order_upvotes_desc when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels) + when 'title_asc' then order_title_asc.with_order_id_desc + when 'title_desc' then order_title_desc.with_order_id_desc else order_by(method) end diff --git a/app/models/concerns/sortable_title.rb b/app/models/concerns/sortable_title.rb new file mode 100644 index 00000000000..7c5cad17f4c --- /dev/null +++ b/app/models/concerns/sortable_title.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module SortableTitle + extend ActiveSupport::Concern + + included do + scope :order_title_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } + scope :order_title_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:title].lower)) } + end + + class_methods do + def simple_sorts + super.merge( + { + 'title_asc' => -> { order_title_asc }, + 'title_desc' => -> { order_title_desc } + } + ) + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index a1cb88d2a67..1e83f271052 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -656,6 +656,10 @@ class Group < Namespace members.owners.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) end + def membership_locked? + false # to support project and group calling this as 'source' + end + def supports_events? false end diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb index ff1f806df45..72e3c4a8cbc 100644 --- a/app/models/integrations/slack_slash_commands.rb +++ b/app/models/integrations/slack_slash_commands.rb @@ -9,7 +9,7 @@ module Integrations end def description - "Perform common operations in Slack" + "Perform common operations in Slack." end def self.to_param diff --git a/app/models/project.rb b/app/models/project.rb index dd3f167b053..c594adaae45 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1677,6 +1677,10 @@ class Project < ApplicationRecord end end + def membership_locked? + false + end + def bots users.project_bot end @@ -2545,6 +2549,10 @@ class Project < ApplicationRecord ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] end + def uses_external_project_ci_config? + !!(ci_config_path =~ %r{@.+/.+}) + end + def limited_protected_branches(limit) protected_branches.limit(limit) end @@ -2653,6 +2661,10 @@ class Project < ApplicationRecord repository.gitlab_ci_yml_for(sha, ci_config_path_or_default) end + def ci_config_external_project + Project.find_by_full_path(ci_config_path.split('@', 2).last) + end + def enabled_group_deploy_keys return GroupDeployKey.none unless group diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index 3e809b11024..0cc62e661a3 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -4,9 +4,12 @@ module Members class CreateService < Members::BaseService BlankInvitesError = Class.new(StandardError) TooManyInvitesError = Class.new(StandardError) + MembershipLockedError = Class.new(StandardError) DEFAULT_INVITE_LIMIT = 100 + attr_reader :membership_locked + def initialize(*args) super @@ -17,18 +20,22 @@ module Members def execute validate_invite_source! - validate_invites! + validate_invitable! add_members enqueue_onboarding_progress_action result - rescue BlankInvitesError, TooManyInvitesError => e + rescue BlankInvitesError, TooManyInvitesError, MembershipLockedError => e error(e.message) end + def single_member + members.last + end + private - attr_reader :source, :errors, :invites, :member_created_namespace_id + attr_reader :source, :errors, :invites, :member_created_namespace_id, :members def invites_from_params params[:user_ids] @@ -38,7 +45,7 @@ module Members raise ArgumentError, s_('AddMember|No invite source provided.') unless invite_source.present? end - def validate_invites! + def validate_invitable! raise BlankInvitesError, blank_invites_message if invites.blank? return unless user_limit && invites.size > user_limit @@ -52,7 +59,7 @@ module Members end def add_members - members = source.add_users( + @members = source.add_users( invites, params[:access_level], expires_at: params[:expires_at], diff --git a/app/services/members/invite_service.rb b/app/services/members/invite_service.rb index 6298943977b..257a986b8dd 100644 --- a/app/services/members/invite_service.rb +++ b/app/services/members/invite_service.rb @@ -18,7 +18,7 @@ module Members params[:email] end - def validate_invites! + def validate_invitable! super # we need the below due to add_users hitting Members::CreatorService.parse_users_list and ignoring invalid emails diff --git a/app/services/users/reject_service.rb b/app/services/users/reject_service.rb index 833c30d9427..459dd81b74d 100644 --- a/app/services/users/reject_service.rb +++ b/app/services/users/reject_service.rb @@ -7,8 +7,8 @@ module Users end def execute(user) - return error(_('You are not allowed to reject a user')) unless allowed? - return error(_('This user does not have a pending request')) unless user.blocked_pending_approval? + return error(_('You are not allowed to reject a user'), :forbidden) unless allowed? + return error(_('User does not have a pending request'), :conflict) unless user.blocked_pending_approval? user.delete_async(deleted_by: current_user, params: { hard_delete: true }) @@ -18,7 +18,7 @@ module Users log_event(user) - success + success(message: 'Success', http_status: :ok) end private diff --git a/app/views/groups/settings/_membership.html.haml b/app/views/groups/settings/_membership.html.haml new file mode 100644 index 00000000000..b05a294e864 --- /dev/null +++ b/app/views/groups/settings/_membership.html.haml @@ -0,0 +1,6 @@ +%h5= _('Membership') + +.form-group + = render 'shared/allow_request_access', form: f + += render_if_exists 'groups/member_lock_setting', f: f, group: @group diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 683e70248b6..8f428909e60 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -4,9 +4,6 @@ %fieldset %h5= _('Permissions') - .form-group - = render 'shared/allow_request_access', form: f - - if @group.root? .form-group.gl-mb-3 = f.gitlab_ui_checkbox_component :prevent_sharing_groups_outside_hierarchy, @@ -43,5 +40,5 @@ = render_if_exists 'groups/settings/prevent_forking', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f, group: @group = render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group - = render_if_exists 'groups/member_lock_setting', f: f, group: @group + = render 'groups/settings/membership', f: f, group: @group = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' } diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 9702f9b08f2..fee0ca15808 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -4,22 +4,21 @@ .info-well .well-segment %p - = s_("SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack.") + = s_("SlackService|Perform common operations in this project by entering slash commands in Slack.") = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do - = _("View documentation") + = _("Learn more.") = sprite_icon('external-link') %p.inline - = s_("SlackService|See list of available commands in Slack after setting up this service, by entering") - %kbd.inline /<command> help + = s_("SlackService|After setup, get a list of available Slack slash commands by entering") + %kbd.inline /<command> help - if integration.project_level? - %p= _("To set up this service:") + %p= _("To set up this integration:") %ul.list-unstyled.indent-list %li - 1. - = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do - Add a slash command - = sprite_icon('external-link') - in your Slack team with these options: + - slash_command_link_url = 'https://my.slack.com/services/new/slash-commands' + - slash_command_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: slash_command_link_url } + - slash_command_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: sprite_icon('external-link') } + = html_escape(s_('SlackService|1. %{slash_command_link_start}Add a slash command%{slash_command_link_end} in your Slack team using this information:')) % { slash_command_link_start: slash_command_link_start, slash_command_link_end: slash_command_link_end } %hr @@ -89,6 +88,6 @@ %ul.list-unstyled.indent-list %li - = html_escape(s_("SlackService|2. Paste the %{strong_open}Token%{strong_close} into the field below")) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } + = html_escape(s_("SlackService|2. Paste the token from Slack in the %{strong_open}Token%{strong_close} field below.")) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } %li - = html_escape(s_("SlackService|3. Select the %{strong_open}Active%{strong_close} checkbox, press %{strong_open}Save changes%{strong_close} and start using GitLab inside Slack!")) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } + = html_escape(s_("SlackService|3. Select the %{strong_open}Active%{strong_close} checkbox, select %{strong_open}Save changes%{strong_close}, and start using slash commands in Slack!")) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 524f6dc820e..98752345074 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -17,6 +17,5 @@ - add_page_specific_style 'page_bundles/boards' = render 'shared/issuable/search_bar', type: :boards, board: board -#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } - %board-content{ ":disabled" => "disabled" } - %board-settings-sidebar + +#js-issuable-board-app{ data: board_data } diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml index f5bf010e4db..5742f22ce05 100644 --- a/app/views/shared/issuable/_sort_dropdown.html.haml +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -21,5 +21,6 @@ = sortable_item(sort_title_merged_date, page_filter_path(sort: sort_value_merged_date), sort_title) if viewing_merge_requests = sortable_item(sort_title_closed_date, page_filter_path(sort: sort_value_closed_date), sort_title) if viewing_merge_requests = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues + = sortable_item(sort_title_title, page_filter_path(sort: sort_value_title), sort_title) if viewing_issues = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title) = issuable_sort_direction_button(sort_value) diff --git a/db/migrate/20210531053916_rename_instance_statistics_measurements.rb b/db/migrate/20210531053916_rename_instance_statistics_measurements.rb index 9fd459b1275..733ca296952 100644 --- a/db/migrate/20210531053916_rename_instance_statistics_measurements.rb +++ b/db/migrate/20210531053916_rename_instance_statistics_measurements.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -class RenameInstanceStatisticsMeasurements < ActiveRecord::Migration[6.0] - include Gitlab::Database::MigrationHelpers +class RenameInstanceStatisticsMeasurements < Gitlab::Database::Migration[1.0] + enable_lock_retries! def up rename_table_safely(:analytics_instance_statistics_measurements, :analytics_usage_trends_measurements) diff --git a/db/migrate/20210621043337_rename_services_to_integrations.rb b/db/migrate/20210621043337_rename_services_to_integrations.rb index 17f4b6a2d4d..845c3c01a2a 100644 --- a/db/migrate/20210621043337_rename_services_to_integrations.rb +++ b/db/migrate/20210621043337_rename_services_to_integrations.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -class RenameServicesToIntegrations < ActiveRecord::Migration[6.1] - include Gitlab::Database::MigrationHelpers +class RenameServicesToIntegrations < Gitlab::Database::Migration[1.0] include Gitlab::Database::SchemaHelpers + enable_lock_retries! + # Function and trigger names match those migrated in: # - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49916 # - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51852 diff --git a/db/migrate/20210902171406_add_latest_column_into_the_security_scans_table.rb b/db/migrate/20210902171406_add_latest_column_into_the_security_scans_table.rb index 90b00fb899a..3c022cbaf5e 100644 --- a/db/migrate/20210902171406_add_latest_column_into_the_security_scans_table.rb +++ b/db/migrate/20210902171406_add_latest_column_into_the_security_scans_table.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true class AddLatestColumnIntoTheSecurityScansTable < Gitlab::Database::Migration[1.0] + enable_lock_retries! + def up - with_lock_retries do - add_column :security_scans, :latest, :boolean, default: true, null: false - end + add_column :security_scans, :latest, :boolean, default: true, null: false end def down - with_lock_retries do - remove_column :security_scans, :latest - end + remove_column :security_scans, :latest end end diff --git a/doc/api/branches.md b/doc/api/branches.md index 15d1b6c2a18..7b9354f3264 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -47,7 +47,7 @@ Example response: "developers_can_push": false, "developers_can_merge": false, "can_push": true, - "web_url": "http://gitlab.example.com/my-group/my-project/-/tree/master", + "web_url": "https://gitlab.example.com/my-group/my-project/-/tree/master", "commit": { "author_email": "john@example.com", "author_name": "John Smith", @@ -103,7 +103,7 @@ Example response: "developers_can_push": false, "developers_can_merge": false, "can_push": true, - "web_url": "http://gitlab.example.com/my-group/my-project/-/tree/master", + "web_url": "https://gitlab.example.com/my-group/my-project/-/tree/master", "commit": { "author_email": "john@example.com", "author_name": "John Smith", @@ -180,7 +180,7 @@ Example response: "developers_can_push": false, "developers_can_merge": false, "can_push": true, - "web_url": "http://gitlab.example.com/my-group/my-project/-/tree/newbranch" + "web_url": "https://gitlab.example.com/my-group/my-project/-/tree/newbranch" } ``` diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index b98373b5a58..5aca0667f31 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -13,7 +13,7 @@ As of GitLab 12.8, GET requests do not require authentication. All other broadca - Guests result in `401 Unauthorized`. - Regular users result in `403 Forbidden`. -## Get all broadcast messages +## Get all broadcast messages **(FREE)** List all broadcast messages. @@ -46,7 +46,7 @@ Example response: ] ``` -## Get a specific broadcast message +## Get a specific broadcast message **(FREE)** Get a specific broadcast message. diff --git a/doc/api/bulk_imports.md b/doc/api/bulk_imports.md index 2325f25e789..2b71c83b224 100644 --- a/doc/api/bulk_imports.md +++ b/doc/api/bulk_imports.md @@ -4,7 +4,7 @@ group: Import 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/#assignments --- -# GitLab Migrations (Bulk Imports) API +# GitLab Migrations (Bulk Imports) API **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64335) in GitLab 14.1. diff --git a/doc/api/commits.md b/doc/api/commits.md index 35ff0367fec..e91da23596f 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -381,7 +381,7 @@ Parameters: | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) | | `sha` | string | yes | Commit SHA to revert | | `branch` | string | yes | Target branch name | -| `dry_run` | boolean | no | Does not commit any changes. Default is false. [Introduced in GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/231032) | +| `dry_run` | boolean | no | Does not commit any changes. Default is false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/231032) in GitLab 13.3 | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "branch=master" \ diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md index e326a8f17ae..2885cc7d803 100644 --- a/doc/api/container_registry.md +++ b/doc/api/container_registry.md @@ -4,7 +4,7 @@ group: Package 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/#assignments --- -# Container Registry API +# Container Registry API **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/55978) in GitLab 11.8. > - The use of `CI_JOB_TOKEN` scoped to the current project was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750) in GitLab 13.12. diff --git a/doc/api/custom_attributes.md b/doc/api/custom_attributes.md index 9908c58de35..94f924c051d 100644 --- a/doc/api/custom_attributes.md +++ b/doc/api/custom_attributes.md @@ -4,7 +4,7 @@ group: Integrations 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/#assignments --- -# Custom Attributes API +# Custom Attributes API **(FREE SELF)** Every API call to custom attributes must be authenticated as administrator. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b17b8459108..161c1d3cd92 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15515,6 +15515,8 @@ Values for sorting issues. | <a id="issuesortseverity_desc"></a>`SEVERITY_DESC` | Severity from more critical to less critical. | | <a id="issuesortsla_due_at_asc"></a>`SLA_DUE_AT_ASC` | Issues with earliest SLA due time shown first. | | <a id="issuesortsla_due_at_desc"></a>`SLA_DUE_AT_DESC` | Issues with latest SLA due time shown first. | +| <a id="issuesorttitle_asc"></a>`TITLE_ASC` | Title by ascending order. | +| <a id="issuesorttitle_desc"></a>`TITLE_DESC` | Title by descending order. | | <a id="issuesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. | | <a id="issuesortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. | | <a id="issuesortweight_asc"></a>`WEIGHT_ASC` | Weight by ascending order. | diff --git a/doc/api/issues.md b/doc/api/issues.md index e4f32d0fcee..d8e9469b59f 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -73,7 +73,7 @@ GET /issues?state=opened | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ | | `non_archived` | boolean | no | Return issues only from non-archived projects. If `false`, the response returns issues from both archived and non-archived projects. Default is `true`. _(Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/197170))_ | | `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `assignee_id`, `assignee_username`, `author_id`, `author_username`, `iids`, `iteration_id`, `iteration_title`, `labels`, `milestone`, `milestone_id` and `weight`. | -| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` | +| `order_by` | string | no | Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, `updated_at`, or `weight` fields. Default is `created_at`. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5. [Changed to snake_case](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18935) in GitLab 11.0)_ | | `search` | string | no | Search issues against their `title` and `description` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | diff --git a/doc/api/users.md b/doc/api/users.md index 222594897b6..dfd2f6cc87d 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1598,6 +1598,45 @@ Example Responses: { "message": "The user you are trying to approve is not pending approval" } ``` +## Reject user + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339925) in GitLab 14.3. + +Rejects specified user that is [pending approval](../user/admin_area/moderate_users.md#users-pending-approval). Available only for administrators. + +```plaintext +POST /users/:id/reject +``` + +Parameters: + +- `id` (required) - ID of specified user + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/42/reject" +``` + +Returns: + +- `200 OK` on success. +- `403 Forbidden` if not authenticated as an administrator. +- `404 User Not Found` if user cannot be found. +- `409 Conflict` if user is not pending approval. + +Example Responses: + +```json +{ "message": "Success" } +``` + +```json +{ "message": "404 User Not Found" } +``` + +```json +{ "message": "User does not have a pending request" } +``` + ## Get an impersonation token of a user > Requires admin permissions. diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index 631b39de0db..4b97c3d761d 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -171,9 +171,6 @@ variables: You cannot set job-level variables to be pre-filled when you run a pipeline manually. -Pre-filled variables do not show up when the CI/CD configuration is [external to the project](settings.md#specify-a-custom-cicd-configuration-file). -See the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336184) for more details. - ### Run a pipeline by using a URL query string > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24146) in GitLab 12.5. diff --git a/doc/development/database/rename_database_tables.md b/doc/development/database/rename_database_tables.md index 29c17e1a235..881adf00ad0 100644 --- a/doc/development/database/rename_database_tables.md +++ b/doc/development/database/rename_database_tables.md @@ -60,6 +60,8 @@ Consider the next release as "Release N.M". Execute a standard migration (not a post-migration): ```ruby + enable_lock_retries! + def up rename_table_safely(:issues, :tickets) end diff --git a/doc/install/aws/gitlab_hybrid_on_aws.md b/doc/install/aws/gitlab_hybrid_on_aws.md index 81d021f19d3..9f53f333463 100644 --- a/doc/install/aws/gitlab_hybrid_on_aws.md +++ b/doc/install/aws/gitlab_hybrid_on_aws.md @@ -21,7 +21,7 @@ Amazon provides a managed Kubernetes service offering known as [Amazon Elastic K | [3K](../../administration/reference_architectures/3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [3k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/3k) | [3K Cloud Native Hybrid on EKS](#3k-cloud-native-hybrid-on-eks) | [3K Full Scale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216_results.txt)<br /><br />[3K AutoScale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-AutoScale-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_194200/3k-QuickStart-AutoScale-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_194200_results.txt) | 1 YR Ec2 Compute Savings + 1 YR RDS & Elasticache RIs<br />(2 AZ Cost Estimate is in BOM Below) | | [5K](../../administration/reference_architectures/5k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [5k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/5k) | [5K Cloud Native Hybrid on EKS](#5k-cloud-native-hybrid-on-eks) | [5K Full Scale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128_results.txt)<br /><br />[5K AutoScale from 25% GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-AutoScale-From-25Percent-ARM-RDS-Redis_v13-12-3-ee_2021-07-24_102717/5k-QuickStart-AutoScale-From-25Percent-ARM-RDS-Redis_v13-12-3-ee_2021-07-24_102717_results.txt) | 1 YR Ec2 Compute Savings + 1 YR RDS & Elasticache RIs | | [10K](../../administration/reference_architectures/10k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [10k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/10k) | [10K Cloud Native Hybrid on EKS](#10k-cloud-native-hybrid-on-eks) | [10K Full Scale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647_results.txt)<br /><br />[10K AutoScale GPT Test Results](hhttps://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139_results.txt) | [10K 1 YR Ec2 Compute Savings + 1 YR RDS & Elasticache RIs](https://calculator.aws/#/estimate?id=5ac2e07a22e01c36ee76b5477c5a046cd1bea792) | -| [50K](../../administration/reference_architectures/50k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [50k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/50k) | [50K Cloud Native Hybrid on EKS](#50k-cloud-native-hybrid-on-eks) | [50K Full Scale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819_results.txt)<br /><br />[10K AutoScale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633.txt) | [10K 1 YR Ec2 Compute Savings + 1 YR RDS & Elasticache RIs] | +| [50K](../../administration/reference_architectures/50k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [50k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/50k) | [50K Cloud Native Hybrid on EKS](#50k-cloud-native-hybrid-on-eks) | [50K Full Scale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819_results.txt)<br /><br />[10K AutoScale GPT Test Results](https://gitlab.com/gitlab-com/alliances/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633.txt) | 10K 1 YR Ec2 Compute Savings + 1 YR RDS & Elasticache RIs | ## Available Infrastructure as Code for GitLab Cloud Native Hybrid diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index a2f8cbc7b02..40f07902026 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -347,18 +347,21 @@ sudo -u git -H GITLAB_ASSUME_YES=1 bundle exec rake gitlab:backup:restore RAILS_ #### Back up Git repositories concurrently -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37158) in GitLab 13.3. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37158) in GitLab 13.3. +> - [Concurrent restore introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69330) in GitLab 14.3 When using [multiple repository storages](../administration/repository_storage_paths.md), -repositories can be backed up concurrently to help fully use CPU time. The +repositories can be backed up or restored concurrently to help fully use CPU time. The following variables are available to modify the default behavior of the Rake task: - `GITLAB_BACKUP_MAX_CONCURRENCY`: The maximum number of projects to back up at - the same time. Defaults to `1`. + the same time. Defaults to the number of logical CPUs (in GitLab 14.1 and + earlier, defaults to `1`). - `GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY`: The maximum number of projects to back up at the same time on each storage. This allows the repository backups - to be spread across storages. Defaults to `1`. + to be spread across storages. Defaults to `2` (in GitLab 14.1 and earlier, + defaults to `1`). For example, for Omnibus GitLab installations with 4 repository storages: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 597170540ab..9df6da9d6ef 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -377,7 +377,7 @@ have lost your code generation device) you can: - [Use a saved recovery code](#use-a-saved-recovery-code). - [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-ssh). - [Regenerate 2FA recovery codes](#regenerate-2fa-recovery-codes). -- [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account). +- [Have 2FA disabled on your account](#have-2fa-disabled-on-your-account). ### Use a saved recovery code @@ -454,12 +454,9 @@ To regenerate 2FA recovery codes, you need access to a desktop browser: NOTE: If you regenerate 2FA recovery codes, save them. You can't use any previously created 2FA codes. -### Ask a GitLab administrator to disable two-factor authentication on your account +### Have 2FA disabled on your account -If you cannot use a saved recovery code or generate new recovery codes, ask a -GitLab global administrator to disable two-factor authentication for your -account. This temporarily leaves your account in a less secure state. -Sign in and re-enable two-factor authentication as soon as possible. +If you cannot use a saved recovery code or generate new recovery codes then please submit a [support ticket](https://support.gitlab.com/hc/en-us/requests/new) requesting that a GitLab global administrator disables two-factor authentication for your account. Please note that only the actual owner of the account can make this request and that disabling this setting will temporarily leave your account in a less secure state. You should therefore sign in and re-enable two-factor authentication as soon as possible. ## Note to GitLab administrators diff --git a/doc/user/project/issues/sorting_issue_lists.md b/doc/user/project/issues/sorting_issue_lists.md index 2681a39aeb6..aed346fb504 100644 --- a/doc/user/project/issues/sorting_issue_lists.md +++ b/doc/user/project/issues/sorting_issue_lists.md @@ -16,6 +16,7 @@ You can sort a list of issues several ways, including by: - Milestone due date - Popularity - Priority +- Title ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67234) in GitLab 14.3) - Weight The available sorting options can change based on the context of the list. diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb index 37ca55120d3..185a10a250c 100644 --- a/lib/api/helpers/issues_helpers.rb +++ b/lib/api/helpers/issues_helpers.rb @@ -34,7 +34,17 @@ module API end def self.sort_options - %w[created_at updated_at priority due_date relative_position label_priority milestone_due popularity] + %w[ + created_at + due_date + label_priority + milestone_due + popularity + priority + relative_position + title + updated_at + ] end def issue_finder(args = {}) diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index e72bbb931f0..1e89f9f97a2 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -50,24 +50,48 @@ module API GroupMembersFinder.new(group).execute end - def create_member(current_user, user, source, params) - source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at]) + def present_members(members) + present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info] end - def track_areas_of_focus(member, areas_of_focus) - return unless areas_of_focus + def present_member_invitations(invitations) + present invitations, with: Entities::Invitation, current_user: current_user + end + + def add_single_member_by_user_id(create_service_params) + source = create_service_params[:source] + user_id = create_service_params[:user_ids] + user = User.find_by(id: user_id) # rubocop: disable CodeReuse/ActiveRecord + + if user + conflict!('Member already exists') if member_already_exists?(source, user_id) + + instance = ::Members::CreateService.new(current_user, create_service_params) + instance.execute + + not_allowed! if instance.membership_locked # This currently can only be reached in EE if group membership is locked - areas_of_focus.each do |area_of_focus| - Gitlab::Tracking.event(::Members::CreateService.name, 'area_of_focus', label: area_of_focus, property: member.id.to_s) + member = instance.single_member + render_validation_error!(member) if member.invalid? + + present_members(member) + else + not_found!('User') end end - def present_members(members) - present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info] + def add_multiple_members?(user_id) + user_id.include?(',') end - def present_member_invitations(invitations) - present invitations, with: Entities::Invitation, current_user: current_user + def add_single_member?(user_id) + user_id.present? + end + + private + + def member_already_exists?(source, user_id) + source.members.exists?(user_id: user_id) # rubocop: disable CodeReuse/ActiveRecord end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 9805d52dc1c..39ce6e0b062 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -78,7 +78,7 @@ module API optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' optional :order_by, type: String, values: Helpers::IssuesHelpers.sort_options, default: 'created_at', - desc: 'Return issues ordered by `created_at` or `updated_at` fields.' + desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return issues sorted in `asc` or `desc` order.' optional :due_date, type: String, values: %w[0 overdue week month next_month_and_previous_two_weeks] << '', diff --git a/lib/api/members.rb b/lib/api/members.rb index 7130635281a..332520ccd26 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -96,42 +96,22 @@ module API optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api' optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon' end - # rubocop: disable CodeReuse/ActiveRecord + post ":id/members" do ::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/333434') source = find_source(source_type, params[:id]) authorize_admin_source!(source_type, source) - if params[:user_id].to_s.include?(',') - create_service_params = params.except(:user_id).merge({ user_ids: params[:user_id], source: source }) + user_id = params[:user_id].to_s + create_service_params = params.except(:user_id).merge({ user_ids: user_id, source: source }) + if add_multiple_members?(user_id) ::Members::CreateService.new(current_user, create_service_params).execute - elsif params[:user_id].present? - member = source.members.find_by(user_id: params[:user_id]) - conflict!('Member already exists') if member - - user = User.find_by_id(params[:user_id]) - not_found!('User') unless user - - member = create_member(current_user, user, source, params) - - if !member - not_allowed! # This currently can only be reached in EE - elsif member.valid? && member.persisted? - present_members(member) - Gitlab::Tracking.event(::Members::CreateService.name, - 'create_member', - label: params[:invite_source], - property: 'existing_user', - user: current_user) - track_areas_of_focus(member, params[:areas_of_focus]) - else - render_validation_error!(member) - end + elsif add_single_member?(user_id) + add_single_member_by_user_id(create_service_params) end end - # rubocop: enable CodeReuse/ActiveRecord desc 'Updates a member of a group or project.' do success Entities::Member diff --git a/lib/api/users.rb b/lib/api/users.rb index 586c9a77b2e..e3271b8b9b2 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -615,6 +615,22 @@ module API end end + desc 'Reject a pending user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + post ':id/reject', feature_category: :authentication_and_authorization do + user = find_user_by_id(params) + + result = ::Users::RejectService.new(current_user).execute(user) + + if result[:success] + present user + else + render_api_error!(result[:message], result[:http_status]) + end + end + # rubocop: enable CodeReuse/ActiveRecord desc 'Deactivate an active user. Available only for admins.' params do diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb index 7c7c07394d1..587640f1a1a 100644 --- a/lib/backup/gitaly_backup.rb +++ b/lib/backup/gitaly_backup.rb @@ -22,8 +22,8 @@ module Backup end args = [] - args += ['-parallel', @parallel.to_s] if type == :create && @parallel - args += ['-parallel-storage', @parallel_storage.to_s] if type == :create && @parallel_storage + args += ['-parallel', @parallel.to_s] if @parallel + args += ['-parallel-storage', @parallel_storage.to_s] if @parallel_storage @stdin, stdout, @thread = Open3.popen2(ENV, bin_path, command, '-path', backup_repos_path, *args) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index a4190514de2..a477f40697c 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -380,6 +380,8 @@ module Gitlab # The timings can be controlled via the +timing_configuration+ parameter. # If the lock was not acquired within the retry period, a last attempt is made without using +lock_timeout+. # + # Note this helper uses subtransactions when run inside an already open transaction. + # # ==== Examples # # Invoking without parameters # with_lock_retries do @@ -411,7 +413,8 @@ module Gitlab raise_on_exhaustion = !!kwargs.delete(:raise_on_exhaustion) merged_args = { klass: self.class, - logger: Gitlab::BackgroundMigration::Logger + logger: Gitlab::BackgroundMigration::Logger, + allow_savepoints: true }.merge(kwargs) Gitlab::Database::WithLockRetries.new(**merged_args) @@ -1376,13 +1379,11 @@ into similar problems in the future (e.g. when new tables are created). # validate - Whether to validate the constraint in this call # def add_check_constraint(table, check, constraint_name, validate: true) - validate_check_constraint_name!(constraint_name) - # Transactions would result in ALTER TABLE locks being held for the # duration of the transaction, defeating the purpose of this method. - if transaction_open? - raise 'add_check_constraint can not be run inside a transaction' - end + validate_not_in_transaction!(:add_check_constraint) + + validate_check_constraint_name!(constraint_name) if check_constraint_exists?(table, constraint_name) warning_message = <<~MESSAGE @@ -1427,6 +1428,10 @@ into similar problems in the future (e.g. when new tables are created). end def remove_check_constraint(table, constraint_name) + # This is technically not necessary, but aligned with add_check_constraint + # and allows us to continue use with_lock_retries here + validate_not_in_transaction!(:remove_check_constraint) + validate_check_constraint_name!(constraint_name) # DROP CONSTRAINT requires an EXCLUSIVE lock diff --git a/lib/gitlab/database/migration_helpers/v2.rb b/lib/gitlab/database/migration_helpers/v2.rb index f20a9b30fa7..79d69676c44 100644 --- a/lib/gitlab/database/migration_helpers/v2.rb +++ b/lib/gitlab/database/migration_helpers/v2.rb @@ -6,6 +6,44 @@ module Gitlab module V2 include Gitlab::Database::MigrationHelpers + # Executes the block with a retry mechanism that alters the +lock_timeout+ and +sleep_time+ between attempts. + # The timings can be controlled via the +timing_configuration+ parameter. + # If the lock was not acquired within the retry period, a last attempt is made without using +lock_timeout+. + # + # In order to retry the block, the method wraps the block into a transaction. + # Note it cannot be used inside an already open transaction and will raise an error in that case. + # + # ==== Examples + # # Invoking without parameters + # with_lock_retries do + # drop_table :my_table + # end + # + # # Invoking with custom +timing_configuration+ + # t = [ + # [1.second, 1.second], + # [2.seconds, 2.seconds] + # ] + # + # with_lock_retries(timing_configuration: t) do + # drop_table :my_table # this will be retried twice + # end + # + # # Disabling the retries using an environment variable + # > export DISABLE_LOCK_RETRIES=true + # + # with_lock_retries do + # drop_table :my_table # one invocation, it will not retry at all + # end + # + # ==== Parameters + # * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION` + # * +logger+ - [Gitlab::JsonLogger] + # * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES` + def with_lock_retries(*args, **kwargs, &block) + super(*args, **kwargs.merge(allow_savepoints: false), &block) + end + # Renames a column without requiring downtime. # # Concurrent renames work by using database triggers to ensure both the diff --git a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb index f1aa7871245..bd8ed677d77 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb @@ -6,6 +6,8 @@ module Gitlab module ForeignKeyHelpers include ::Gitlab::Database::SchemaHelpers + ERROR_SCOPE = 'foreign keys' + # Adds a foreign key with only minimal locking on the tables involved. # # In concept it works similarly to add_concurrent_foreign_key, but we have @@ -32,6 +34,8 @@ module Gitlab # name - The name of the foreign key. # def add_concurrent_partitioned_foreign_key(source, target, column:, on_delete: :cascade, name: nil) + assert_not_in_transaction_block(scope: ERROR_SCOPE) + partition_options = { column: column, on_delete: on_delete, diff --git a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb index c0cc97de276..c9a3b5caf79 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb @@ -7,6 +7,8 @@ module Gitlab include Gitlab::Database::MigrationHelpers include Gitlab::Database::SchemaHelpers + ERROR_SCOPE = 'index' + # Concurrently creates a new index on a partitioned table. In concept this works similarly to # `add_concurrent_index`, and won't block reads or writes on the table while the index is being built. # @@ -21,6 +23,8 @@ module Gitlab # # See Rails' `add_index` for more info on the available arguments. def add_concurrent_partitioned_index(table_name, column_names, options = {}) + assert_not_in_transaction_block(scope: ERROR_SCOPE) + raise ArgumentError, 'A name is required for indexes added to partitioned tables' unless options[:name] partitioned_table = find_partitioned_table(table_name) @@ -57,6 +61,8 @@ module Gitlab # # remove_concurrent_partitioned_index_by_name :users, 'index_name_goes_here' def remove_concurrent_partitioned_index_by_name(table_name, index_name) + assert_not_in_transaction_block(scope: ERROR_SCOPE) + find_partitioned_table(table_name) unless index_name_exists?(table_name, index_name) diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index 9ccbdc9930e..0dc9f92e4c8 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -431,7 +431,7 @@ module Gitlab replace_table = Gitlab::Database::Partitioning::ReplaceTable.new(original_table_name.to_s, replacement_table_name, replaced_table_name, primary_key_name) - with_lock_retries do + transaction do drop_sync_trigger(original_table_name) replace_table.perform do |sql| diff --git a/lib/gitlab/database/rename_table_helpers.rb b/lib/gitlab/database/rename_table_helpers.rb index 7f5af038c6d..e881c0e5455 100644 --- a/lib/gitlab/database/rename_table_helpers.rb +++ b/lib/gitlab/database/rename_table_helpers.rb @@ -4,27 +4,27 @@ module Gitlab module Database module RenameTableHelpers def rename_table_safely(old_table_name, new_table_name) - with_lock_retries do + transaction do rename_table(old_table_name, new_table_name) execute("CREATE VIEW #{old_table_name} AS SELECT * FROM #{new_table_name}") end end def undo_rename_table_safely(old_table_name, new_table_name) - with_lock_retries do + transaction do execute("DROP VIEW IF EXISTS #{old_table_name}") rename_table(new_table_name, old_table_name) end end def finalize_table_rename(old_table_name, new_table_name) - with_lock_retries do + transaction do execute("DROP VIEW IF EXISTS #{old_table_name}") end end def undo_finalize_table_rename(old_table_name, new_table_name) - with_lock_retries do + transaction do execute("CREATE VIEW #{old_table_name} AS SELECT * FROM #{new_table_name}") end end diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb index 70acbeac9e1..e55390e679a 100644 --- a/lib/gitlab/database/with_lock_retries.rb +++ b/lib/gitlab/database/with_lock_retries.rb @@ -61,9 +61,10 @@ module Gitlab [10.seconds, 10.minutes] ].freeze - def initialize(logger: NULL_LOGGER, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV) + def initialize(logger: NULL_LOGGER, allow_savepoints: true, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV) @logger = logger @klass = klass + @allow_savepoints = allow_savepoints @timing_configuration = timing_configuration @env = env @current_iteration = 1 @@ -122,6 +123,8 @@ module Gitlab end def run_block_with_lock_timeout + raise "WithLockRetries should not run inside already open transaction" if ActiveRecord::Base.connection.transaction_open? && @allow_savepoints.blank? + ActiveRecord::Base.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'") diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6befab5ae6e..f0ed6be6962 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7462,9 +7462,6 @@ msgstr "" msgid "ClusterIntegration|Create Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}" -msgstr "" - msgid "ClusterIntegration|Create cluster on" msgstr "" @@ -7897,7 +7894,7 @@ msgstr "" msgid "ClusterIntegration|Subnets" msgstr "" -msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}" +msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{awsLinkStart}Amazon Web Services %{awsLinkEnd} using the above account and external IDs. %{moreInfoStart}More information%{moreInfoEnd}" msgstr "" msgid "ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster." @@ -20873,9 +20870,6 @@ msgstr "" msgid "Medium vulnerabilities present" msgstr "" -msgid "Member lock" -msgstr "" - msgid "Member since" msgstr "" @@ -20912,6 +20906,9 @@ msgstr "" msgid "Members of a group may only view projects they have permission to access" msgstr "" +msgid "Membership" +msgstr "" + msgid "Members|%{time} by %{user}" msgstr "" @@ -31161,19 +31158,22 @@ msgstr "" msgid "SlackIntegration|Sends notifications about project events to Slack channels." msgstr "" -msgid "SlackService|2. Paste the %{strong_open}Token%{strong_close} into the field below" +msgid "SlackService|1. %{slash_command_link_start}Add a slash command%{slash_command_link_end} in your Slack team using this information:" msgstr "" -msgid "SlackService|3. Select the %{strong_open}Active%{strong_close} checkbox, press %{strong_open}Save changes%{strong_close} and start using GitLab inside Slack!" +msgid "SlackService|2. Paste the token from Slack in the %{strong_open}Token%{strong_close} field below." msgstr "" -msgid "SlackService|Fill in the word that works best for your team." +msgid "SlackService|3. Select the %{strong_open}Active%{strong_close} checkbox, select %{strong_open}Save changes%{strong_close}, and start using slash commands in Slack!" msgstr "" -msgid "SlackService|See list of available commands in Slack after setting up this service, by entering" +msgid "SlackService|After setup, get a list of available Slack slash commands by entering" msgstr "" -msgid "SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack." +msgid "SlackService|Fill in the word that works best for your team." +msgstr "" + +msgid "SlackService|Perform common operations in this project by entering slash commands in Slack." msgstr "" msgid "Slice multiplier" @@ -34572,9 +34572,6 @@ msgstr "" msgid "This user cannot be unlocked manually from GitLab" msgstr "" -msgid "This user does not have a pending request" -msgstr "" - msgid "This user has an unconfirmed email address (%{email}). You may force a confirmation." msgstr "" @@ -35146,7 +35143,7 @@ msgstr "" msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:" msgstr "" -msgid "To set up this service:" +msgid "To set up this integration:" msgstr "" msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there." @@ -36507,6 +36504,9 @@ msgstr "" msgid "User and IP Rate Limits" msgstr "" +msgid "User does not have a pending request" +msgstr "" + msgid "User identity was successfully created." msgstr "" diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6e172f53257..2d5125c9d5e 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -165,7 +165,7 @@ RSpec.describe Admin::UsersController do it 'displays the error' do subject - expect(flash[:alert]).to eq('This user does not have a pending request') + expect(flash[:alert]).to eq('User does not have a pending request') end it 'does not email the user' do diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 774971e992d..1354e894872 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -1292,6 +1292,38 @@ RSpec.describe Projects::PipelinesController do end end + context 'when project uses external project ci config' do + let(:other_project) { create(:project) } + let(:sha) { 'master' } + let(:service) { ::Ci::ListConfigVariablesService.new(other_project, user) } + + let(:ci_config) do + { + variables: { + KEY1: { value: 'val 1', description: 'description 1' } + }, + test: { + stage: 'test', + script: 'echo' + } + } + end + + before do + project.update!(ci_config_path: ".gitlab-ci.yml@#{other_project.full_path}") + synchronous_reactive_cache(service) + end + + it 'returns other project config variables' do + expect(::Ci::ListConfigVariablesService).to receive(:new).with(other_project, anything).and_return(service) + + get_config_variables + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['KEY1']).to eq({ 'value' => 'val 1', 'description' => 'description 1' }) + end + end + private def stub_gitlab_ci_yml_for_sha(sha, result) diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb index 4dfd4416eeb..bc84ccaa432 100644 --- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'Slack slash commands', :js do end it 'shows a help message' do - expect(page).to have_content('This service allows users to perform common') + expect(page).to have_content('Perform common operations in this project') end it 'redirects to the integrations page after saving but not activating' do @@ -42,6 +42,6 @@ RSpec.describe 'Slack slash commands', :js do end it 'shows help content' do - expect(page).to have_content('This service allows users to perform common operations on this project by entering slash commands in Slack.') + expect(page).to have_content('Perform common operations in this project by entering slash commands in Slack.') end end diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js new file mode 100644 index 00000000000..dee097bfb08 --- /dev/null +++ b/spec/frontend/boards/components/board_app_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; + +import BoardApp from '~/boards/components/board_app.vue'; + +describe('BoardApp', () => { + let wrapper; + let store; + + Vue.use(Vuex); + + const createStore = ({ mockGetters = {} } = {}) => { + store = new Vuex.Store({ + state: {}, + actions: { + performSearch: jest.fn(), + }, + getters: { + isSidebarOpen: () => true, + ...mockGetters, + }, + }); + }; + + const createComponent = ({ provide = { disabled: true } } = {}) => { + wrapper = shallowMount(BoardApp, { + store, + provide: { + ...provide, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + store = null; + }); + + it("should have 'is-compact' class when sidebar is open", () => { + createStore(); + createComponent(); + + expect(wrapper.classes()).toContain('is-compact'); + }); + + it("should not have 'is-compact' class when sidebar is closed", () => { + createStore({ mockGetters: { isSidebarOpen: () => false } }); + createComponent(); + + expect(wrapper.classes()).not.toContain('is-compact'); + }); +}); diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 719c14ee188..e992b2b04ae 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -414,6 +414,22 @@ RSpec.describe Resolvers::IssuesResolver do end end end + + context 'when sorting by title' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue1) { create(:issue, project: project, title: 'foo') } + let_it_be(:issue2) { create(:issue, project: project, title: 'bar') } + let_it_be(:issue3) { create(:issue, project: project, title: 'baz') } + let_it_be(:issue4) { create(:issue, project: project, title: 'Baz 2') } + + it 'sorts issues ascending' do + expect(resolve_issues(sort: :title_asc).to_a).to eq [issue2, issue3, issue4, issue1] + end + + it 'sorts issues descending' do + expect(resolve_issues(sort: :title_desc).to_a).to eq [issue1, issue4, issue3, issue2] + end + end end it 'returns issues user can see' do diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index a48a1752eff..7797bd12f0e 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -131,8 +131,19 @@ RSpec.describe Backup::GitalyBackup do context 'parallel option set' do let(:parallel) { 3 } - it 'does not pass parallel option through' do - expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything).and_call_original + it 'passes parallel option through' do + expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything, '-parallel', '3').and_call_original + + subject.start(:restore) + subject.wait + end + end + + context 'parallel_storage option set' do + let(:parallel_storage) { 3 } + + it 'passes parallel option through' do + expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything, '-parallel-storage', '3').and_call_original subject.start(:restore) subject.wait diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb index f132ecbf13b..97ef09e320a 100644 --- a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb @@ -11,6 +11,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do before do allow(migration).to receive(:puts) + + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) end shared_examples_for 'Setting up to rename a column' do @@ -218,4 +220,49 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do let(:added_column) { :original } end end + + describe '#with_lock_retries' do + let(:model) do + ActiveRecord::Migration.new.extend(described_class) + end + + let(:buffer) { StringIO.new } + let(:in_memory_logger) { Gitlab::JsonLogger.new(buffer) } + let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } } + + it 'sets the migration class name in the logs' do + model.with_lock_retries(env: env, logger: in_memory_logger) { } + + buffer.rewind + expect(buffer.read).to include("\"class\":\"#{model.class}\"") + end + + where(raise_on_exhaustion: [true, false]) + + with_them do + it 'sets raise_on_exhaustion as requested' do + with_lock_retries = double + expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries) + expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: raise_on_exhaustion) + + model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) { } + end + end + + it 'does not raise on exhaustion by default' do + with_lock_retries = double + expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries) + expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) + + model.with_lock_retries(env: env, logger: in_memory_logger) { } + end + + it 'defaults to disallowing subtransactions' do + with_lock_retries = double + expect(Gitlab::Database::WithLockRetries).to receive(:new).with(hash_including(allow_savepoints: false)).and_return(with_lock_retries) + expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) + + model.with_lock_retries(env: env, logger: in_memory_logger) { } + end + end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index bc417a6b554..92485f0be7c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2310,8 +2310,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(buffer.read).to include("\"class\":\"#{model.class}\"") end - using RSpec::Parameterized::TableSyntax - where(raise_on_exhaustion: [true, false]) with_them do @@ -2331,6 +2329,15 @@ RSpec.describe Gitlab::Database::MigrationHelpers do model.with_lock_retries(env: env, logger: in_memory_logger) { } end + + it 'defaults to allowing subtransactions' do + with_lock_retries = double + + expect(Gitlab::Database::WithLockRetries).to receive(:new).with(hash_including(allow_savepoints: true)).and_return(with_lock_retries) + expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) + + model.with_lock_retries(env: env, logger: in_memory_logger) { } + end end describe '#backfill_iids' do @@ -2683,6 +2690,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end describe '#remove_check_constraint' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + it 'removes the constraint' do drop_sql = /ALTER TABLE test_table\s+DROP CONSTRAINT IF EXISTS check_name/ diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb index a524fe681e9..f0e34476cf2 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers before do allow(migration).to receive(:puts) + allow(migration).to receive(:transaction_open?).and_return(false) connection.execute(<<~SQL) CREATE TABLE #{target_table_name} ( @@ -141,5 +142,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers .with(source_table_name, target_table_name, options) end end + + context 'when run inside a transaction block' do + it 'raises an error' do + expect(migration).to receive(:transaction_open?).and_return(true) + + expect do + migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, column: column_name) + end.to raise_error(/can not be run inside a transaction/) + end + end end end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb index c3edc3a0c87..8ab3816529b 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -20,6 +20,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do before do allow(migration).to receive(:puts) + allow(migration).to receive(:transaction_open?).and_return(false) connection.execute(<<~SQL) CREATE TABLE #{table_name} ( @@ -127,6 +128,16 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do end.to raise_error(ArgumentError, /#{table_name} is not a partitioned table/) end end + + context 'when run inside a transaction block' do + it 'raises an error' do + expect(migration).to receive(:transaction_open?).and_return(true) + + expect do + migration.add_concurrent_partitioned_index(table_name, column_name) + end.to raise_error(/can not be run inside a transaction/) + end + end end describe '#remove_concurrent_partitioned_index_by_name' do @@ -182,5 +193,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do end.to raise_error(ArgumentError, /#{table_name} is not a partitioned table/) end end + + context 'when run inside a transaction block' do + it 'raises an error' do + expect(migration).to receive(:transaction_open?).and_return(true) + + expect do + migration.remove_concurrent_partitioned_index_by_name(table_name, index_name) + end.to raise_error(/can not be run inside a transaction/) + end + end end end diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb index 72074f06210..0d8f42aa7a2 100644 --- a/spec/lib/gitlab/database/with_lock_retries_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe Gitlab::Database::WithLockRetries do let(:env) { {} } let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER } - let(:subject) { described_class.new(env: env, logger: logger, timing_configuration: timing_configuration) } + let(:subject) { described_class.new(env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) } + let(:allow_savepoints) { true } let(:timing_configuration) do [ @@ -256,4 +257,20 @@ RSpec.describe Gitlab::Database::WithLockRetries do subject.run { } end end + + context 'Stop using subtransactions - allow_savepoints: false' do + let(:allow_savepoints) { false } + + it 'prevents running inside already open transaction' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true) + + expect { subject.run { } }.to raise_error(/should not run inside already open transaction/) + end + + it 'does not raise the error if not inside open transaction' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + + expect { subject.run { } }.not_to raise_error + end + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 071e0dcba44..2a3f639a8ac 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -368,6 +368,23 @@ RSpec.describe Issuable do expect(sorted_issue_ids).to eq(sorted_issue_ids.uniq) end end + + context 'by title' do + let!(:issue1) { create(:issue, project: project, title: 'foo') } + let!(:issue2) { create(:issue, project: project, title: 'bar') } + let!(:issue3) { create(:issue, project: project, title: 'baz') } + let!(:issue4) { create(:issue, project: project, title: 'Baz 2') } + + it 'sorts asc' do + issues = project.issues.sort_by_attribute('title_asc') + expect(issues).to eq([issue2, issue3, issue4, issue1]) + end + + it 'sorts desc' do + issues = project.issues.sort_by_attribute('title_desc') + expect(issues).to eq([issue1, issue4, issue3, issue2]) + end + end end describe '#subscribed?' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ff0c9b435a0..a839d400b9d 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -2486,6 +2486,12 @@ RSpec.describe Group do end end + describe '#membership_locked?' do + it 'returns false' do + expect(build(:group)).not_to be_membership_locked + end + end + describe '#default_owner' do let(:group) { build(:group) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 09528b6304e..f1bec85e336 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -165,8 +165,8 @@ RSpec.describe Issue do expect(described_class.simple_sorts.keys).to include( *%w(created_asc created_at_asc created_date created_desc created_at_desc closest_future_date closest_future_date_asc due_date due_date_asc due_date_desc - id_asc id_desc relative_position relative_position_asc - updated_desc updated_asc updated_at_asc updated_at_desc)) + id_asc id_desc relative_position relative_position_asc updated_desc updated_asc + updated_at_asc updated_at_desc title_asc title_desc)) end end @@ -203,6 +203,25 @@ RSpec.describe Issue do end end + describe '.order_title' do + let_it_be(:issue1) { create(:issue, title: 'foo') } + let_it_be(:issue2) { create(:issue, title: 'bar') } + let_it_be(:issue3) { create(:issue, title: 'baz') } + let_it_be(:issue4) { create(:issue, title: 'Baz 2') } + + context 'sorting ascending' do + subject { described_class.order_title_asc } + + it { is_expected.to eq([issue2, issue3, issue4, issue1]) } + end + + context 'sorting descending' do + subject { described_class.order_title_desc } + + it { is_expected.to eq([issue1, issue4, issue3, issue2]) } + end + end + describe '#order_by_position_and_priority' do let(:project) { reusable_project } let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bb5bda2afbf..438d47e56a9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -604,6 +604,12 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#membership_locked?' do + it 'returns false' do + expect(build(:project)).not_to be_membership_locked + end + end + describe '#autoclose_referenced_issues' do context 'when DB entry is nil' do let(:project) { build(:project, autoclose_referenced_issues: nil) } @@ -2542,7 +2548,7 @@ RSpec.describe Project, factory_default: :keep do end describe '#uses_default_ci_config?' do - let(:project) { build(:project)} + let(:project) { build(:project) } it 'has a custom ci config path' do project.ci_config_path = 'something_custom' @@ -2563,6 +2569,44 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#uses_external_project_ci_config?' do + subject(:uses_external_project_ci_config) { project.uses_external_project_ci_config? } + + let(:project) { build(:project) } + + context 'when ci_config_path is configured with external project' do + before do + project.ci_config_path = '.gitlab-ci.yml@hello/world' + end + + it { is_expected.to eq(true) } + end + + context 'when ci_config_path is nil' do + before do + project.ci_config_path = nil + end + + it { is_expected.to eq(false) } + end + + context 'when ci_config_path is configured with a file in the project' do + before do + project.ci_config_path = 'hello/world/gitlab-ci.yml' + end + + it { is_expected.to eq(false) } + end + + context 'when ci_config_path is configured with remote file' do + before do + project.ci_config_path = 'https://example.org/file.yml' + end + + it { is_expected.to eq(false) } + end + end + describe '#latest_successful_build_for_ref' do let_it_be(:project) { create(:project, :repository) } let_it_be(:pipeline) { create_pipeline(project) } @@ -7043,6 +7087,15 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#ci_config_external_project' do + subject(:ci_config_external_project) { project.ci_config_external_project } + + let(:other_project) { create(:project) } + let(:project) { build(:project, ci_config_path: ".gitlab-ci.yml@#{other_project.full_path}") } + + it { is_expected.to eq(other_project) } + end + describe '#enabled_group_deploy_keys' do let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 7245a989a62..8a33e63b80b 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -815,6 +815,18 @@ RSpec.describe API::Issues do expect_paginated_array_response([closed_issue.id, issue.id]) end + it 'sorts by title asc when requested' do + get api('/issues', user), params: { order_by: :title, sort: :asc } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'sorts by title desc when requested' do + get api('/issues', user), params: { order_by: :title, sort: :desc } + + expect_paginated_array_response([closed_issue.id, issue.id]) + end + context 'with issues list sort options' do it 'accepts only predefined order by params' do API::Helpers::IssuesHelpers.sort_options.each do |sort_opt| @@ -824,7 +836,7 @@ RSpec.describe API::Issues do end it 'fails to sort with non predefined options' do - %w(milestone title abracadabra).each do |sort_opt| + %w(milestone abracadabra).each do |sort_opt| get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' } expect(response).to have_gitlab_http_status(:bad_request) end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index caae35dc29d..a1daf86de31 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -311,36 +311,6 @@ RSpec.describe API::Members do expect(json_response['status']).to eq('error') expect(json_response['message']).to eq(error_message) end - - context 'with invite_source considerations', :snowplow do - let(:params) { { user_id: user_ids, access_level: Member::DEVELOPER } } - - it 'tracks the invite source as api' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: params - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'create_member', - label: 'members-api', - property: 'existing_user', - user: maintainer - ) - end - - it 'tracks the invite source from params' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: params.merge(invite_source: '_invite_source_') - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'create_member', - label: '_invite_source_', - property: 'existing_user', - user: maintainer - ) - end - end end end @@ -410,48 +380,28 @@ RSpec.describe API::Members do end context 'with areas_of_focus considerations', :snowplow do - context 'when there is 1 user to add' do - let(:user_id) { stranger.id } + let(:user_id) { stranger.id } - context 'when areas_of_focus is present in params' do - it 'tracks the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end - end - - context 'when areas_of_focus is not present in params' do - it 'does not track the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER } + context 'when areas_of_focus is present in params' do + it 'tracks the areas_of_focus' do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - expect_no_snowplow_event(category: 'Members::CreateService', action: 'area_of_focus') - end + expect_snowplow_event( + category: 'Members::CreateService', + action: 'area_of_focus', + label: 'Other', + property: source.members.last.id.to_s + ) end end - context 'when there are multiple users to add' do - let(:user_id) { [developer.id, stranger.id].join(',') } + context 'when areas_of_focus is not present in params' do + it 'does not track the areas_of_focus' do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: user_id, access_level: Member::DEVELOPER } - context 'when areas_of_focus is present in params' do - it 'tracks the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end + expect_no_snowplow_event(category: 'Members::CreateService', action: 'area_of_focus') end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 7d4f2b69fc7..788febc7f2b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -2778,7 +2778,9 @@ RSpec.describe API::Users do end end - context 'approve pending user' do + context 'approve and reject pending user' do + let(:pending_user) { create(:user, :blocked_pending_approval) } + shared_examples '404' do it 'returns 404' do expect(response).to have_gitlab_http_status(:not_found) @@ -2789,7 +2791,6 @@ RSpec.describe API::Users do describe 'POST /users/:id/approve' do subject(:approve) { post api("/users/#{user_id}/approve", api_user) } - let_it_be(:pending_user) { create(:user, :blocked_pending_approval) } let_it_be(:deactivated_user) { create(:user, :deactivated) } let_it_be(:blocked_user) { create(:user, :blocked) } @@ -2868,6 +2869,96 @@ RSpec.describe API::Users do end end end + + describe 'POST /users/:id/reject', :aggregate_failures do + subject(:reject) { post api("/users/#{user_id}/reject", api_user) } + + shared_examples 'returns 409' do + it 'returns 409' do + reject + + expect(response).to have_gitlab_http_status(:conflict) + expect(json_response['message']).to eq('User does not have a pending request') + end + end + + context 'performed by a non-admin user' do + let(:api_user) { user } + let(:user_id) { pending_user.id } + + it 'returns 403' do + expect { reject }.not_to change { pending_user.reload.state } + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('You are not allowed to reject a user') + end + end + + context 'performed by an admin user' do + let(:api_user) { admin } + + context 'for an pending approval user' do + let(:user_id) { pending_user.id } + + it 'returns 200' do + reject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['message']).to eq('Success') + end + end + + context 'for a deactivated user' do + let(:user_id) { deactivated_user.id } + + it 'does not reject a deactivated user' do + expect { reject }.not_to change { deactivated_user.reload.state } + end + + it_behaves_like 'returns 409' + end + + context 'for an active user' do + let(:user_id) { user.id } + + it 'does not reject an active user' do + expect { reject }.not_to change { user.reload.state } + end + + it_behaves_like 'returns 409' + end + + context 'for a blocked user' do + let(:blocked_user) { create(:user, :blocked) } + let(:user_id) { blocked_user.id } + + it 'does not reject a blocked user' do + expect { reject }.not_to change { blocked_user.reload.state } + end + + it_behaves_like 'returns 409' + end + + context 'for a ldap blocked user' do + let(:user_id) { ldap_blocked_user.id } + + it 'does not reject a ldap blocked user' do + expect { reject }.not_to change { ldap_blocked_user.reload.state } + end + + it_behaves_like 'returns 409' + end + + context 'for a user that does not exist' do + let(:user_id) { non_existing_record_id } + + before do + reject + end + + it_behaves_like '404' + end + end + end end describe 'POST /users/:id/block', :aggregate_failures do diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb index b0094a7c47e..0e34f0e67ba 100644 --- a/spec/services/users/reject_service_spec.rb +++ b/spec/services/users/reject_service_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Users::RejectService do it 'returns error result' do expect(subject[:status]).to eq(:error) expect(subject[:message]) - .to match(/This user does not have a pending request/) + .to match(/User does not have a pending request/) end end end |