summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-09 09:11:16 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-09 09:11:16 +0000
commit54573816ab79fac6716d1a1eae8c0da602765774 (patch)
tree2ebdf1558969cc88eacba7ce5618a299c88dcf3d
parent8602c599660974a47ad705fd5cc3f97cd37b1a0f (diff)
downloadgitlab-ce-54573816ab79fac6716d1a1eae8c0da602765774.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue29
-rw-r--r--app/assets/javascripts/boards/index.js169
-rw-r--r--app/assets/javascripts/boards/stores/actions.js3
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue79
-rw-r--r--app/assets/javascripts/issues_list/constants.js8
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue7
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue8
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue11
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue8
-rw-r--r--app/controllers/projects/pipelines_controller.rb3
-rw-r--r--app/graphql/types/issue_sort_enum.rb2
-rw-r--r--app/helpers/sorting_helper.rb9
-rw-r--r--app/helpers/sorting_titles_values_helper.rb12
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/concerns/sortable_title.rb21
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/integrations/slack_slash_commands.rb2
-rw-r--r--app/models/project.rb12
-rw-r--r--app/services/members/create_service.rb17
-rw-r--r--app/services/members/invite_service.rb2
-rw-r--r--app/services/users/reject_service.rb6
-rw-r--r--app/views/groups/settings/_membership.html.haml6
-rw-r--r--app/views/groups/settings/_permissions.html.haml5
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml23
-rw-r--r--app/views/shared/boards/_show.html.haml5
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml1
-rw-r--r--db/migrate/20210531053916_rename_instance_statistics_measurements.rb4
-rw-r--r--db/migrate/20210621043337_rename_services_to_integrations.rb5
-rw-r--r--db/migrate/20210902171406_add_latest_column_into_the_security_scans_table.rb10
-rw-r--r--doc/api/branches.md6
-rw-r--r--doc/api/broadcast_messages.md4
-rw-r--r--doc/api/bulk_imports.md2
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/container_registry.md2
-rw-r--r--doc/api/custom_attributes.md2
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/issues.md2
-rw-r--r--doc/api/users.md39
-rw-r--r--doc/ci/pipelines/index.md3
-rw-r--r--doc/development/database/rename_database_tables.md2
-rw-r--r--doc/install/aws/gitlab_hybrid_on_aws.md2
-rw-r--r--doc/raketasks/backup_restore.md11
-rw-r--r--doc/user/profile/account/two_factor_authentication.md9
-rw-r--r--doc/user/project/issues/sorting_issue_lists.md1
-rw-r--r--lib/api/helpers/issues_helpers.rb12
-rw-r--r--lib/api/helpers/members_helpers.rb44
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/members.rb32
-rw-r--r--lib/api/users.rb16
-rw-r--r--lib/backup/gitaly_backup.rb4
-rw-r--r--lib/gitlab/database/migration_helpers.rb17
-rw-r--r--lib/gitlab/database/migration_helpers/v2.rb38
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb4
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb6
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb2
-rw-r--r--lib/gitlab/database/rename_table_helpers.rb8
-rw-r--r--lib/gitlab/database/with_lock_retries.rb5
-rw-r--r--locale/gitlab.pot32
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb32
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb4
-rw-r--r--spec/frontend/boards/components/board_app_spec.js54
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb16
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb15
-rw-r--r--spec/lib/gitlab/database/migration_helpers/v2_spec.rb47
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb15
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb11
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb21
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb19
-rw-r--r--spec/models/concerns/issuable_spec.rb17
-rw-r--r--spec/models/group_spec.rb6
-rw-r--r--spec/models/issue_spec.rb23
-rw-r--r--spec/models/project_spec.rb55
-rw-r--r--spec/requests/api/issues/issues_spec.rb14
-rw-r--r--spec/requests/api/members_spec.rb82
-rw-r--r--spec/requests/api/users_spec.rb95
-rw-r--r--spec/services/users/reject_service_spec.rb2
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 /&lt;command&gt; help
+ = s_("SlackService|After setup, get a list of available Slack slash commands by entering")
+ %kbd.inline /&lt;command&gt; 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