summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-18 21:09:37 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-18 21:09:37 +0000
commitcace5e8ff1f766b8098e35adc94abc4402aeb2a9 (patch)
tree96bea3616ee60702be89f4845580f3b3db22f936
parente4220eeccaf1d53444fdd9102a4061336f91784e (diff)
downloadgitlab-ce-cace5e8ff1f766b8098e35adc94abc4402aeb2a9.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/glfm.gitlab-ci.yml12
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js26
-rw-r--r--app/assets/javascripts/groups/components/app.vue8
-rw-r--r--app/assets/javascripts/groups/components/groups.vue20
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue17
-rw-r--r--app/assets/javascripts/groups/constants.js2
-rw-r--r--app/assets/javascripts/ide/index.js4
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js3
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue95
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js20
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/constants.js7
-rw-r--r--app/assets/javascripts/users_select/index.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js2
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_app.vue73
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_mask_item.vue37
-rw-r--r--app/assets/javascripts/webhooks/index.js9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue248
-rw-r--r--app/assets/javascripts/work_items/constants.js1
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql15
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql11
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss3
-rw-r--r--app/controllers/ide_controller.rb3
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/graphql/types/environment_type.rb2
-rw-r--r--app/helpers/hooks_helper.rb7
-rw-r--r--app/helpers/ide_helper.rb32
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/clusters/agents/implicit_authorization.rb2
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/user_preference.rb1
-rw-r--r--app/presenters/ci/build_runner_presenter.rb4
-rw-r--r--app/services/bulk_imports/uploads_export_service.rb5
-rw-r--r--app/services/ci/generate_kubeconfig_service.rb12
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml6
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml30
-rw-r--r--app/views/shared/web_hooks/_form.html.haml2
-rw-r--r--db/migrate/20221006141145_add_targets_to_elastic_reindexing_tasks.rb7
-rw-r--r--db/migrate/20221011210455_add_use_legacy_web_ide_to_user_preferences.rb9
-rw-r--r--db/post_migrate/20221013154159_update_invalid_dormant_user_setting.rb17
-rw-r--r--db/schema_migrations/202210061411451
-rw-r--r--db/schema_migrations/202210112104551
-rw-r--r--db/schema_migrations/202210131541591
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/geo/replication/datatypes.md10
-rw-r--r--doc/administration/operations/rails_console.md18
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md13
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/packages/terraform-modules.md16
-rw-r--r--doc/api/repositories.md141
-rw-r--r--doc/development/gitlab_flavored_markdown/specification_guide/index.md14
-rw-r--r--doc/development/performance.md2
-rw-r--r--doc/user/admin_area/moderate_users.md5
-rw-r--r--doc/user/project/import/manifest.md2
-rw-r--r--doc/user/project/issues/csv_export.md2
-rw-r--r--lib/gitlab/ci/variables/collection.rb17
-rw-r--r--lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb31
-rw-r--r--lib/gitlab/import_export/uploads_manager.rb5
-rw-r--r--locale/gitlab.pot51
-rw-r--r--qa/qa/page/group/show.rb4
-rwxr-xr-xscripts/glfm/verify-all-generated-files-are-up-to-date.rb5
-rw-r--r--scripts/lib/glfm/constants.rb15
-rw-r--r--scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb48
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb3
-rw-r--r--spec/features/admin/admin_settings_spec.rb6
-rw-r--r--spec/finders/merge_requests_finder_spec.rb40
-rw-r--r--spec/frontend/groups/components/app_spec.js36
-rw-r--r--spec/frontend/groups/components/groups_spec.js9
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js6
-rw-r--r--spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js81
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js2
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js97
-rw-r--r--spec/frontend/webhooks/components/form_url_mask_item_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js29
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js247
-rw-r--r--spec/frontend/work_items/mock_data.js63
-rw-r--r--spec/helpers/hooks_helper_spec.rb7
-rw-r--r--spec/helpers/ide_helper_spec.rb130
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb37
-rw-r--r--spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/uploads_manager_spec.rb26
-rw-r--r--spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb40
-rw-r--r--spec/models/application_setting_spec.rb11
-rw-r--r--spec/models/ci/pipeline_spec.rb11
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/clusters/agents/implicit_authorization_spec.rb2
-rw-r--r--spec/models/user_preference_spec.rb7
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb9
-rw-r--r--spec/requests/ide_controller_spec.rb34
-rw-r--r--spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb62
-rw-r--r--spec/services/bulk_imports/uploads_export_service_spec.rb62
-rw-r--r--spec/services/ci/generate_kubeconfig_service_spec.rb6
-rw-r--r--spec/support/cross_database_modification.rb9
-rw-r--r--spec/support/database/prevent_cross_database_modification.rb4
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb13
-rw-r--r--spec/tooling/quality/test_level_spec.rb2
99 files changed, 1999 insertions, 366 deletions
diff --git a/.gitlab/ci/glfm.gitlab-ci.yml b/.gitlab/ci/glfm.gitlab-ci.yml
new file mode 100644
index 00000000000..fcc9a035c1f
--- /dev/null
+++ b/.gitlab/ci/glfm.gitlab-ci.yml
@@ -0,0 +1,12 @@
+glfm-verify:
+ # NOTE: We do not restrict this job to any specific subset of file changes via rules, because
+ # there are potentially many different source files within the codebase which could
+ # change the contents of the generated GLFM files. It is therefore safer to always
+ # run this job to ensure that no changes are missed.
+ extends:
+ - .rspec-ee-base-pg12
+ stage: test
+ needs: ["setup-test-env"]
+ script:
+ - !reference [.base-script, script]
+ - bundle exec scripts/glfm/verify-all-generated-files-are-up-to-date.rb
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index 3849bd0289d..3b737dfff33 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -4,10 +4,14 @@ import { concatPagination } from '@apollo/client/utilities';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
+import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants';
export const temporaryConfig = {
typeDefs,
cacheConfig: {
+ possibleTypes: {
+ LocalWorkItemWidget: ['LocalWorkItemMilestone'],
+ },
typePolicies: {
Project: {
fields: {
@@ -18,6 +22,28 @@ export const temporaryConfig = {
},
WorkItem: {
fields: {
+ mockWidgets: {
+ read(widgets) {
+ return (
+ widgets || [
+ {
+ __typename: 'LocalWorkItemMilestone',
+ type: WIDGET_TYPE_MILESTONE,
+ nodes: [
+ {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Milestone',
+ },
+ ],
+ },
+ ]
+ );
+ },
+ },
widgets: {
merge(existing = [], incoming) {
if (existing.length === 0) {
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index d74cb2d8175..15f5a3518a5 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -51,7 +51,6 @@ export default {
isModalVisible: false,
isLoading: true,
isSearchEmpty: false,
- searchEmptyMessage: '',
targetGroup: null,
targetParentGroup: null,
showEmptyState: false,
@@ -88,10 +87,6 @@ export default {
},
},
created() {
- this.searchEmptyMessage = this.hideProjects
- ? COMMON_STR.GROUP_SEARCH_EMPTY
- : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
-
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
@@ -259,7 +254,7 @@ export default {
const hasGroups = groups && groups.length > 0;
if (this.renderEmptyState) {
- this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups;
+ this.isSearchEmpty = fromSearch && !hasGroups;
} else {
this.isSearchEmpty = !hasGroups;
}
@@ -294,7 +289,6 @@ export default {
v-else
:groups="groups"
:search-empty="isSearchEmpty"
- :search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 3a05c308a2a..43aa0753082 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,11 +1,18 @@
<script>
+import { GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { getParameterByName } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
+ i18n: {
+ emptyStateTitle: __('No results found'),
+ emptyStateDescription: __('Edit your search and try again'),
+ },
components: {
PaginationLinks,
+ GlEmptyState,
},
props: {
groups: {
@@ -20,10 +27,6 @@ export default {
type: Boolean,
required: true,
},
- searchEmptyMessage: {
- type: String,
- required: true,
- },
action: {
type: String,
required: false,
@@ -43,12 +46,11 @@ export default {
<template>
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
- <div
+ <gl-empty-state
v-if="searchEmpty"
- class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5"
- >
- {{ searchEmptyMessage }}
- </div>
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ />
<template v-else>
<group-folder :groups="groups" :action="action" />
<pagination-links
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 15a0c686548..d0c5846ac88 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -2,6 +2,7 @@
import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
import { isString, debounce } from 'lodash';
import { __ } from '~/locale';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import GroupsStore from '../store/groups_store';
import GroupsService from '../service/groups_service';
import {
@@ -61,11 +62,6 @@ export default {
return this.isAscending ? this.sort.asc : this.sort.desc;
},
},
- watch: {
- search: debounce(async function debouncedSearch() {
- this.handleSearchOrSortChange();
- }, 250),
- },
mounted() {
this.search = this.$route.query?.filter || '';
@@ -137,6 +133,14 @@ export default {
this.handleSearchOrSortChange();
},
+ handleSearchInput(value) {
+ this.search = value;
+
+ this.debouncedSearch();
+ },
+ debouncedSearch: debounce(async function debouncedSearch() {
+ this.handleSearchOrSortChange();
+ }, DEBOUNCE_DELAY),
},
i18n: {
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'),
@@ -169,9 +173,10 @@ export default {
<div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2">
<div class="gl-p-2 gl-lg-form-input-md gl-w-full">
<gl-search-box-by-type
- v-model="search"
+ :value="search"
:placeholder="$options.i18n.searchPlaceholder"
data-qa-selector="groups_filter_field"
+ @input="handleSearchInput"
/>
</div>
<div class="gl-p-2 gl-w-full gl-lg-w-auto">
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index 33bfcade336..6fb12cd6270 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -24,8 +24,6 @@ export const COMMON_STR = {
EDIT_BTN_TITLE: s__('GroupsTree|Edit'),
REMOVE_BTN_TITLE: s__('GroupsTree|Delete'),
OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'),
- GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
- GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
};
export const ITEM_TYPE = {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 10e9f6a9488..1a191f6f76f 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -99,7 +99,9 @@ export function startIde(options) {
return;
}
- if (gon.features?.vscodeWebIde) {
+ const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
+
+ if (useNewWebIde) {
initGitlabWebIDE(ideElement);
} else {
resetServiceWorkersPublicPath();
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index a061da38d4f..140f2895a29 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -7,8 +7,7 @@ export const initGitlabWebIDE = async (el) => {
const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
// what: Pull what we need from the element. We will replace it soon.
- const { path_with_namespace: projectPath } = JSON.parse(el.dataset.project);
- const { cspNonce: nonce, branchName: ref } = el.dataset;
+ const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
// what: Clean up the element, but preserve id.
// why: This way we don't inherit any `ide-loading` side-effects. This
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
new file mode 100644
index 00000000000..20ce296bbec
--- /dev/null
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlAvatarLabeled, GlListbox } from '@gitlab/ui';
+import { __ } from '~/locale';
+import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const USERS_PER_PAGE = 20;
+
+export default {
+ components: {
+ GlAvatarLabeled,
+ GlListbox,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ usersQuery: {
+ query: searchUsersQuery,
+ variables() {
+ return {
+ search: this.search,
+ first: USERS_PER_PAGE,
+ };
+ },
+ update(data) {
+ return data;
+ },
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ },
+ },
+ data() {
+ return {
+ user: '',
+ search: '',
+ };
+ },
+ computed: {
+ userId() {
+ return getIdFromGraphQLId(this.user);
+ },
+ users() {
+ return [
+ { text: __('(no user)'), value: '' },
+ ...(this.usersQuery?.users.nodes || []).map((u) => ({
+ username: `@${u.username}`,
+ avatarUrl: u.avatarUrl,
+ text: u.name,
+ value: u.id,
+ })),
+ ];
+ },
+ },
+ methods: {
+ clearTransform() {
+ // FIXME: workaround for listbox issue
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1986
+ const { listbox } = this.$refs;
+ if (listbox.querySelector('.dropdown-menu')) {
+ listbox.querySelector('.dropdown-menu').style.transform = '';
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-listbox
+ ref="listbox"
+ v-model="user"
+ :items="users"
+ searchable
+ is-check-centered
+ :searching="$apollo.loading"
+ @click.capture.native="clearTransform"
+ @search="search = $event"
+ >
+ <template #list-item="{ item }">
+ <gl-avatar-labeled
+ shape="circle"
+ :size="32"
+ :src="item.avatarUrl"
+ :label="item.text"
+ :sub-label="item.username"
+ />
+ </template>
+ </gl-listbox>
+ <input type="hidden" :name="name" :value="userId" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
index 86b80a0ba5b..ef549f20cf3 100644
--- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
@@ -1,3 +1,19 @@
-import UsersSelect from '~/users_select';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import UserSelect from './components/user_select.vue';
-new UsersSelect(); // eslint-disable-line no-new
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+Array.from(document.querySelectorAll('.js-gitlab-user')).forEach(
+ (node) =>
+ new Vue({
+ el: node,
+ apolloProvider,
+ render: (h) => h(UserSelect, { props: { name: node.dataset.name } }),
+ }),
+);
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 7126d69c8c6..c33b1468ca4 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -25,6 +25,8 @@ import {
Tracking,
IssuableAttributeState,
IssuableAttributeType,
+ LocalizedIssuableAttributeType,
+ IssuableAttributeTypeKeyMap,
issuableAttributesQueries,
noAttributeId,
defaultEpicSort,
@@ -229,7 +231,9 @@ export default {
return timeFor(this.currentAttribute?.dueDate);
},
i18n() {
- return dropdowni18nText(this.issuableAttribute, this.issuableType);
+ const localizedAttribute =
+ LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]];
+ return dropdowni18nText(localizedAttribute, this.issuableType);
},
isEpic() {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 60cb4cff727..6248bcb8e2d 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,3 +1,4 @@
+import { invert } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
@@ -251,6 +252,12 @@ export const IssuableAttributeType = {
Milestone: 'milestone',
};
+export const LocalizedIssuableAttributeType = {
+ Milestone: s__('Issuable|milestone'),
+};
+
+export const IssuableAttributeTypeKeyMap = invert(IssuableAttributeType);
+
export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 5963568a00b..bd425bdc2a8 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -819,13 +819,14 @@ UsersSelect.prototype.renderRow = function (
const tooltipAttributes = tooltip
? `data-container="body" data-placement="left" data-title="${tooltip}"`
: '';
+ const dataUserSuggested = user.suggested ? `data-user-suggested=${user.suggested}` : '';
const name =
user?.availability && isUserBusy(user.availability)
? sprintf(__('%{name} (Busy)'), { name: user.name })
: user.name;
return `
- <li data-user-id=${user.id}>
+ <li data-user-id=${user.id} ${dataUserSuggested}>
<a href="#" class="dropdown-menu-user-link gl-display-flex! gl-align-items-center ${linkClasses}" ${tooltipAttributes}>
${this.renderRowAvatar(issuableType, user, img)}
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
index d67ff11f297..e3f87c08ad4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -28,7 +28,7 @@ const nonStandardEvents = {
},
counter: {},
},
- testReport: {
+ testSummary: {
uniqueUser: {
expand: ['i_testing_summary_widget_total'],
},
diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue
index 62d6c03bbb3..5ec16d4ba15 100644
--- a/app/assets/javascripts/webhooks/components/form_url_app.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_app.vue
@@ -1,5 +1,6 @@
<script>
-import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import FormUrlMaskItem from './form_url_mask_item.vue';
@@ -11,19 +12,60 @@ export default {
GlFormInput,
GlFormRadio,
GlFormRadioGroup,
+ GlLink,
+ },
+ props: {
+ initialUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ initialUrlVariables: {
+ type: Array,
+ required: false,
+ default: null,
+ },
},
data() {
return {
- maskEnabled: false,
- url: null,
+ maskEnabled: !isEmpty(this.initialUrlVariables),
+ url: this.initialUrl,
+ items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
};
},
computed: {
maskedUrl() {
- return this.url;
+ if (!this.url) {
+ return null;
+ }
+
+ let maskedUrl = this.url;
+
+ this.items.forEach(({ key, value }) => {
+ if (!key || !value) {
+ return;
+ }
+
+ const replacementExpression = new RegExp(value, 'g');
+ maskedUrl = maskedUrl.replace(replacementExpression, `{${key}}`);
+ });
+
+ return maskedUrl;
+ },
+ },
+ methods: {
+ onItemInput({ index, key, value }) {
+ this.$set(this.items, index, { key, value });
+ },
+ addItem() {
+ this.items.push({});
+ },
+ removeItem(index) {
+ this.items.splice(index, 1);
},
},
i18n: {
+ addItem: s__('Webhooks|+ Mask another portion of URL'),
radioFullUrlText: s__('Webhooks|Show full URL'),
radioMaskUrlText: s__('Webhooks|Mask portions of URL'),
radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'),
@@ -49,6 +91,7 @@ export default {
v-model="url"
name="hook[url]"
:placeholder="$options.i18n.urlPlaceholder"
+ data-testid="form-url"
/>
</gl-form-group>
<div class="gl-mt-5">
@@ -63,9 +106,27 @@ export default {
</gl-form-radio-group>
<div v-if="maskEnabled" class="gl-ml-6" data-testid="url-mask-section">
- <form-url-mask-item :index="0" />
+ <form-url-mask-item
+ v-for="({ key, value }, index) in items"
+ :key="index"
+ :index="index"
+ :item-key="key"
+ :item-value="value"
+ @input="onItemInput"
+ @remove="removeItem"
+ />
+ <div class="gl-mb-5">
+ <gl-link @click="addItem">{{ $options.i18n.addItem }}</gl-link>
+ </div>
+
<gl-form-group :label="$options.i18n.urlPreview" label-for="webhook-url-preview">
- <gl-form-input id="webhook-url-preview" :value="maskedUrl" readonly />
+ <gl-form-input
+ id="webhook-url-preview"
+ :value="maskedUrl"
+ readonly
+ name="hook[url]"
+ data-testid="form-url-preview"
+ />
</gl-form-group>
</div>
</div>
diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
index 1e74b4a8215..3b75f9b6c0d 100644
--- a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
@@ -14,6 +14,16 @@ export default {
required: false,
default: null,
},
+ itemKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ itemValue: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
keyInputId() {
@@ -30,6 +40,15 @@ export default {
inputName(type) {
return `hook[url_variables][][${type}]`;
},
+ onKeyInput(key) {
+ this.$emit('input', { index: this.index, key, value: this.itemValue });
+ },
+ onValueInput(value) {
+ this.$emit('input', { index: this.index, key: this.itemKey, value });
+ },
+ onRemoveClick() {
+ this.$emit('remove', this.index);
+ },
},
i18n: {
keyLabel: s__('Webhooks|How it looks in the UI'),
@@ -39,14 +58,19 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-5">
+ <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-3">
<gl-form-group
:label="$options.i18n.valueLabel"
:label-for="valueInputId"
class="gl-flex-grow-1 gl-mb-0"
data-testid="mask-item-value"
>
- <gl-form-input :id="valueInputId" :name="inputName('value')" />
+ <gl-form-input
+ :id="valueInputId"
+ :name="inputName('value')"
+ :value="itemValue"
+ @input="onValueInput"
+ />
</gl-form-group>
<gl-form-group
:label="$options.i18n.keyLabel"
@@ -54,8 +78,13 @@ export default {
class="gl-flex-grow-1 gl-mb-0"
data-testid="mask-item-key"
>
- <gl-form-input :id="keyInputId" :name="inputName('key')" />
+ <gl-form-input
+ :id="keyInputId"
+ :name="inputName('key')"
+ :value="itemKey"
+ @input="onKeyInput"
+ />
</gl-form-group>
- <gl-button icon="remove" />
+ <gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
</div>
</template>
diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js
index bfa33560fa5..1b2b33e44c1 100644
--- a/app/assets/javascripts/webhooks/index.js
+++ b/app/assets/javascripts/webhooks/index.js
@@ -8,11 +8,18 @@ export default () => {
return null;
}
+ const { url: initialUrl, urlVariables } = el.dataset;
+
return new Vue({
el,
name: 'WebhookFormRoot',
render(createElement) {
- return createElement(FormUrlApp, {});
+ return createElement(FormUrlApp, {
+ props: {
+ initialUrl,
+ initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index cf0aafc2eb0..af9b8c6101a 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -23,6 +23,7 @@ import {
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_VIEWED_STORAGE_KEY,
+ WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_ITERATION,
} from '../constants';
@@ -40,6 +41,7 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
+import WorkItemMilestone from './work_item_milestone.vue';
import WorkItemInformation from './work_item_information.vue';
export default {
@@ -67,6 +69,7 @@ export default {
LocalStorageSync,
WorkItemTypeIcon,
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+ WorkItemMilestone,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -208,6 +211,9 @@ export default {
workItemIteration() {
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
+ workItemMilestone() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
+ },
},
beforeDestroy() {
/** make sure that if the user has not even dismissed the alert ,
@@ -411,6 +417,17 @@ export default {
:work-item-type="workItemType"
@error="updateError = $event"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-milestone
+ v-if="workItemMilestone"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.nodes[0]"
+ :work-item-type="workItemType"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
+ </template>
<work-item-weight
v-if="workItemWeight"
class="gl-mb-5"
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
new file mode 100644
index 00000000000..c4a36e36555
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -0,0 +1,248 @@
+<script>
+import {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { debounce } from 'lodash';
+import Tracking from '~/tracking';
+import { s__ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+const noMilestoneId = 'no-milestone-id';
+
+export default {
+ i18n: {
+ MILESTONE: s__('WorkItem|Milestone'),
+ NONE: s__('WorkItem|None'),
+ MILESTONE_PLACEHOLDER: s__('WorkItem|Add to milestone'),
+ NO_MATCHING_RESULTS: s__('WorkItem|No matching results'),
+ NO_MILESTONE: s__('WorkItem|No milestone'),
+ MILESTONE_FETCH_ERROR: s__(
+ 'WorkItem|Something went wrong while fetching milestones. Please try again.',
+ ),
+ },
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemMilestone: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ localMilestone: this.workItemMilestone,
+ searchTerm: '',
+ shouldFetch: false,
+ updateInProgress: false,
+ isFocused: false,
+ milestones: [],
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ emptyPlaceholder() {
+ return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
+ },
+ dropdownText() {
+ return this.localMilestone?.title || this.emptyPlaceholder;
+ },
+ isLoadingMilestones() {
+ return this.$apollo.queries.milestones.loading;
+ },
+ isNoMilestone() {
+ return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ },
+ dropdownClasses() {
+ return {
+ 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
+ 'is-not-focused': !this.isFocused,
+ };
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ milestones: {
+ query: projectMilestonesQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ title: this.searchTerm,
+ first: 20,
+ };
+ },
+ skip() {
+ return !this.shouldFetch;
+ },
+ update(data) {
+ return data?.workspace?.attributes?.nodes || [];
+ },
+ error() {
+ this.$emit('error', this.i18n.MILESTONE_FETCH_ERROR);
+ },
+ },
+ },
+ methods: {
+ handleMilestoneClick(milestone) {
+ this.localMilestone = milestone;
+ },
+ onDropdownShown() {
+ this.$refs.search.focusInput();
+ this.shouldFetch = true;
+ this.isFocused = true;
+ },
+ onDropdownHide() {
+ this.isFocused = false;
+ this.searchTerm = '';
+ this.shouldFetch = false;
+ this.updateMilestone();
+ },
+ setSearchKey(value) {
+ this.searchTerm = value;
+ },
+ isMilestoneChecked(milestone) {
+ return this.localMilestone?.id === milestone?.id;
+ },
+ updateMilestone() {
+ if (this.workItemMilestone?.id === this.localMilestone?.id) {
+ return;
+ }
+
+ this.track('updated_milestone');
+ this.updateInProgress = true;
+ this.$apollo
+ .mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ milestone: {
+ milestoneId: this.localMilestone?.id,
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('\n'));
+ }
+ })
+ .catch((error) => {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.updateInProgress = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-dropdown"
+ :label="$options.i18n.MILESTONE"
+ label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span
+ v-if="!canUpdate"
+ class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal"
+ data-testid="disabled-text"
+ >
+ {{ dropdownText }}
+ </span>
+ <gl-dropdown
+ v-else
+ :toggle-class="dropdownClasses"
+ :text="dropdownText"
+ :loading="updateInProgress"
+ @shown="onDropdownShown"
+ @hide="onDropdownHide"
+ >
+ <template #header>
+ <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ </template>
+ <gl-dropdown-item
+ data-testid="no-milestone"
+ is-check-item
+ :is-checked="isNoMilestone"
+ @click="handleMilestoneClick({ id: 'no-milestone-id' })"
+ >
+ {{ $options.i18n.NO_MILESTONE }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-text v-if="isLoadingMilestones">
+ <gl-skeleton-loader :height="90">
+ <rect width="380" height="10" x="10" y="15" rx="4" />
+ <rect width="280" height="10" x="10" y="30" rx="4" />
+ <rect width="380" height="10" x="10" y="50" rx="4" />
+ <rect width="280" height="10" x="10" y="65" rx="4" />
+ </gl-skeleton-loader>
+ </gl-dropdown-text>
+ <template v-else-if="milestones.length">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ is-check-item
+ :is-checked="isMilestoneChecked(milestone)"
+ @click="handleMilestoneClick(milestone)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
+ </gl-dropdown>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 0d426299408..7737c535650 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -17,6 +17,7 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
+export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index d3712da1329..36779dfe11e 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,5 +1,6 @@
enum LocalWidgetType {
ASSIGNEES
+ MILESTONE
}
interface LocalWorkItemWidget {
@@ -11,6 +12,15 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
+type LocalWorkItemMilestone implements LocalWorkItemWidget {
+ type: LocalWidgetType!
+ nodes: [Milestone!]
+}
+
+extend type WorkItem {
+ mockWidgets: [LocalWorkItemWidget]
+}
+
input LocalUserInput {
id: ID!
name: String
@@ -19,9 +29,14 @@ input LocalUserInput {
avatarUrl: String
}
+input LocalMilestoneInput {
+ milestoneId: ID!
+}
+
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [LocalUserInput!]
+ milestone: LocalMilestoneInput!
}
type LocalWorkItemPayload {
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 3b46fed97ec..fa0ab56df75 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -3,5 +3,16 @@
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
+ mockWidgets @client {
+ ... on LocalWorkItemMilestone {
+ type
+ nodes {
+ id
+ title
+ expired
+ dueDate
+ }
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 7a5cc72ceb8..820a1a0b53e 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -64,7 +64,7 @@
}
}
-.work-item-iteration {
+.work-item-dropdown {
.gl-dropdown-toggle {
background: none !important;
@@ -82,4 +82,3 @@
}
}
}
-
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index ebd958822ed..fcf6871d137 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -11,7 +11,6 @@ class IdeController < ApplicationController
push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:schema_linting)
push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab)
- push_frontend_feature_flag(:vscode_web_ide, current_user)
define_index_vars
end
@@ -27,7 +26,7 @@ class IdeController < ApplicationController
namespace: project&.namespace, user: current_user)
end
- render layout: 'fullscreen', locals: { minimal: Feature.enabled?(:vscode_web_ide, current_user) }
+ render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? }
end
private
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index c0360d10392..a57c87bf691 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -56,7 +56,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:gitpod_enabled,
:render_whitespace_in_code,
:markdown_surround_selection,
- :markdown_automatic_lists
+ :markdown_automatic_lists,
+ :use_legacy_web_ide
]
end
end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 2484081a828..dd2286d333d 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -57,7 +57,7 @@ module Types
field :deployments,
Types::DeploymentType.connection_type,
null: true,
- description: 'Deployments of the environment. This field can only be resolved for one project in any single request.',
+ description: 'Deployments of the environment. This field can only be resolved for one environment in any single request.',
resolver: Resolvers::DeploymentsResolver do
extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
end
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 1e50033e0e0..e050ccc0e40 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -1,6 +1,13 @@
# frozen_string_literal: true
module HooksHelper
+ def webhook_form_data(hook)
+ {
+ url: hook.url,
+ url_variables: nil
+ }
+ end
+
def link_to_test_hook(hook, trigger)
path = test_hook_path(hook, trigger)
trigger_human_name = trigger.to_s.tr('_', ' ').camelize
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index ec1327cf7ae..5b3ca25b5af 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -3,6 +3,32 @@
module IdeHelper
def ide_data
{
+ 'can-use-new-web-ide' => can_use_new_web_ide?.to_s,
+ 'use-new-web-ide' => use_new_web_ide?.to_s,
+ 'user-preferences-path' => profile_preferences_path,
+ 'branch-name' => @branch
+ }.merge(use_new_web_ide? ? new_ide_data : legacy_ide_data)
+ end
+
+ def can_use_new_web_ide?
+ Feature.enabled?(:vscode_web_ide, current_user)
+ end
+
+ def use_new_web_ide?
+ can_use_new_web_ide? && !current_user.use_legacy_web_ide
+ end
+
+ private
+
+ def new_ide_data
+ {
+ 'project-path' => @project&.path_with_namespace,
+ 'csp-nonce' => content_security_policy_nonce
+ }
+ end
+
+ def legacy_ide_data
+ {
'empty-state-svg-path' => image_path('illustrations/multi_file_editor_empty.svg'),
'no-changes-state-svg-path' => image_path('illustrations/multi-editor_no_changes_empty.svg'),
'committed-state-svg-path' => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
@@ -13,7 +39,6 @@ module IdeHelper
'clientside-preview-enabled': Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?.to_s,
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
'codesandbox-bundler-url': Gitlab::CurrentSettings.web_ide_clientside_preview_bundler_url,
- 'branch-name' => @branch,
'default-branch' => @project && @project.default_branch,
'file-path' => @path,
'merge-request' => @merge_request,
@@ -24,13 +49,10 @@ module IdeHelper
'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'),
'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'),
'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'),
- 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration'),
- 'csp-nonce' => content_security_policy_nonce
+ 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration')
}
end
- private
-
def convert_to_project_entity_json(project)
return unless project
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f83aa79b461..361b1a8dca9 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -410,6 +410,13 @@ class ApplicationSetting < ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ # rubocop:disable Cop/StaticTranslationDefinition
+ validates :deactivate_dormant_users_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") },
+ if: :deactivate_dormant_users?
+ # rubocop:enable Cop/StaticTranslationDefinition
+
Gitlab::SSHPublicKey.supported_types.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 4287c0b7884..950e0a583bc 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1364,9 +1364,9 @@ module Ci
self.builds.latest.build_matchers(project)
end
- def authorized_cluster_agents
- strong_memoize(:authorized_cluster_agents) do
- ::Clusters::AgentAuthorizationsFinder.new(project).execute.map(&:agent)
+ def cluster_agent_authorizations
+ strong_memoize(:cluster_agent_authorizations) do
+ ::Clusters::AgentAuthorizationsFinder.new(project).execute
end
end
diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb
index 9f7f653ed65..a365ccdc568 100644
--- a/app/models/clusters/agents/implicit_authorization.rb
+++ b/app/models/clusters/agents/implicit_authorization.rb
@@ -16,7 +16,7 @@ module Clusters
end
def config
- nil
+ {}
end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b36b00fcbaf..6d198fc755b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -354,6 +354,7 @@ class User < ApplicationRecord
:markdown_automatic_lists, :markdown_automatic_lists=,
:diffs_deletion_color, :diffs_deletion_color=,
:diffs_addition_color, :diffs_addition_color=,
+ :use_legacy_web_ide, :use_legacy_web_ide=,
to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index b8f30413404..c6ebd550daf 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -22,6 +22,7 @@ class UserPreference < ApplicationRecord
validates :diffs_deletion_color, :diffs_addition_color,
format: { with: ColorsHelper::HEX_COLOR_PATTERN },
allow_blank: true
+ validates :use_legacy_web_ide, allow_nil: false, inclusion: { in: [true, false] }
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 71a05ef2c72..706608e3029 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -34,7 +34,9 @@ module Ci
def runner_variables
stop_expanding_file_vars = ::Feature.enabled?(:ci_stop_expanding_file_vars_for_runners, project)
- variables.sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars).to_runner_variables
+ variables
+ .sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars, project: project)
+ .to_runner_variables
end
def refspecs
diff --git a/app/services/bulk_imports/uploads_export_service.rb b/app/services/bulk_imports/uploads_export_service.rb
index 7f5ee7b8624..315590bea31 100644
--- a/app/services/bulk_imports/uploads_export_service.rb
+++ b/app/services/bulk_imports/uploads_export_service.rb
@@ -22,8 +22,9 @@ module BulkImports
subdir_path = export_subdir_path(upload)
mkdir_p(subdir_path)
download_or_copy_upload(uploader, File.join(subdir_path, uploader.filename))
- rescue Errno::ENAMETOOLONG => e
- # Do not fail entire export process if downloaded file has filename that exceeds 255 characters.
+ rescue StandardError => e
+ # Do not fail entire project export if something goes wrong during file download
+ # (e.g. downloaded file has filename that exceeds 255 characters).
# Ignore raised exception, skip such upload, log the error and keep going with the export instead.
Gitlab::ErrorTracking.log_exception(e, portable_id: portable.id, portable_class: portable.class.name, upload_id: upload.id)
end
diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
index 894ab8e8505..347bc99dbf5 100644
--- a/app/services/ci/generate_kubeconfig_service.rb
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -14,7 +14,8 @@ module Ci
url: Gitlab::Kas.tunnel_url
)
- agents.each do |agent|
+ agent_authorizations.each do |authorization|
+ agent = authorization.agent
user = user_name(agent)
template.add_user(
@@ -24,6 +25,7 @@ module Ci
template.add_context(
name: context_name(agent),
+ namespace: context_namespace(authorization),
cluster: cluster_name,
user: user
)
@@ -36,8 +38,8 @@ module Ci
attr_reader :pipeline, :token, :template
- def agents
- pipeline.authorized_cluster_agents
+ def agent_authorizations
+ pipeline.cluster_agent_authorizations
end
def cluster_name
@@ -52,6 +54,10 @@ module Ci
[agent.project.full_path, agent.name].join(delimiter)
end
+ def context_namespace(authorization)
+ authorization.config['default_namespace']
+ end
+
def agent_token(agent)
['ci', agent.id, token].join(delimiter)
end
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index b08a549148d..c091a2180c5 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -54,10 +54,10 @@
- dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link }
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
.form-group
- = f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light'
- = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1'
+ = f.label :deactivate_dormant_users_period, _('Days of inactivity before deactivation'), class: 'label-light'
+ = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '90', step: '1'
.form-text.text-muted
- = _('Period of inactivity before deactivation.')
+ = _('Must be 90 days or more.')
.form-group
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 28836055e0e..9d4c0f62134 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -23,23 +23,21 @@
%p
= html_escape(_('Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. "By %{link_open}@johnsmith%{link_close}"). It will also associate and/or assign these issues and comments with the selected user.')) % { link_open: '<a href="#">'.html_safe, link_close: '</a>'.html_safe }
- .table-holder
- %table.table
- %thead
+ %table.table
+ %thead
+ %tr
+ %th= _("ID")
+ %th= _("Name")
+ %th= _("Email")
+ %th= _("GitLab User")
+ %tbody
+ - @user_map.each do |id, user|
%tr
- %th= _("ID")
- %th= _("Name")
- %th= _("Email")
- %th= _("GitLab User")
- %tbody
- - @user_map.each do |id, user|
- %tr
- %td= id
- %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
- %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
- %td
- = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
- scope: :all, email_user: true, selected: user[:gitlab_user])
+ %td= id
+ %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control gl-form-input'
+ %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control gl-form-input'
+ %td
+ .js-gitlab-user{ data: { name: "users[#{id}][gitlab_user]" } }
.form-actions
= submit_tag _('Continue to the next step'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 549436ccabf..c95e63bdc83 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,7 +1,7 @@
= form_errors(hook)
- if Feature.enabled?(:webhook_form_mask_url)
- .js-vue-webhook-form
+ .js-vue-webhook-form{ data: webhook_form_data(hook) }
- else
.form-group
= form.label :url, s_('Webhooks|URL'), class: 'label-bold'
diff --git a/db/migrate/20221006141145_add_targets_to_elastic_reindexing_tasks.rb b/db/migrate/20221006141145_add_targets_to_elastic_reindexing_tasks.rb
new file mode 100644
index 00000000000..1631f8ae57e
--- /dev/null
+++ b/db/migrate/20221006141145_add_targets_to_elastic_reindexing_tasks.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTargetsToElasticReindexingTasks < Gitlab::Database::Migration[2.0]
+ def change
+ add_column :elastic_reindexing_tasks, :targets, :text, array: true
+ end
+end
diff --git a/db/migrate/20221011210455_add_use_legacy_web_ide_to_user_preferences.rb b/db/migrate/20221011210455_add_use_legacy_web_ide_to_user_preferences.rb
new file mode 100644
index 00000000000..1b434e10ab0
--- /dev/null
+++ b/db/migrate/20221011210455_add_use_legacy_web_ide_to_user_preferences.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddUseLegacyWebIdeToUserPreferences < Gitlab::Database::Migration[2.0]
+ enable_lock_retries!
+
+ def change
+ add_column :user_preferences, :use_legacy_web_ide, :boolean, default: false, null: false
+ end
+end
diff --git a/db/post_migrate/20221013154159_update_invalid_dormant_user_setting.rb b/db/post_migrate/20221013154159_update_invalid_dormant_user_setting.rb
new file mode 100644
index 00000000000..1f1e47fdac1
--- /dev/null
+++ b/db/post_migrate/20221013154159_update_invalid_dormant_user_setting.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class UpdateInvalidDormantUserSetting < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ # rubocop:disable Layout/LineLength
+ def up
+ execute("update application_settings set deactivate_dormant_users_period=90 where deactivate_dormant_users_period < 90")
+ end
+ # rubocop:enable Layout/LineLength
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20221006141145 b/db/schema_migrations/20221006141145
new file mode 100644
index 00000000000..269913ca389
--- /dev/null
+++ b/db/schema_migrations/20221006141145
@@ -0,0 +1 @@
+ae45bc7d67354b64e359ac7fadefec6a0d81cd529f5ae2517a6a6a5d250f9024 \ No newline at end of file
diff --git a/db/schema_migrations/20221011210455 b/db/schema_migrations/20221011210455
new file mode 100644
index 00000000000..2a6a7349f5d
--- /dev/null
+++ b/db/schema_migrations/20221011210455
@@ -0,0 +1 @@
+3c2445871613743560b2dd0a111fafab30f503b1c462e7ba7aee03f85e25f775 \ No newline at end of file
diff --git a/db/schema_migrations/20221013154159 b/db/schema_migrations/20221013154159
new file mode 100644
index 00000000000..2e147bb199d
--- /dev/null
+++ b/db/schema_migrations/20221013154159
@@ -0,0 +1 @@
+dbf241baf6d3deb1ef29a7cdca012050cab51c5f86762a0363d9dc4dc14fd804 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 10b2d26b1e0..2dab5e7abc9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14981,6 +14981,7 @@ CREATE TABLE elastic_reindexing_tasks (
delete_original_index_at timestamp with time zone,
max_slices_running smallint DEFAULT 60 NOT NULL,
slice_multiplier smallint DEFAULT 2 NOT NULL,
+ targets text[],
CONSTRAINT check_7f64acda8e CHECK ((char_length(error_message) <= 255))
);
@@ -22272,6 +22273,7 @@ CREATE TABLE user_preferences (
diffs_deletion_color text,
diffs_addition_color text,
markdown_automatic_lists boolean DEFAULT true NOT NULL,
+ use_legacy_web_ide boolean DEFAULT false NOT NULL,
CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)),
CONSTRAINT check_d07ccd35f7 CHECK ((char_length(diffs_addition_color) <= 7))
);
diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md
index 69883b159c3..566df2ee509 100644
--- a/doc/administration/geo/replication/datatypes.md
+++ b/doc/administration/geo/replication/datatypes.md
@@ -61,6 +61,8 @@ verification methods:
| Blobs | CI Secure Files _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | Incident Metric Images _(file system)_ | Geo with API/Managed | SHA256 checksum |
| Blobs | Incident Metric Images _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
+| Blobs | Alert Metric Images _(file system)_ | Geo with API | SHA256 checksum |
+| Blobs | Alert Metric Images _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
- (*1*): Redis replication can be used as part of HA with Redis sentinel. It's not used between Geo sites.
- (*2*): Object storage replication can be performed by Geo or by your object storage provider/appliance
@@ -207,10 +209,10 @@ Requires additional configuration. See [instructions](container_registry.md) to
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | **Yes** (13.12) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0. |
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification was behind the feature flag `geo_merge_request_diff_verification`, removed in 14.7.|
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | N/A | N/A | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
-| [GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
-| [Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
-| [Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | **Yes** (15.5) | **Yes**(15.5) | Yes | Yes | Replication/Verification is handled via the Uploads data type |
-|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | No | No | |
+|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
+|[Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
+| [Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | **Yes** (15.5) | **Yes**(15.5) | **Yes** (15.5) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication/Verification is handled via the Uploads data type. | |
+|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | **Yes** (15.5) | **Yes** (15.5) | **Yes** (15.5) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication/Verification is handled via the Uploads data type. |
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | N/A | N/A | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|[Elasticsearch integration](../../../integration/advanced_search/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries use the same ES cluster as the primary. |
|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [Planned](https://gitlab.com/groups/gitlab-org/-/epics/8833) | No | No | No | Blocked by [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Replication of this cache is not needed for disaster recovery purposes because it can be recreated from external sources. |
diff --git a/doc/administration/operations/rails_console.md b/doc/administration/operations/rails_console.md
index 9d6d0a0774d..1ef985b8938 100644
--- a/doc/administration/operations/rails_console.md
+++ b/doc/administration/operations/rails_console.md
@@ -240,6 +240,24 @@ project.id
# => 2537
```
+## Time an operation
+
+If you'd like to time one or more operations, use the following format, replacing
+the placeholder `<operation>` with your Ruby or Rails commands of choice:
+
+```ruby
+# A single operation
+Benchmark.measure { <operation> }
+
+# A breakdown of multiple operations
+Benchmark.bm do |x|
+ x.report(:label1) { <operation_1> }
+ x.report(:label2) { <operation_2> }
+end
+```
+
+For more information, review [our developer documentation about benchmarks](../../development/performance.md#benchmarks).
+
## Active Record objects
### Looking up database-persisted objects
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index e5ee6051000..6ae840a18ec 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -62,19 +62,6 @@ Notify.test_email(e, "Test email for #{n}", 'Test email').deliver_now
Notify.test_email(u.email, "Test email for #{u.name}", 'Test email').deliver_now
```
-## Time an operation
-
-```ruby
-# A single operation
-Benchmark.measure { <operation> }
-
-# A breakdown of multiple operations
-Benchmark.bm do |x|
- x.report(:label1) { <operation_1> }
- x.report(:label2) { <operation_2> }
-end
-```
-
## Imports and exports
### Import a project
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 544251668f9..1cf1051cb47 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11916,7 +11916,7 @@ Describes where code is deployed for a project.
##### `Environment.deployments`
-Deployments of the environment. This field can only be resolved for one project in any single request.
+Deployments of the environment. This field can only be resolved for one environment in any single request.
Returns [`DeploymentConnection`](#deploymentconnection).
diff --git a/doc/api/packages/terraform-modules.md b/doc/api/packages/terraform-modules.md
index d7b14cb7c96..4c32e3f7cb4 100644
--- a/doc/api/packages/terraform-modules.md
+++ b/doc/api/packages/terraform-modules.md
@@ -25,12 +25,12 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
+| `module_namespace` | string | yes | The top-level group (namespace) to which Terraform module's project or subgroup belongs.|
| `module_name` | string | yes | The module name. |
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/versions"
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/versions"
```
Example response:
@@ -88,7 +88,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local"
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local"
```
Example response:
@@ -127,7 +127,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0"
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0"
```
Example response:
@@ -166,7 +166,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/download"
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/download"
```
Example response:
@@ -195,7 +195,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
| `module_version` | string | yes | Specific module version to download. |
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/download"
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/download"
```
Example response:
@@ -220,11 +220,11 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
| `module_version` | string | yes | Specific module version to download. |
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file"
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file"
```
To write the output to file:
```shell
-curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file" --output hello-world-local.tgz
+curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file" --output hello-world-local.tgz
```
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index b0cfb7c7d9f..751bbd75c7a 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -9,16 +9,19 @@ type: reference, api
## List repository tree
+> Iterating pages of results with a number (`?page=2`) [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67509) in GitLab 14.3.
+
Get a list of repository files and directories in a project. This endpoint can
be accessed without authentication if the repository is publicly accessible.
-This command provides essentially the same functionality as the `git ls-tree` command. For more information, see the section _Tree Objects_ in the [Git internals documentation](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects).
+This command provides essentially the same features as the `git ls-tree`
+command. For more information, refer to the section
+[Tree Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects)
+in the Git internals documentation.
WARNING:
-This endpoint is changing to keyset-based pagination. Iterating pages of results
-with a number (`?page=2`) [is deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67509).
-Support for iterating with a number became supported in GitLab 15.0. Use
-the new [keyset pagination system](index.md#keyset-based-pagination) instead.
+This endpoint changed to [keyset-based pagination](index.md#keyset-based-pagination)
+in GitLab 15.0. Iterating pages of results with a number (`?page=2`) is unsupported.
```plaintext
GET /projects/:id/repository/tree
@@ -29,12 +32,12 @@ Supported attributes:
| Attribute | Type | Required | Description |
| :---------- | :------------- | :------- | :---------- |
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `path` | string | no | The path inside repository. Used to get content of subdirectories. |
-| `ref` | string | no | The name of a repository branch or tag or if not given the default branch. |
-| `recursive` | boolean | no | Boolean value used to get a recursive tree (false by default). |
-| `per_page` | integer | no | Number of results to show per page. If not specified, defaults to `20`. [Learn more on pagination](index.md#pagination). |
-| `pagination` | string | no | If set to `keyset`, use the new keyset pagination method. |
| `page_token` | string | no | The tree record ID at which to fetch the next page. Used only with keyset pagination. |
+| `pagination` | string | no | If `keyset`, use the [keyset-based pagination method](index.md#keyset-based-pagination). |
+| `path` | string | no | The path inside the repository. Used to get content of subdirectories. |
+| `per_page` | integer | no | Number of results to show per page. If not specified, defaults to `20`. [Learn more on pagination](index.md#pagination). |
+| `recursive` | boolean | no | Boolean value used to get a recursive tree. Default is `false`. |
+| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch. |
```json
[
@@ -92,9 +95,9 @@ Supported attributes:
## Get a blob from repository
-Allows you to receive information about blob in repository like size and
-content. Blob content is Base64 encoded. This endpoint can be accessed
-without authentication if the repository is publicly accessible.
+Allows you to receive information, such as size and content, about blobs in a repository.
+Blob content is Base64 encoded. This endpoint can be accessed without authentication,
+if the repository is publicly accessible.
```plaintext
GET /projects/:id/repository/blobs/:sha
@@ -109,7 +112,7 @@ Supported attributes:
## Raw blob content
-Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
+Get the raw file contents for a blob, by blob SHA. This endpoint can be accessed
without authentication if the repository is publicly accessible.
```plaintext
@@ -131,24 +134,32 @@ Supported attributes:
Get an archive of the repository. This endpoint can be accessed without
authentication if the repository is publicly accessible.
-This endpoint has a rate limit threshold of 5 requests per minute for GitLab.com users.
+For GitLab.com users, this endpoint has a rate limit threshold of 5 requests per minute.
```plaintext
GET /projects/:id/repository/archive[.format]
```
-`format` is an optional suffix for the archive format. Default is
-`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, `tbz2`, `tb2`,
-`bz2`, `tar`, and `zip`. For example, specifying `archive.zip`
-would send an archive in ZIP format.
+`format` is an optional suffix for the archive format, and defaults to
+`tar.gz`. For example, specifying `archive.zip` sends an archive in ZIP format.
+Available options are:
+
+- `bz2`
+- `tar`
+- `tar.bz2`
+- `tar.gz`
+- `tb2`
+- `tbz`
+- `tbz2`
+- `zip`
Supported attributes:
| Attribute | Type | Required | Description |
|:------------|:---------------|:---------|:----------------------|
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `sha` | string | no | The commit SHA to download. A tag, branch reference, or SHA can be used. This defaults to the tip of the default branch if not specified. |
-| `path` | string | no | The subpath of the repository to download. This defaults to the whole repository (empty string). |
+| `path` | string | no | The subpath of the repository to download. If an empty string, defaults to the whole repository. |
+| `sha` | string | no | The commit SHA to download. A tag, branch reference, or SHA can be used. If not specified, defaults to the tip of the default branch. |
Example request:
@@ -159,7 +170,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.com/api/v4/pr
## Compare branches, tags or commits
This endpoint can be accessed without authentication if the repository is
-publicly accessible. Diffs can have an empty diff string if [diff limits](../development/diffs.md#diff-limits) are reached.
+publicly accessible. Diffs can have an empty diff string if
+[diff limits](../development/diffs.md#diff-limits) are reached.
```plaintext
GET /projects/:id/repository/compare
@@ -172,8 +184,8 @@ Supported attributes:
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
| `from` | string | yes | The commit SHA or branch name. |
| `to` | string | yes | The commit SHA or branch name. |
-| `from_project_id` | integer | no | The ID to compare from |
-| `straight` | boolean | no | Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)'. Default is `false`. |
+| `from_project_id` | integer | no | The ID to compare from. |
+| `straight` | boolean | no | Comparison method: `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)'. Default is `false`. |
```plaintext
GET /projects/:id/repository/compare?from=master&to=feature
@@ -217,6 +229,9 @@ Example response:
## Contributors
+> - Attributes `additions` and `deletions` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39653) in GitLab 13.4, because they [always returned `0`](https://gitlab.com/gitlab-org/gitlab/-/issues/233119).
+> - Attributes `additions` and `deletions` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38920) in GitLab 14.0.
+
Get repository contributors list. This endpoint can be accessed without
authentication if the repository is publicly accessible.
@@ -224,9 +239,6 @@ authentication if the repository is publicly accessible.
GET /projects/:id/repository/contributors
```
-WARNING:
-The `additions` and `deletions` attributes are [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39653) as of GitLab 13.4, because they [always return `0`](https://gitlab.com/gitlab-org/gitlab/-/issues/233119).
-
Supported attributes:
| Attribute | Type | Required | Description |
@@ -255,16 +267,16 @@ Example response:
## Merge Base
-Get the common ancestor for 2 or more refs (commit SHAs, branch names or tags).
+Get the common ancestor for 2 or more refs, such as commit SHAs, branch names, or tags.
```plaintext
GET /projects/:id/repository/merge_base
```
| Attribute | Type | Required | Description |
-| --------- | -------------- | -------- | ------------------------------------------------------------------------------- |
-| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
-| `refs` | array | yes | The refs to find the common ancestor of, multiple refs can be passed |
+| --------- | -------------- | -------- | ---------------------------------------------------------------------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
+| `refs` | array | yes | The refs to find the common ancestor of. Accepts multiple refs. |
Example request:
@@ -293,17 +305,16 @@ Example response:
## Add changelog data to a changelog file
-> [Introduced](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/351) in GitLab 13.9.
+> - [Introduced](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/351) in GitLab 13.9.
+> - Commit range limits [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89032) in GitLab 15.1 [with a flag](../administration/feature_flags.md) named `changelog_commits_limitation`. Enabled by default.
Generate changelog data based on commits in a repository.
-Given a version (using [semantic versioning](https://semver.org/)) and a range
+Given a [semantic version](https://semver.org/) and a range
of commits, GitLab generates a changelog for all commits that use a particular
-[Git trailer](https://git-scm.com/docs/git-interpret-trailers).
-
-The output of this process is a new section in a changelog file in the Git
-repository of the given project. The output format is in Markdown, and can be
-customized.
+[Git trailer](https://git-scm.com/docs/git-interpret-trailers). GitLab adds
+a new Markdown-formatted section to a changelog file in the Git repository of
+the project. The output format can be customized.
```plaintext
POST /projects/:id/repository/changelog
@@ -314,30 +325,21 @@ Supported attributes:
| Attribute | Type | Required | Description |
| :-------- | :------- | :--------- | :---------- |
| `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). |
-| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
-| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the branch specified in the `branch` attribute. |
-| `date` | datetime | no | The date and time of the release, defaults to the current time. |
-| `branch` | string | no | The branch to commit the changelog changes to, defaults to the project's default branch. |
-| `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. |
+| `branch` | string | no | The branch to commit the changelog changes to. Defaults to the project's default branch. |
| `config_file` | string | no | Path to the changelog configuration file in the project's Git repository. Defaults to `.gitlab/changelog_config.yml`. |
-| `file` | string | no | The file to commit the changes to, defaults to `CHANGELOG.md`. |
-| `message` | string | no | The commit message to produce when committing the changes, defaults to `Add changelog for version X` where X is the value of the `version` argument. |
+| `date` | datetime | no | The date and time of the release. Defaults to the current time. |
+| `file` | string | no | The file to commit the changes to. Defaults to `CHANGELOG.md`. |
+| `from` | string | no | The SHA of the commit that marks the beginning of the range of commits to include in the changelog. This commit isn't included in the changelog. |
+| `message` | string | no | The commit message to use when committing the changes. Defaults to `Add changelog for version X`, where `X` is the value of the `version` argument. |
+| `to` | string | no | The SHA of the commit that marks the end of the range of commits to include in the changelog. This commit _is_ included in the changelog. Defaults to the branch specified in the `branch` attribute. Limited to 15000 commits unless the feature flag `changelog_commits_limitation` is disabled. |
+| `trailer` | string | no | The Git trailer to use for including commits. Defaults to `Changelog`. Case-sensitive: `Example` does not match `example` or `eXaMpLE`. |
-WARNING:
-GitLab treats trailers case-sensitively. If you set the `trailer` field to
-`Example`, GitLab _won't_ include commits that use the trailer `example`,
-`eXaMpLE`, or anything else that isn't _exactly_ `Example`.
-
-WARNING:
-The allowed commits range between `from` and `to` is limited to 15000 commits. To disable
-this restriction, [turn off the feature flag](../administration/feature_flags.md)
-`changelog_commits_limitation`.
+### Requirements for `from` attribute
If the `from` attribute is unspecified, GitLab uses the Git tag of the last
stable version that came before the version specified in the `version`
-attribute. This requires that Git tag names follow a specific format, allowing
-GitLab to extract a version from the tag names. By default, GitLab considers
-tags using these formats:
+attribute. For GitLab to extract version numbers from tag names, Git tag names
+must follow a specific format. By default, GitLab considers tags using these formats:
- `vX.Y.Z`
- `X.Y.Z`
@@ -350,7 +352,7 @@ For example, consider a project with the following tags:
- v1.1.0
- v2.0.0
-If the `version` attribute is `2.1.0`, GitLab uses tag v2.0.0. And when the
+If the `version` attribute is `2.1.0`, GitLab uses tag `v2.0.0`. And when the
version is `1.1.1`, or `1.2.0`, GitLab uses tag v1.1.0. The tag `v1.0.0-pre1` is
never used, because pre-release tags are ignored.
@@ -372,7 +374,8 @@ This command generates a changelog for version `1.0.0`.
The commit range:
- Starts with the tag of the last release.
-- Ends with the last commit on the target branch. The default target branch is the project's default branch.
+- Ends with the last commit on the target branch. The default target branch is
+ the project's default branch.
If the last tag is `v0.9.0` and the default branch is `main`, the range of commits
included in this example is `v0.9.0..main`:
@@ -638,28 +641,28 @@ At the top level, the following variable is available:
In a category, the following variables are available:
-- `title`: the title of the category (after it has been remapped).
- `count`: the number of entries in this category.
+- `entries`: the entries that belong to this category.
- `single_change`: a boolean that indicates if there is only one change (`true`),
or multiple changes (`false`).
-- `entries`: the entries that belong to this category.
+- `title`: the title of the category (after it has been remapped).
In an entry, the following variables are available (here `foo.bar` means that
`bar` is a sub-field of `foo`):
-- `title`: the title of the changelog entry (this is the commit title).
-- `commit.reference`: a reference to the commit, for example,
- `gitlab-org/gitlab@0a4cdd86ab31748ba6dac0f69a8653f206e5cfc7`.
-- `commit.trailers`: an object containing all the Git trailers that were present
- in the commit body.
-- `author.reference`: a reference to the commit author (for example, `@alice`).
- `author.contributor`: a boolean set to `true` when the author is not a project
member, otherwise `false`.
- `author.credit`: a boolean set to `true` when `author.contributor` is `true` or
when `include_groups` is configured, and the author is a member of one of the
groups.
+- `author.reference`: a reference to the commit author (for example, `@alice`).
+- `commit.reference`: a reference to the commit, for example,
+ `gitlab-org/gitlab@0a4cdd86ab31748ba6dac0f69a8653f206e5cfc7`.
+- `commit.trailers`: an object containing all the Git trailers that were present
+ in the commit body.
- `merge_request.reference`: a reference to the merge request that first
introduced the change (for example, `gitlab-org/gitlab!50063`).
+- `title`: the title of the changelog entry (this is the commit title).
The `author` and `merge_request` objects might not be present if the data
couldn't be determined. For example, when a commit is created without a
@@ -732,11 +735,11 @@ Supported attributes:
| Attribute | Type | Required | Description |
| :-------- | :------- | :--------- | :---------- |
| `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). |
+| `config_file` | string | no | The path of changelog configuration file in the project's Git repository, defaults to `.gitlab/changelog_config.yml`. |
+| `date` | datetime | no | The date and time of the release, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the branch specified in the `branch` attribute. |
-| `date` | datetime | no | The date and time of the release, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
| `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. |
-| `config_file` | string | no | The path of changelog configuration file in the project's Git repository, defaults to `.gitlab/changelog_config.yml`. |
```shell
curl --header "PRIVATE-TOKEN: token" "https://gitlab.com/api/v4/projects/42/repository/changelog?version=1.0.0"
diff --git a/doc/development/gitlab_flavored_markdown/specification_guide/index.md b/doc/development/gitlab_flavored_markdown/specification_guide/index.md
index b1ab39b0321..95d06907aa6 100644
--- a/doc/development/gitlab_flavored_markdown/specification_guide/index.md
+++ b/doc/development/gitlab_flavored_markdown/specification_guide/index.md
@@ -738,6 +738,20 @@ subgraph output:<br/>test results/output
end
```
+#### `verify-all-generated-files-are-up-to-date.rb` script
+
+The `scripts/glfm/verify-all-generated-files-are-up-to-date.rb` script
+runs the [`update-specification.rb`](#update-specificationrb-script).
+[`update-example-snapshots.rb`](#update-example-snapshotsrb-script) scripts,
+It fails with an exception and non-zero return code if running these scripts
+results in any diffs to the generated and committed
+[output specification files](#output-specification-files) or
+[example snapshot files](#example-snapshot-files).
+
+This script is run via the `glfm-verify` CI job to ensure that all changes to the
+[input specification files](#input-specification-files)
+are reflected in the generated output specification and example snapshot files.
+
### Specification files
These files represent the GLFM specification itself. They are all
diff --git a/doc/development/performance.md b/doc/development/performance.md
index f2417a49b27..4f22d9ceb03 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -391,7 +391,7 @@ We store these results also when running nightly scheduled CI jobs on the
default branch on `gitlab.com`. Statistics of these profiling data are
[available online](https://gitlab-org.gitlab.io/rspec_profiling_stats/). For
example, you can find which tests take longest to run or which execute the most
-queries. This can be handy for optimizing our tests or identifying performance
+queries. Use this to optimize our tests or identify performance
issues in our code.
## Memory optimization
diff --git a/doc/user/admin_area/moderate_users.md b/doc/user/admin_area/moderate_users.md
index fa2bf4b9616..ace1c6be5f8 100644
--- a/doc/user/admin_area/moderate_users.md
+++ b/doc/user/admin_area/moderate_users.md
@@ -171,11 +171,12 @@ Users can also be deactivated using the [GitLab API](../../api/users.md#deactiva
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320875) in GitLab 14.0.
> - Customizable time period [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336747) in GitLab 15.4
+> - The lower limit for inactive period set to 90 days [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100793) in GitLab 15.5
Administrators can enable automatic deactivation of users who either:
- Were created more than a week ago and have not signed in.
-- Have no activity for a specified period of time (defaults to 90 days).
+- Have no activity for a specified period of time (default and minimum is 90 days).
To do this:
@@ -183,7 +184,7 @@ To do this:
1. On the left sidebar, select **Settings > General**.
1. Expand the **Account and limit** section.
1. Under **Dormant users**, check **Deactivate dormant users after a period of inactivity**.
-1. Under **Period of inactivity (days)**, enter a period of time before deactivation.
+1. Under **Days of inactivity before deactivation**, enter the number of days before deactivation. Minimum value is 90 days.
1. Select **Save changes**.
When this feature is enabled, GitLab runs a job once a day to deactivate the dormant users.
diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md
index 18055e95bfb..ea26613639d 100644
--- a/doc/user/project/import/manifest.md
+++ b/doc/user/project/import/manifest.md
@@ -12,7 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
GitLab allows you to import all the required Git repositories
based on a manifest file like the one used by the
[Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml).
-This feature can be very handy when you need to import a project with many
+Use the manifest to import a project with many
repositories like the Android Open Source Project (AOSP).
## Requirements
diff --git a/doc/user/project/issues/csv_export.md b/doc/user/project/issues/csv_export.md
index 2e945402d7f..83265d3e954 100644
--- a/doc/user/project/issues/csv_export.md
+++ b/doc/user/project/issues/csv_export.md
@@ -15,7 +15,7 @@ notification email address as an attachment.
collected from issues into a **[comma-separated values](https://en.wikipedia.org/wiki/Comma-separated_values)** (CSV)
file, which stores tabular data in plain text.
-> _CSVs are a handy way of getting data from one program to another where one
+> _CSVs are a way of getting data from one program to another where one
program cannot read the other ones normal output._ [Ref](https://www.quora.com/What-is-a-CSV-file-and-its-uses)
<!-- vale gitlab.Spelling = NO -->
diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb
index 52673d03e69..b6d6e1a3e5f 100644
--- a/lib/gitlab/ci/variables/collection.rb
+++ b/lib/gitlab/ci/variables/collection.rb
@@ -72,7 +72,7 @@ module Gitlab
Collection.new(@variables.reject(&block))
end
- def expand_value(value, keep_undefined: false, expand_file_vars: true)
+ def expand_value(value, keep_undefined: false, expand_file_vars: true, project: nil)
value.gsub(Item::VARIABLES_REGEXP) do
match = Regexp.last_match # it is either a valid variable definition or a ($$ / %%)
full_match = match[0]
@@ -88,6 +88,16 @@ module Gitlab
if variable # VARIABLE_NAME is an existing variable
next variable.value unless variable.file?
+ # Will be cleaned up with https://gitlab.com/gitlab-org/gitlab/-/issues/378266
+ if project
+ # We only log if `project` exists to make sure it is called from `Ci::BuildRunnerPresenter`
+ # when the variables are sent to Runner.
+ Gitlab::AppJsonLogger.info(
+ event: 'file_variable_is_referenced_in_another_variable',
+ project_id: project.id
+ )
+ end
+
expand_file_vars ? variable.value : full_match
elsif keep_undefined
full_match # we do not touch the variable definition
@@ -97,7 +107,7 @@ module Gitlab
end
end
- def sort_and_expand_all(keep_undefined: false, expand_file_vars: true)
+ def sort_and_expand_all(keep_undefined: false, expand_file_vars: true, project: nil)
sorted = Sort.new(self)
return self.class.new(self, sorted.errors) unless sorted.valid?
@@ -112,7 +122,8 @@ module Gitlab
# expand variables as they are added
variable = item.to_runner_variable
variable[:value] = new_collection.expand_value(variable[:value], keep_undefined: keep_undefined,
- expand_file_vars: expand_file_vars)
+ expand_file_vars: expand_file_vars,
+ project: project)
new_collection.append(variable)
end
diff --git a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
index 4c8c7f26fe2..23a8dc0b44f 100644
--- a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
+++ b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb
@@ -173,9 +173,21 @@ module Gitlab
def alter_sequence_statements(old_table:, new_table:)
sequences_owned_by(old_table).map do |seq_info|
seq_name, column_name = seq_info.values_at(:name, :column_name)
- <<~SQL.chomp
+
+ statement_parts = []
+
+ # If a different user owns the old table, the conversion process will fail to reassign the sequence
+ # ownership to the new parent table (as it will be owned by the current user).
+ # Force the old table to be owned by the current user in that case.
+ unless current_user_owns_table?(old_table)
+ statement_parts << set_current_user_owns_table_statement(old_table)
+ end
+
+ statement_parts << <<~SQL.chomp
ALTER SEQUENCE #{quote_table_name(seq_name)} OWNED BY #{quote_table_name(new_table)}.#{quote_column_name(column_name)}
SQL
+
+ statement_parts.join(SQL_STATEMENT_SEPARATOR)
end
end
@@ -206,6 +218,23 @@ module Gitlab
{ name: name, column_name: column_name }
end
end
+
+ def table_owner(table_name)
+ connection.select_value(<<~SQL, nil, [table_name])
+ SELECT tableowner FROM pg_tables WHERE tablename = $1
+ SQL
+ end
+
+ def current_user_owns_table?(table_name)
+ current_user = connection.select_value('select current_user')
+ table_owner(table_name) == current_user
+ end
+
+ def set_current_user_owns_table_statement(table_name)
+ <<~SQL.chomp
+ ALTER TABLE #{connection.quote_table_name(table_name)} OWNER TO CURRENT_USER
+ SQL
+ end
end
end
end
diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb
index ad19508fb99..bc0563729a7 100644
--- a/lib/gitlab/import_export/uploads_manager.rb
+++ b/lib/gitlab/import_export/uploads_manager.rb
@@ -86,8 +86,9 @@ module Gitlab
mkdir_p(File.join(uploads_export_path, secret))
download_or_copy_upload(upload, upload_path)
- rescue Errno::ENAMETOOLONG => e
- # Do not fail entire project export if downloaded file has filename that exceeds 255 characters.
+ rescue StandardError => e
+ # Do not fail entire project export if something goes wrong during file download
+ # (e.g. downloaded file has filename that exceeds 255 characters).
# Ignore raised exception, skip such upload, log the error and keep going with the export instead.
Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6a40f2b7142..359e6b8ea8d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1253,6 +1253,9 @@ msgstr ""
msgid "'%{template_name}' is unknown or invalid"
msgstr ""
+msgid "'%{value}' days of inactivity must be greater than or equal to 90"
+msgstr ""
+
msgid "(%d closed)"
msgid_plural "(%d closed)"
msgstr[0] ""
@@ -1291,6 +1294,9 @@ msgstr ""
msgid "(max size 15 MB)"
msgstr ""
+msgid "(no user)"
+msgstr ""
+
msgid "(optional)"
msgstr ""
@@ -12565,6 +12571,9 @@ msgstr ""
msgid "Days"
msgstr ""
+msgid "Days of inactivity before deactivation"
+msgstr ""
+
msgid "Days to merge"
msgstr ""
@@ -19520,12 +19529,6 @@ msgstr ""
msgid "GroupsTree|Loading groups"
msgstr ""
-msgid "GroupsTree|No groups matched your search"
-msgstr ""
-
-msgid "GroupsTree|No groups or projects matched your search"
-msgstr ""
-
msgid "GroupsTree|Options"
msgstr ""
@@ -22424,6 +22427,18 @@ msgstr ""
msgid "IssuableStatus|promoted"
msgstr ""
+msgid "Issuable|epic"
+msgstr ""
+
+msgid "Issuable|escalation policy"
+msgstr ""
+
+msgid "Issuable|iteration"
+msgstr ""
+
+msgid "Issuable|milestone"
+msgstr ""
+
msgid "Issue"
msgstr ""
@@ -26426,6 +26441,9 @@ msgstr ""
msgid "Multiplier to apply to polling intervals. Decimal values are supported. Defaults to 1."
msgstr ""
+msgid "Must be 90 days or more."
+msgstr ""
+
msgid "My awesome group"
msgstr ""
@@ -29354,12 +29372,6 @@ msgstr ""
msgid "Period in seconds"
msgstr ""
-msgid "Period of inactivity (days)"
-msgstr ""
-
-msgid "Period of inactivity before deactivation."
-msgstr ""
-
msgid "Permalink"
msgstr ""
@@ -45217,6 +45229,9 @@ msgstr ""
msgid "Webhooks Help"
msgstr ""
+msgid "Webhooks|+ Mask another portion of URL"
+msgstr ""
+
msgid "Webhooks|A comment is added to a confidential issue."
msgstr ""
@@ -45797,6 +45812,9 @@ msgstr ""
msgid "WorkItem|Add to iteration"
msgstr ""
+msgid "WorkItem|Add to milestone"
+msgstr ""
+
msgid "WorkItem|Are you sure you want to cancel editing?"
msgstr ""
@@ -45853,12 +45871,18 @@ msgstr ""
msgid "WorkItem|Learn about tasks."
msgstr ""
+msgid "WorkItem|Milestone"
+msgstr ""
+
msgid "WorkItem|No iteration"
msgstr ""
msgid "WorkItem|No matching results"
msgstr ""
+msgid "WorkItem|No milestone"
+msgstr ""
+
msgid "WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts."
msgstr ""
@@ -45907,6 +45931,9 @@ msgstr ""
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
msgstr ""
+msgid "WorkItem|Something went wrong while fetching milestones. Please try again."
+msgstr ""
+
msgid "WorkItem|Something went wrong while updating the %{workItemType}. Please try again."
msgstr ""
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index b057a27fa3e..a30d489e6ff 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -13,10 +13,6 @@ module QA
element :group_id_content
end
- view 'app/assets/javascripts/groups/constants.js' do
- element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern
- end
-
view 'app/views/shared/members/_access_request_links.html.haml' do
element :leave_group_link
end
diff --git a/scripts/glfm/verify-all-generated-files-are-up-to-date.rb b/scripts/glfm/verify-all-generated-files-are-up-to-date.rb
new file mode 100755
index 00000000000..7710997e3ed
--- /dev/null
+++ b/scripts/glfm/verify-all-generated-files-are-up-to-date.rb
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../lib/glfm/verify_all_generated_files_are_up_to_date'
+Glfm::VerifyAllGeneratedFilesAreUpToDate.new.process
diff --git a/scripts/lib/glfm/constants.rb b/scripts/lib/glfm/constants.rb
index 352bd867a61..d020d2fec5c 100644
--- a/scripts/lib/glfm/constants.rb
+++ b/scripts/lib/glfm/constants.rb
@@ -23,15 +23,16 @@ module Glfm
GLFM_EXAMPLE_METADATA_YML_PATH =
specification_input_glfm_path.join('glfm_example_metadata.yml')
GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH = specification_input_glfm_path.join('glfm_example_normalizations.yml')
- GLFM_SPEC_TXT_PATH = specification_path.join('output/spec.txt')
- GLFM_SPEC_HTML_PATH = specification_path.join('output/spec.html')
+ GLFM_SPEC_OUTPUT_PATH = specification_path.join('output')
+ GLFM_SPEC_TXT_PATH = GLFM_SPEC_OUTPUT_PATH.join('spec.txt')
+ GLFM_SPEC_HTML_PATH = GLFM_SPEC_OUTPUT_PATH.join('spec.html')
# Example Snapshot (ES) files
- es_fixtures_path = File.expand_path("../../../glfm_specification/example_snapshots", __dir__)
- ES_EXAMPLES_INDEX_YML_PATH = File.join(es_fixtures_path, 'examples_index.yml')
- ES_MARKDOWN_YML_PATH = File.join(es_fixtures_path, 'markdown.yml')
- ES_HTML_YML_PATH = File.join(es_fixtures_path, 'html.yml')
- ES_PROSEMIRROR_JSON_YML_PATH = File.join(es_fixtures_path, 'prosemirror_json.yml')
+ EXAMPLE_SNAPSHOTS_PATH = File.expand_path("../../../glfm_specification/example_snapshots", __dir__)
+ ES_EXAMPLES_INDEX_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'examples_index.yml')
+ ES_MARKDOWN_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'markdown.yml')
+ ES_HTML_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'html.yml')
+ ES_PROSEMIRROR_JSON_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'prosemirror_json.yml')
# Other constants used for processing files
GLFM_SPEC_TXT_HEADER = <<~MARKDOWN
diff --git a/scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb b/scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb
new file mode 100644
index 00000000000..0b824fc589d
--- /dev/null
+++ b/scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+require_relative 'constants'
+require_relative 'shared'
+
+# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#verify-all-generated-files-are-up-to-daterb-script
+# for details on the implementation and usage of this script. This developers guide
+# contains diagrams and documentation of this script,
+# including explanations and examples of all files it reads and writes.
+module Glfm
+ class VerifyAllGeneratedFilesAreUpToDate
+ include Constants
+ include Shared
+
+ def process
+ verify_cmd = "git status --porcelain #{GLFM_SPEC_OUTPUT_PATH} #{EXAMPLE_SNAPSHOTS_PATH}"
+ verify_cmd_output = run_external_cmd(verify_cmd)
+ unless verify_cmd_output.empty?
+ msg = "ERROR: Cannot run `#{__FILE__}` because `#{verify_cmd}` shows the following uncommitted changes:\n" \
+ "#{verify_cmd_output}"
+ raise(msg)
+ end
+
+ output('Verifying all generated files are up to date after running GLFM scripts...')
+
+ output("Running `yarn install --frozen-lockfile` to ensure `yarn check-dependencies` doesn't fail...")
+ run_external_cmd('yarn install --frozen-lockfile')
+
+ # noinspection RubyMismatchedArgumentType
+ update_specification_script = File.expand_path('../../glfm/update-specification.rb', __dir__)
+ # noinspection RubyMismatchedArgumentType
+ update_example_snapshots_script = File.expand_path('../../glfm/update-example-snapshots.rb', __dir__)
+
+ output("Running `#{update_specification_script}`...")
+ run_external_cmd(update_specification_script)
+
+ output("Running `#{update_example_snapshots_script}`...")
+ run_external_cmd(update_example_snapshots_script)
+
+ output("Running `#{verify_cmd}` to check that no modifications to generated files have occurred...")
+ verify_cmd_output = run_external_cmd(verify_cmd)
+
+ return if verify_cmd_output.empty?
+
+ raise "The following files were modified by running GLFM scripts. Please review, verify, and commit " \
+ "the changes:\n#{verify_cmd_output}"
+ end
+ end
+end
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 7add3a72337..e2a216bb462 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -53,7 +53,8 @@ RSpec.describe Profiles::PreferencesController do
first_day_of_week: '1',
preferred_language: 'jp',
tab_width: '5',
- render_whitespace_in_code: 'true'
+ render_whitespace_in_code: 'true',
+ use_legacy_web_ide: 'true'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!)
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 9e7666b920f..94c5f397670 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -177,10 +177,10 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Dormant users period' do
- expect(page).to have_field _('Period of inactivity (days)')
+ expect(page).to have_field _('Days of inactivity before deactivation')
page.within(find('[data-testid="account-limit"]')) do
- fill_in _('application_setting_deactivate_dormant_users_period'), with: '35'
+ fill_in _('application_setting_deactivate_dormant_users_period'), with: '90'
click_button 'Save changes'
end
@@ -188,7 +188,7 @@ RSpec.describe 'Admin updates settings' do
page.refresh
- expect(page).to have_field _('Period of inactivity (days)'), with: '35'
+ expect(page).to have_field _('Days of inactivity before deactivation'), with: '90'
end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 54081e21a46..349ffd09324 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -228,9 +228,9 @@ RSpec.describe MergeRequestsFinder do
end
describe ':label_name parameter' do
- let(:common_labels) { create_list(:label, 3) }
- let(:distinct_labels) { create_list(:label, 3) }
- let(:merge_requests) do
+ let_it_be(:common_labels) { create_list(:label, 3) }
+ let_it_be(:distinct_labels) { create_list(:label, 3) }
+ let_it_be(:merge_requests) do
common_attrs = {
source_project: project1, target_project: project1, author: user
}
@@ -561,7 +561,7 @@ RSpec.describe MergeRequestsFinder do
end
context 'filtering by created_at/updated_at' do
- let(:new_project) { create(:project, forked_from_project: project1) }
+ let_it_be(:new_project) { create(:project, forked_from_project: project1) }
let!(:new_merge_request) do
create(:merge_request,
@@ -584,7 +584,7 @@ RSpec.describe MergeRequestsFinder do
target_project: new_project)
end
- before do
+ before_all do
new_project.add_maintainer(user)
end
@@ -646,10 +646,10 @@ RSpec.describe MergeRequestsFinder do
end
context 'filtering by the merge request deployments' do
- let(:gstg) { create(:environment, project: project4, name: 'gstg') }
- let(:gprd) { create(:environment, project: project4, name: 'gprd') }
+ let_it_be(:gstg) { create(:environment, project: project4, name: 'gstg') }
+ let_it_be(:gprd) { create(:environment, project: project4, name: 'gprd') }
- let(:mr1) do
+ let_it_be(:mr1) do
create(
:merge_request,
:simple,
@@ -660,7 +660,7 @@ RSpec.describe MergeRequestsFinder do
)
end
- let(:mr2) do
+ let_it_be(:mr2) do
create(
:merge_request,
:simple,
@@ -671,7 +671,7 @@ RSpec.describe MergeRequestsFinder do
)
end
- let(:deploy1) do
+ let_it_be(:deploy1) do
create(
:deployment,
:success,
@@ -683,7 +683,7 @@ RSpec.describe MergeRequestsFinder do
)
end
- let(:deploy2) do
+ let_it_be(:deploy2) do
create(
:deployment,
:success,
@@ -695,7 +695,7 @@ RSpec.describe MergeRequestsFinder do
)
end
- before do
+ before_all do
deploy1.link_merge_requests(MergeRequest.where(id: mr1.id))
deploy2.link_merge_requests(MergeRequest.where(id: mr2.id))
end
@@ -833,13 +833,13 @@ RSpec.describe MergeRequestsFinder do
end
context 'when projects require different access levels for merge requests' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
- let(:public_project) { create(:project, :public) }
- let(:internal) { create(:project, :internal) }
- let(:private_project) { create(:project, :private) }
- let(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) }
- let(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) }
+ let_it_be(:public_project) { create(:project, :public) }
+ let_it_be(:internal) { create(:project, :internal) }
+ let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) }
+ let_it_be(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) }
let(:merge_requests) { described_class.new(user, {}).execute }
@@ -850,7 +850,7 @@ RSpec.describe MergeRequestsFinder do
let!(:mr_internal_private_repo_access) { create(:merge_request, source_project: internal_with_private_repo) }
context 'with admin user' do
- let(:user) { create(:user, :admin) }
+ let_it_be(:user) { create(:user, :admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it 'returns all merge requests' do
@@ -968,7 +968,7 @@ RSpec.describe MergeRequestsFinder do
let_it_be(:labels) { create_list(:label, 2, project: project) }
let_it_be(:merge_requests) { create_list(:merge_request, 4, :unique_branches, author: user, target_project: project, source_project: project, labels: labels) }
- before do
+ before_all do
project.add_developer(user)
end
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 56529726350..091ec17d58e 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -11,6 +11,7 @@ import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
import EmptyState from '~/groups/components/empty_state.vue';
+import GroupsComponent from '~/groups/components/groups.vue';
import axios from '~/lib/utils/axios_utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -388,24 +389,27 @@ describe('AppComponent', () => {
});
describe.each`
- action | groups | fromSearch | renderEmptyState | expected
- ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${true}
- ${''} | ${[]} | ${false} | ${true} | ${false}
- ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${true} | ${false}
- ${'subgroups_and_projects'} | ${[]} | ${true} | ${true} | ${false}
+ action | groups | fromSearch | shouldRenderEmptyState | searchEmpty
+ ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${false}
+ ${''} | ${[]} | ${false} | ${false} | ${false}
+ ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${false} | ${false}
+ ${'subgroups_and_projects'} | ${[]} | ${true} | ${false} | ${true}
`(
- 'when `action` is $action, `groups` is $groups, `fromSearch` is $fromSearch, and `renderEmptyState` is $renderEmptyState',
- ({ action, groups, fromSearch, renderEmptyState, expected }) => {
- it(`${expected ? 'renders' : 'does not render'} empty state`, async () => {
+ 'when `action` is $action, `groups` is $groups, and `fromSearch` is $fromSearch',
+ ({ action, groups, fromSearch, shouldRenderEmptyState, searchEmpty }) => {
+ it(`${shouldRenderEmptyState ? 'renders' : 'does not render'} empty state`, async () => {
createShallowComponent({
- propsData: { action, renderEmptyState },
+ propsData: { action, renderEmptyState: true },
});
+ await waitForPromises();
+
vm.updateGroups(groups, fromSearch);
await nextTick();
- expect(wrapper.findComponent(EmptyState).exists()).toBe(expected);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(shouldRenderEmptyState);
+ expect(wrapper.findComponent(GroupsComponent).props('searchEmpty')).toBe(searchEmpty);
});
},
);
@@ -445,18 +449,6 @@ describe('AppComponent', () => {
expect.any(Function),
);
});
-
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => {
- createShallowComponent();
- await nextTick();
- expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search');
- });
-
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => {
- createShallowComponent({ propsData: { hideProjects: true } });
- await nextTick();
- expect(vm.searchEmptyMessage).toBe('No groups matched your search');
- });
});
describe('beforeDestroy', () => {
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 866868eff36..0cbb6cc8309 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlEmptyState } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import GroupFolderComponent from '~/groups/components/group_folder.vue';
@@ -15,7 +16,6 @@ describe('GroupsComponent', () => {
const defaultPropsData = {
groups: mockGroups,
pageInfo: mockPageInfo,
- searchEmptyMessage: 'No matching results',
searchEmpty: false,
};
@@ -67,13 +67,16 @@ describe('GroupsComponent', () => {
expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true);
expect(findPaginationLinks().exists()).toBe(true);
- expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
it('should render empty search message when `searchEmpty` is `true`', () => {
createComponent({ propsData: { searchEmpty: true } });
- expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true);
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: GroupsComponent.i18n.emptyStateTitle,
+ description: GroupsComponent.i18n.emptyStateDescription,
+ });
});
});
});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index ec8559f1b56..067da25cb52 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -6,7 +6,7 @@ jest.mock('@gitlab/web-ide');
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
-const TEST_PROJECT = { path_with_namespace: 'group1/project1' };
+const TEST_PROJECT_PATH = 'group1/project1';
const TEST_BRANCH_NAME = '12345-foo-patch';
const TEST_GITLAB_URL = 'https://test-gitlab/';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
@@ -18,7 +18,7 @@ describe('ide/init_gitlab_web_ide', () => {
el.id = ROOT_ELEMENT_ID;
// why: We'll test that this class is removed later
el.classList.add('ide-loading');
- el.dataset.project = JSON.stringify(TEST_PROJECT);
+ el.dataset.projectPath = TEST_PROJECT_PATH;
el.dataset.cspNonce = TEST_NONCE;
el.dataset.branchName = TEST_BRANCH_NAME;
@@ -43,7 +43,7 @@ describe('ide/init_gitlab_web_ide', () => {
it('calls start with element', () => {
expect(start).toHaveBeenCalledWith(findRootElement(), {
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- projectPath: TEST_PROJECT.path_with_namespace,
+ projectPath: TEST_PROJECT_PATH,
ref: TEST_BRANCH_NAME,
gitlabUrl: TEST_GITLAB_URL,
nonce: TEST_NONCE,
diff --git a/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
new file mode 100644
index 00000000000..c1e1545944b
--- /dev/null
+++ b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
@@ -0,0 +1,81 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import UserSelect from '~/pages/import/fogbugz/new_user_map/components/user_select.vue';
+
+Vue.use(VueApollo);
+
+const USERS_RESPONSE = {
+ data: {
+ users: {
+ nodes: [
+ {
+ id: 'gid://gitlab/User/44',
+ avatarUrl: '/avatar1',
+ webUrl: '/reported_user_22',
+ name: 'Birgit Steuber',
+ username: 'reported_user_22',
+ __typename: 'UserCore',
+ },
+ {
+ id: 'gid://gitlab/User/43',
+ avatarUrl: '/avatar2',
+ webUrl: '/reported_user_21',
+ name: 'Luke Spinka',
+ username: 'reported_user_21',
+ __typename: 'UserCore',
+ },
+ ],
+ __typename: 'UserCoreConnection',
+ },
+ },
+};
+
+describe('fogbugz user select component', () => {
+ let wrapper;
+ const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(USERS_RESPONSE);
+
+ const createComponent = (propsData = { name: 'demo' }) => {
+ const fakeApollo = createMockApollo([[searchUsersQuery, searchQueryHandlerSuccess]]);
+
+ wrapper = shallowMount(UserSelect, {
+ apolloProvider: fakeApollo,
+ propsData,
+ });
+ };
+
+ it('renders hidden input with name from props', () => {
+ const name = 'test';
+ createComponent({ name });
+ expect(wrapper.find('input').attributes('name')).toBe(name);
+ });
+
+ it('syncs input value with value emitted from listbox', async () => {
+ createComponent();
+
+ const id = 8;
+
+ wrapper.findComponent(GlListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
+ await nextTick();
+
+ expect(wrapper.get('input').attributes('value')).toBe(id.toString());
+ });
+
+ it('filters users when search is performed in listbox', async () => {
+ createComponent();
+ jest.runOnlyPendingTimers();
+
+ wrapper.findComponent(GlListbox).vm.$emit('search', 'test');
+ await nextTick();
+ jest.runOnlyPendingTimers();
+
+ expect(searchQueryHandlerSuccess).toHaveBeenCalledWith({
+ first: expect.anything(),
+ search: 'test',
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 5788968100a..6622749da92 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -1144,7 +1144,7 @@ describe('MrWidgetOptions', () => {
${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
${'WidgetIssues'} | ${'i_testing_issues_widget_total'}
- ${'WidgetTestReport'} | ${'i_testing_summary_widget_total'}
+ ${'WidgetTestSummary'} | ${'i_testing_summary_widget_total'}
`(
"sends non-standard events for the '$widgetName' widget",
async ({ widgetName, nonStandardEvent }) => {
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
index 40de3cc0d33..16e0a3f549e 100644
--- a/spec/frontend/webhooks/components/form_url_app_spec.js
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -1,15 +1,18 @@
import { nextTick } from 'vue';
-import { GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
+import { GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import FormUrlApp from '~/webhooks/components/form_url_app.vue';
+import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FormUrlApp', () => {
let wrapper;
- const createComponent = () => {
- wrapper = shallowMountExtended(FormUrlApp);
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMountExtended(FormUrlApp, {
+ propsData: { ...props },
+ });
};
afterEach(() => {
@@ -20,13 +23,17 @@ describe('FormUrlApp', () => {
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findUrlMaskDisable = () => findAllRadioButtons().at(0);
const findUrlMaskEnable = () => findAllRadioButtons().at(1);
+ const findAllUrlMaskItems = () => wrapper.findAllComponents(FormUrlMaskItem);
+ const findAddItem = () => wrapper.findComponent(GlLink);
+ const findFormUrl = () => wrapper.findByTestId('form-url');
+ const findFormUrlPreview = () => wrapper.findByTestId('form-url-preview');
const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section');
describe('template', () => {
it('renders radio buttons for URL masking', () => {
createComponent();
- expect(findAllRadioButtons().length).toBe(2);
+ expect(findAllRadioButtons()).toHaveLength(2);
expect(findUrlMaskDisable().text()).toBe(FormUrlApp.i18n.radioFullUrlText);
expect(findUrlMaskEnable().text()).toBe(FormUrlApp.i18n.radioMaskUrlText);
});
@@ -48,6 +55,88 @@ describe('FormUrlApp', () => {
it('renders mask section', () => {
expect(findUrlMaskSection().exists()).toBe(true);
});
+
+ it('renders an empty mask item by default', () => {
+ expect(findAllUrlMaskItems()).toHaveLength(1);
+
+ const firstItem = findAllUrlMaskItems().at(0);
+ expect(firstItem.props('itemKey')).toBeNull();
+ expect(firstItem.props('itemValue')).toBeNull();
+ });
+ });
+
+ describe('with mask items', () => {
+ const mockItem1 = { key: 'key1', value: 'value1' };
+ const mockItem2 = { key: 'key2', value: 'value2' };
+
+ beforeEach(() => {
+ createComponent({
+ props: { initialUrlVariables: [mockItem1, mockItem2] },
+ });
+ });
+
+ it('renders masked URL preview', async () => {
+ const mockUrl = 'https://test.host/value1?secret=value2';
+
+ findFormUrl().vm.$emit('input', mockUrl);
+ await nextTick();
+
+ expect(findFormUrlPreview().attributes('value')).toBe(
+ 'https://test.host/{key1}?secret={key2}',
+ );
+ });
+
+ it('renders mask items correctly', () => {
+ expect(findAllUrlMaskItems()).toHaveLength(2);
+
+ const firstItem = findAllUrlMaskItems().at(0);
+ expect(firstItem.props('itemKey')).toBe(mockItem1.key);
+ expect(firstItem.props('itemValue')).toBe(mockItem1.value);
+
+ const secondItem = findAllUrlMaskItems().at(1);
+ expect(secondItem.props('itemKey')).toBe(mockItem2.key);
+ expect(secondItem.props('itemValue')).toBe(mockItem2.value);
+ });
+
+ describe('on mask item input', () => {
+ const mockInput = { index: 0, key: 'display', value: 'secret' };
+
+ it('updates mask item', async () => {
+ const firstItem = findAllUrlMaskItems().at(0);
+ firstItem.vm.$emit('input', mockInput);
+ await nextTick();
+
+ expect(firstItem.props('itemKey')).toBe(mockInput.key);
+ expect(firstItem.props('itemValue')).toBe(mockInput.value);
+ });
+ });
+
+ describe('when add item is clicked', () => {
+ it('adds mask item', async () => {
+ findAddItem().vm.$emit('click');
+ await nextTick();
+
+ expect(findAllUrlMaskItems()).toHaveLength(3);
+
+ const lastItem = findAllUrlMaskItems().at(-1);
+ expect(lastItem.props('itemKey')).toBeNull();
+ expect(lastItem.props('itemValue')).toBeNull();
+ });
+ });
+
+ describe('when remove item is clicked', () => {
+ it('removes the correct mask item', async () => {
+ const firstItem = findAllUrlMaskItems().at(0);
+ firstItem.vm.$emit('remove');
+ await nextTick();
+
+ expect(findAllUrlMaskItems()).toHaveLength(1);
+
+ const newFirstItem = findAllUrlMaskItems().at(0);
+ expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
+ expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
+ });
+ });
});
});
});
diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
index 76681e6ab26..ab028ef2997 100644
--- a/spec/frontend/webhooks/components/form_url_mask_item_spec.js
+++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlButton, GlFormInput } from '@gitlab/ui';
import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
@@ -10,10 +11,13 @@ describe('FormUrlMaskItem', () => {
const defaultProps = {
index: 0,
};
+ const mockKey = 'key';
+ const mockValue = 'value';
+ const mockInput = 'input';
- const createComponent = () => {
+ const createComponent = ({ props } = {}) => {
wrapper = shallowMountExtended(FormUrlMaskItem, {
- propsData: { ...defaultProps },
+ propsData: { ...defaultProps, ...props },
});
};
@@ -42,10 +46,55 @@ describe('FormUrlMaskItem', () => {
);
});
+ describe('on key input', () => {
+ beforeEach(async () => {
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
+
+ findMaskItemKey().findComponent(GlFormInput).vm.$emit('input', mockInput);
+ await nextTick();
+ });
+
+ it('emits input event', () => {
+ expect(wrapper.emitted('input')).toEqual([
+ [{ index: defaultProps.index, key: mockInput, value: mockValue }],
+ ]);
+ });
+ });
+
+ describe('on value input', () => {
+ beforeEach(async () => {
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
+
+ findMaskItemValue().findComponent(GlFormInput).vm.$emit('input', mockInput);
+ await nextTick();
+ });
+
+ it('emits input event', () => {
+ expect(wrapper.emitted('input')).toEqual([
+ [{ index: defaultProps.index, key: mockKey, value: mockInput }],
+ ]);
+ });
+ });
+
it('renders remove button', () => {
createComponent();
expect(findRemoveButton().props('icon')).toBe('remove');
});
+
+ describe('when remove button is clicked', () => {
+ const mockIndex = 5;
+
+ beforeEach(async () => {
+ createComponent({ props: { index: mockIndex } });
+
+ findRemoveButton().vm.$emit('click');
+ await nextTick();
+ });
+
+ it('emits remove event', () => {
+ expect(wrapper.emitted('remove')).toEqual([[mockIndex]]);
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 3580842fc1a..aae61b11196 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -20,6 +20,7 @@ import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -28,6 +29,7 @@ import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subs
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
+import { temporaryConfig } from '~/graphql_shared/issuable_client';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockParent,
@@ -67,6 +69,7 @@ describe('WorkItemDetail component', () => {
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
+ const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
const findParentButton = () => findParent().findComponent(GlButton);
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
@@ -82,6 +85,8 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
+ includeWidgets = false,
+ workItemsMvc2Enabled = false,
} = {}) => {
const handlers = [
[workItemQuery, handler],
@@ -92,7 +97,13 @@ describe('WorkItemDetail component', () => {
];
wrapper = shallowMount(WorkItemDetail, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo(
+ handlers,
+ {},
+ {
+ typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
+ },
+ ),
propsData: { isModal, workItemId },
data() {
return {
@@ -101,6 +112,9 @@ describe('WorkItemDetail component', () => {
};
},
provide: {
+ glFeatures: {
+ workItemsMvc2: workItemsMvc2Enabled,
+ },
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
projectNamespace: 'namespace',
@@ -527,6 +541,19 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('milestone widget', () => {
+ it.each`
+ description | includeWidgets | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', async ({ includeWidgets, exists }) => {
+ createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(findWorkItemMilestone().exists()).toBe(exists);
+ });
+ });
+
describe('work item information', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
new file mode 100644
index 00000000000..08cdf62ae52
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -0,0 +1,247 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlSkeletonLoader,
+ GlFormGroup,
+ GlDropdownText,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+import { resolvers, temporaryConfig } from '~/graphql_shared/issuable_client';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import {
+ projectMilestonesResponse,
+ projectMilestonesResponseWithNoMilestones,
+ mockMilestoneWidgetResponse,
+ workItemResponseFactory,
+ updateWorkItemMutationErrorResponse,
+} from 'jest/work_items/mock_data';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+
+describe('WorkItemMilestone component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const workItemType = 'Task';
+ const fullPath = 'full-path';
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findNoMilestoneDropdownItem = () => wrapper.findByTestId('no-milestone');
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findFirstDropdownItem = () => findDropdownItems().at(0);
+ const findDropdownTexts = () => wrapper.findAllComponents(GlDropdownText);
+ const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
+ const findDisabledTextSpan = () => wrapper.findByTestId('disabled-text');
+ const findDropdownTextAtIndex = (index) => findDropdownTexts().at(index);
+ const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+
+ const networkResolvedValue = new Error();
+
+ const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
+ const successSearchWithNoMatchingMilestones = jest
+ .fn()
+ .mockResolvedValue(projectMilestonesResponseWithNoMilestones);
+
+ const showDropdown = () => {
+ findDropdown().vm.$emit('shown');
+ };
+
+ const hideDropdown = () => {
+ findDropdown().vm.$emit('hide');
+ };
+
+ const createComponent = ({
+ canUpdate = true,
+ milestone = mockMilestoneWidgetResponse,
+ searchQueryHandler = successSearchQueryHandler,
+ } = {}) => {
+ const apolloProvider = createMockApollo(
+ [[projectMilestonesQuery, searchQueryHandler]],
+ resolvers,
+ {
+ typePolicies: temporaryConfig.cacheConfig.typePolicies,
+ },
+ );
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: workItemId,
+ },
+ data: workItemQueryResponse.data,
+ });
+
+ wrapper = shallowMountExtended(WorkItemMilestone, {
+ apolloProvider,
+ propsData: {
+ canUpdate,
+ workItemMilestone: milestone,
+ workItemId,
+ workItemType,
+ fullPath,
+ },
+ stubs: {
+ GlDropdown,
+ GlSearchBoxByType,
+ },
+ });
+ };
+
+ it('has "Milestone" label', () => {
+ createComponent();
+
+ expect(findInputGroup().exists()).toBe(true);
+ expect(findInputGroup().attributes('label')).toBe(WorkItemMilestone.i18n.MILESTONE);
+ });
+
+ describe('Default text with canUpdate false and milestone value', () => {
+ describe.each`
+ description | milestone | value
+ ${'when no milestone'} | ${null} | ${WorkItemMilestone.i18n.NONE}
+ ${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title}
+ `('$description', ({ milestone, value }) => {
+ it(`has a value of "${value}"`, () => {
+ createComponent({ canUpdate: false, milestone });
+
+ expect(findDisabledTextSpan().text()).toBe(value);
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Default text value when canUpdate true and no milestone set', () => {
+ it(`has a value of "Add to milestone"`, () => {
+ createComponent({ canUpdate: true, milestone: null });
+
+ expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ });
+ });
+
+ describe('Dropdown search', () => {
+ it('has the search box', () => {
+ createComponent();
+
+ expect(findSearchBox().exists()).toBe(true);
+ });
+
+ it('shows no matching results when no items', () => {
+ createComponent({
+ searchQueryHandler: successSearchWithNoMatchingMilestones,
+ });
+
+ expect(findDropdownTextAtIndex(0).text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
+ expect(findDropdownItems()).toHaveLength(1);
+ expect(findDropdownTexts()).toHaveLength(1);
+ });
+ });
+
+ describe('Dropdown options', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true });
+ });
+
+ it('shows the skeleton loader when the items are being fetched on click', async () => {
+ showDropdown();
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('shows the milestones in dropdown when the items have finished fetching', async () => {
+ showDropdown();
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findNoMilestoneDropdownItem().exists()).toBe(true);
+ expect(findDropdownItems()).toHaveLength(
+ projectMilestonesResponse.data.workspace.attributes.nodes.length + 1,
+ );
+ });
+
+ it('changes the milestone to null when clicked on no milestone', async () => {
+ showDropdown();
+ findFirstDropdownItem().vm.$emit('click');
+
+ hideDropdown();
+ await nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ });
+
+ it('changes the milestone to the selected milestone', async () => {
+ const milestoneIndex = 1;
+ /** the index is -1 since no matching results is also a dropdown item */
+ const milestoneAtIndex =
+ projectMilestonesResponse.data.workspace.attributes.nodes[milestoneIndex - 1];
+ showDropdown();
+
+ await waitForPromises();
+ findDropdownItemAtIndex(milestoneIndex).vm.$emit('click');
+
+ hideDropdown();
+ await waitForPromises();
+
+ expect(findDropdown().props('text')).toBe(milestoneAtIndex.title);
+ });
+ });
+
+ describe('Error handlers', () => {
+ it.each`
+ errorType | expectedErrorMessage | mockValue | resolveFunction
+ ${'graphql error'} | ${'Something went wrong while updating the task. Please try again.'} | ${updateWorkItemMutationErrorResponse} | ${'mockResolvedValue'}
+ ${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${networkResolvedValue} | ${'mockRejectedValue'}
+ `(
+ 'emits an error when there is a $errorType',
+ async ({ mockValue, expectedErrorMessage, resolveFunction }) => {
+ createComponent({
+ mutationHandler: jest.fn()[resolveFunction](mockValue),
+ canUpdate: true,
+ });
+
+ showDropdown();
+ findFirstDropdownItem().vm.$emit('click');
+ hideDropdown();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[expectedErrorMessage]]);
+ },
+ );
+ });
+
+ describe('Tracking event', () => {
+ it('tracks updating the milestone', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ createComponent({ canUpdate: true });
+
+ showDropdown();
+ findFirstDropdownItem().vm.$emit('click');
+ hideDropdown();
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_milestone', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: 'type_Task',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index a0ed4ed1425..ed90b11222a 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -186,6 +186,7 @@ export const workItemResponseFactory = ({
datesWidgetPresent = true,
labelsWidgetPresent = true,
weightWidgetPresent = true,
+ milestoneWidgetPresent = true,
iterationWidgetPresent = true,
confidential = false,
canInviteMembers = false,
@@ -279,6 +280,16 @@ export const workItemResponseFactory = ({
},
}
: { type: 'MOCK TYPE' },
+ milestoneWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetMilestone',
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ type: 'MILESTONE',
+ }
+ : { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
@@ -1059,3 +1070,55 @@ export const groupIterationsResponseWithNoIterations = {
},
},
};
+
+export const mockMilestoneWidgetResponse = {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+};
+
+export const projectMilestonesResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ attributes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Milestone/5',
+ title: 'v4.0',
+ webUrl: '/gitlab-org/gitlab-test/-/milestones/5',
+ dueDate: null,
+ expired: false,
+ __typename: 'Milestone',
+ state: 'active',
+ },
+ {
+ id: 'gid://gitlab/Milestone/4',
+ title: 'v3.0',
+ webUrl: '/gitlab-org/gitlab-test/-/milestones/4',
+ dueDate: null,
+ expired: false,
+ __typename: 'Milestone',
+ state: 'active',
+ },
+ ],
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const projectMilestonesResponseWithNoMilestones = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ attributes: {
+ nodes: [],
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index bac73db5dd4..8f438a3ddc8 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -8,6 +8,13 @@ RSpec.describe HooksHelper do
let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
let(:system_hook) { create(:system_hook) }
+ describe '#webhook_form_data' do
+ subject { helper.webhook_form_data(project_hook) }
+
+ it { expect(subject[:url]).to eq(project_hook.url) }
+ it { expect(subject[:url_variables]).to be_nil }
+ end
+
describe '#link_to_test_hook' do
let(:trigger) { 'push_events' }
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index dc0a234f981..e750379f62d 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -5,75 +5,113 @@ require 'spec_helper'
RSpec.describe IdeHelper do
describe '#ide_data' do
let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.creator }
before do
- allow(helper).to receive(:current_user).and_return(project.creator)
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:content_security_policy_nonce).and_return('test-csp-nonce')
end
- context 'when instance vars are not set' do
- it 'returns instance data in the hash as nil' do
- expect(helper.ide_data)
- .to include(
- 'branch-name' => nil,
- 'file-path' => nil,
- 'merge-request' => nil,
- 'fork-info' => nil,
- 'project' => nil,
- 'preview-markdown-path' => nil
- )
- end
- end
-
- context 'when instance vars are set' do
- it 'returns instance data in the hash' do
- fork_info = { ide_path: '/test/ide/path' }
+ context 'with vscode_web_ide=true and instance vars set' do
+ before do
+ stub_feature_flags(vscode_web_ide: true)
self.instance_variable_set(:@branch, 'master')
- self.instance_variable_set(:@path, 'foo/bar')
- self.instance_variable_set(:@merge_request, '1')
- self.instance_variable_set(:@fork_info, fork_info)
self.instance_variable_set(:@project, project)
+ end
- serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
-
+ it 'returns hash' do
expect(helper.ide_data)
- .to include(
+ .to eq(
+ 'can-use-new-web-ide' => 'true',
+ 'use-new-web-ide' => 'true',
+ 'user-preferences-path' => profile_preferences_path,
'branch-name' => 'master',
- 'file-path' => 'foo/bar',
- 'merge-request' => '1',
- 'fork-info' => fork_info.to_json,
- 'project' => serialized_project,
- 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
+ 'project-path' => project.path_with_namespace,
+ 'csp-nonce' => 'test-csp-nonce'
)
end
+
+ it 'does not use new web ide if user.use_legacy_web_ide' do
+ allow(user).to receive(:use_legacy_web_ide).and_return(true)
+
+ expect(helper.ide_data).to include('use-new-web-ide' => 'false')
+ end
end
- context 'environments guidance experiment', :experiment do
+ context 'with vscode_web_ide=false' do
before do
- stub_experiments(in_product_guidance_environments_webide: :candidate)
- self.instance_variable_set(:@project, project)
+ stub_feature_flags(vscode_web_ide: false)
end
- context 'when project has no enviornments' do
- it 'enables environment guidance' do
- expect(helper.ide_data).to include('enable-environments-guidance' => 'true')
+ context 'when instance vars are not set' do
+ it 'returns instance data in the hash as nil' do
+ expect(helper.ide_data)
+ .to include(
+ 'can-use-new-web-ide' => 'false',
+ 'use-new-web-ide' => 'false',
+ 'user-preferences-path' => profile_preferences_path,
+ 'branch-name' => nil,
+ 'file-path' => nil,
+ 'merge-request' => nil,
+ 'fork-info' => nil,
+ 'project' => nil,
+ 'preview-markdown-path' => nil
+ )
end
+ end
- context 'and the callout has been dismissed' do
- it 'disables environment guidance' do
- callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
- callout.update!(dismissed_at: Time.now - 1.week)
- allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
- expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
- end
+ context 'when instance vars are set' do
+ it 'returns instance data in the hash' do
+ fork_info = { ide_path: '/test/ide/path' }
+
+ self.instance_variable_set(:@branch, 'master')
+ self.instance_variable_set(:@path, 'foo/bar')
+ self.instance_variable_set(:@merge_request, '1')
+ self.instance_variable_set(:@fork_info, fork_info)
+ self.instance_variable_set(:@project, project)
+
+ serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
+
+ expect(helper.ide_data)
+ .to include(
+ 'branch-name' => 'master',
+ 'file-path' => 'foo/bar',
+ 'merge-request' => '1',
+ 'fork-info' => fork_info.to_json,
+ 'project' => serialized_project,
+ 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
+ )
end
end
- context 'when the project has environments' do
- it 'disables environment guidance' do
- create(:environment, project: project)
+ context 'environments guidance experiment', :experiment do
+ before do
+ stub_experiments(in_product_guidance_environments_webide: :candidate)
+ self.instance_variable_set(:@project, project)
+ end
+
+ context 'when project has no enviornments' do
+ it 'enables environment guidance' do
+ expect(helper.ide_data).to include('enable-environments-guidance' => 'true')
+ end
+
+ context 'and the callout has been dismissed' do
+ it 'disables environment guidance' do
+ callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
+ callout.update!(dismissed_at: Time.now - 1.week)
+ allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
+ expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
+ end
+ end
+ end
- expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
+ context 'when the project has environments' do
+ it 'disables environment guidance' do
+ create(:environment, project: project)
+
+ expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 8ac03301322..7d4a1eef70b 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -571,5 +571,42 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
end
end
end
+
+ context 'with the file_variable_is_referenced_in_another_variable logging' do
+ let(:collection) do
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'VAR1', value: 'test-1')
+ .append(key: 'VAR2', value: '$VAR1')
+ .append(key: 'VAR3', value: '$VAR1', raw: true)
+ .append(key: 'FILEVAR4', value: 'file-test-4', file: true)
+ .append(key: 'VAR5', value: '$FILEVAR4')
+ .append(key: 'VAR6', value: '$FILEVAR4', raw: true)
+ end
+
+ subject(:sort_and_expand_all) { collection.sort_and_expand_all(project: project) }
+
+ context 'when a project is not passed' do
+ let(:project) {}
+
+ it 'does not log anything' do
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+
+ sort_and_expand_all
+ end
+ end
+
+ context 'when a project is passed' do
+ let(:project) { create(:project) }
+
+ it 'logs file_variable_is_referenced_in_another_variable once for VAR5' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(
+ event: 'file_variable_is_referenced_in_another_variable',
+ project_id: project.id
+ ).once
+
+ sort_and_expand_all
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
index af7d751a404..0e804b4feac 100644
--- a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
@@ -153,6 +153,21 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
expect(parent_model.pluck(:id)).to match_array([1, 2, 3])
end
+ context 'when the existing table is owned by a different user' do
+ before do
+ connection.execute(<<~SQL)
+ CREATE USER other_user SUPERUSER;
+ ALTER TABLE #{table_name} OWNER TO other_user;
+ SQL
+ end
+
+ let(:current_user) { model.connection.select_value('select current_user') }
+
+ it 'partitions without error' do
+ expect { partition }.not_to raise_error
+ end
+ end
+
context 'when an error occurs during the conversion' do
def fail_first_time
# We can't directly use a boolean here, as we need something that will be passed by-reference to the proc
diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb
index 0cfe3a69a09..5fc3a70169a 100644
--- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb
@@ -78,16 +78,30 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
context 'when upload is in object storage' do
before do
stub_uploads_object_storage(FileUploader)
- allow(manager).to receive(:download_or_copy_upload).and_raise(Errno::ENAMETOOLONG)
end
- it 'ignores problematic upload and logs exception' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Errno::ENAMETOOLONG), project_id: project.id)
+ shared_examples 'export with invalid upload' do
+ it 'ignores problematic upload and logs exception' do
+ allow(manager).to receive(:download_or_copy_upload).and_raise(exception)
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(exception), project_id: project.id)
- manager.save # rubocop:disable Rails/SaveBang
+ manager.save # rubocop:disable Rails/SaveBang
- expect(shared.errors).to be_empty
- expect(File).not_to exist(exported_file_path)
+ expect(shared.errors).to be_empty
+ expect(File).not_to exist(exported_file_path)
+ end
+ end
+
+ context 'when filename is too long' do
+ let(:exception) { Errno::ENAMETOOLONG }
+
+ include_examples 'export with invalid upload'
+ end
+
+ context 'when network exception occurs' do
+ let(:exception) { Net::OpenTimeout }
+
+ include_examples 'export with invalid upload'
end
end
end
diff --git a/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb b/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb
new file mode 100644
index 00000000000..eac71e428be
--- /dev/null
+++ b/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateInvalidDormantUserSetting, :migration do
+ let(:settings) { table(:application_settings) }
+
+ context 'with no rows in the application_settings table' do
+ it 'does not insert a row' do
+ expect { migrate! }.to not_change { settings.count }
+ end
+ end
+
+ context 'with a row in the application_settings table' do
+ before do
+ settings.create!(deactivate_dormant_users_period: days)
+ end
+
+ context 'with deactivate_dormant_users_period set to a value greater than or equal to 90' do
+ let(:days) { 90 }
+
+ it 'does not update the row' do
+ expect { migrate! }
+ .to not_change { settings.count }
+ .and not_change { settings.first.deactivate_dormant_users_period }
+ end
+ end
+
+ context 'with deactivate_dormant_users_period set to a value less than or equal to 90' do
+ let(:days) { 1 }
+
+ it 'updates the existing row' do
+ expect { migrate! }
+ .to not_change { settings.count }
+ .and change { settings.first.deactivate_dormant_users_period }
+ end
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 687ffbe87bf..77bb6b502b5 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -203,6 +203,17 @@ RSpec.describe ApplicationSetting do
it { is_expected.to allow_value([]).for(:valid_runner_registrars) }
it { is_expected.to allow_value(%w(project group)).for(:valid_runner_registrars) }
+ context 'when deactivate_dormant_users is enabled' do
+ before do
+ stub_application_setting(deactivate_dormant_users: true)
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:deactivate_dormant_users_period) }
+ it { is_expected.to allow_value(90).for(:deactivate_dormant_users_period) }
+ it { is_expected.to allow_value(365).for(:deactivate_dormant_users_period) }
+ it { is_expected.not_to allow_value(89).for(:deactivate_dormant_users_period) }
+ end
+
context 'help_page_documentation_base_url validations' do
it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) }
it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 751a303739c..b2316949497 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -5337,19 +5337,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#authorized_cluster_agents' do
+ describe '#cluster_agent_authorizations' do
let(:pipeline) { create(:ci_empty_pipeline, :created) }
- let(:agent) { instance_double(Clusters::Agent) }
- let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization, agent: agent) }
+ let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization) }
let(:finder) { double(execute: [authorization]) }
- it 'retrieves agent records from the finder and caches the result' do
+ it 'retrieves authorization records from the finder and caches the result' do
expect(Clusters::AgentAuthorizationsFinder).to receive(:new).once
.with(pipeline.project)
.and_return(finder)
- expect(pipeline.authorized_cluster_agents).to contain_exactly(agent)
- expect(pipeline.authorized_cluster_agents).to contain_exactly(agent) # cached
+ expect(pipeline.cluster_agent_authorizations).to contain_exactly(authorization)
+ expect(pipeline.cluster_agent_authorizations).to contain_exactly(authorization) # cached
end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index f0af229ff2c..5f2b5971508 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Ci::Variable do
context 'loose foreign key on ci_variables.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
- let!(:parent) { create(:project) }
+ let!(:parent) { create(:project, namespace: create(:group)) }
let!(:model) { create(:ci_variable, project: parent) }
end
end
diff --git a/spec/models/clusters/agents/implicit_authorization_spec.rb b/spec/models/clusters/agents/implicit_authorization_spec.rb
index 2d6c3ddb426..1f4c5b1ac9e 100644
--- a/spec/models/clusters/agents/implicit_authorization_spec.rb
+++ b/spec/models/clusters/agents/implicit_authorization_spec.rb
@@ -10,5 +10,5 @@ RSpec.describe Clusters::Agents::ImplicitAuthorization do
it { expect(subject.agent).to eq(agent) }
it { expect(subject.agent_id).to eq(agent.id) }
it { expect(subject.config_project).to eq(agent.project) }
- it { expect(subject.config).to be_nil }
+ it { expect(subject.config).to eq({}) }
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 029667a60b0..d76334d7c9e 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -45,6 +45,13 @@ RSpec.describe UserPreference do
it { is_expected.not_to allow_value(color).for(:diffs_addition_color) }
end
end
+
+ describe 'use_legacy_web_ide' do
+ it { is_expected.to allow_value(true).for(:use_legacy_web_ide) }
+ it { is_expected.to allow_value(false).for(:use_legacy_web_ide) }
+ it { is_expected.not_to allow_value(nil).for(:use_legacy_web_ide) }
+ it { is_expected.not_to allow_value("").for(:use_legacy_web_ide) }
+ end
end
describe 'notes filters global keys' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 73ac4e7d3f2..8ebf3d70165 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -78,6 +78,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:diffs_addition_color).to(:user_preference) }
it { is_expected.to delegate_method(:diffs_addition_color=).to(:user_preference).with_arguments(:args) }
+ it { is_expected.to delegate_method(:use_legacy_web_ide).to(:user_preference) }
+ it { is_expected.to delegate_method(:use_legacy_web_ide=).to(:user_preference).with_arguments(:args) }
+
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index 33d1bc104ce..396fe7843ba 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -350,6 +350,15 @@ RSpec.describe Ci::BuildRunnerPresenter do
)
end
+ it 'logs file_variable_is_referenced_in_another_variable' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(
+ event: 'file_variable_is_referenced_in_another_variable',
+ project_id: project.id
+ ).once
+
+ runner_variables
+ end
+
context 'when the FF ci_stop_expanding_file_vars_for_runners is disabled' do
before do
stub_feature_flags(ci_stop_expanding_file_vars_for_runners: false)
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index 191fc2a6704..8d61399c824 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IdeController do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:reporter) { create(:user) }
let_it_be(:project) do
@@ -237,21 +239,29 @@ RSpec.describe IdeController do
end
# This indirectly tests that `minimal: true` was passed to the fullscreen layout
- it 'does not render top nav' do
- subject
-
- expect(response).not_to render_template(top_nav_partial)
- end
-
- context 'without vscode_web_ide feature flag' do
- before do
- stub_feature_flags(vscode_web_ide: false)
+ describe 'layout' do
+ where(:ff_state, :use_legacy_web_ide, :expect_top_nav) do
+ false | false | true
+ false | true | true
+ true | true | true
+ true | false | false
end
- it 'renders top nav' do
- subject
+ with_them do
+ before do
+ stub_feature_flags(vscode_web_ide: ff_state)
+ allow(user).to receive(:use_legacy_web_ide).and_return(use_legacy_web_ide)
+
+ subject
+ end
- expect(response).to render_template(top_nav_partial)
+ it 'handles rendering top nav' do
+ if expect_top_nav
+ expect(response).to render_template(top_nav_partial)
+ else
+ expect(response).not_to render_template(top_nav_partial)
+ end
+ end
end
end
end
diff --git a/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb b/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb
new file mode 100644
index 00000000000..fca037c9ff3
--- /dev/null
+++ b/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+require 'fast_spec_helper'
+require_relative '../../../../scripts/lib/glfm/verify_all_generated_files_are_up_to_date'
+
+# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#verify-all-generated-files-are-up-to-daterb-script
+# for details on the implementation and usage of the `verify_all_generated_files_are_up_to_date.rb` script being tested.
+# This developers guide contains diagrams and documentation of the script,
+# including explanations and examples of all files it reads and writes.
+RSpec.describe Glfm::VerifyAllGeneratedFilesAreUpToDate, '#process' do
+ subject { described_class.new }
+
+ let(:output_path) { described_class::GLFM_SPEC_OUTPUT_PATH }
+ let(:snapshots_path) { described_class::EXAMPLE_SNAPSHOTS_PATH }
+ let(:verify_cmd) { "git status --porcelain #{output_path} #{snapshots_path}" }
+
+ before do
+ # Prevent console output when running tests
+ allow(subject).to receive(:output)
+ end
+
+ context 'when repo is dirty' do
+ before do
+ # Simulate a dirty repo
+ allow(subject).to receive(:run_external_cmd).with(verify_cmd).and_return(" M #{output_path}")
+ end
+
+ it 'raises an error', :unlimited_max_formatted_output_length do
+ expect { subject.process }.to raise_error(/Cannot run.*uncommitted changes.*#{output_path}/m)
+ end
+ end
+
+ context 'when repo is clean' do
+ before do
+ # Mock out all yarn install and script execution
+ allow(subject).to receive(:run_external_cmd).with('yarn install --frozen-lockfile')
+ allow(subject).to receive(:run_external_cmd).with(/update-specification.rb/)
+ allow(subject).to receive(:run_external_cmd).with(/update-example-snapshots.rb/)
+ end
+
+ context 'when all generated files are up to date' do
+ before do
+ # Simulate a clean repo, then simulate no changes to generated files
+ allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', '')
+ end
+
+ it 'does not raise an error', :unlimited_max_formatted_output_length do
+ expect { subject.process }.not_to raise_error
+ end
+ end
+
+ context 'when generated file(s) are not up to date' do
+ before do
+ # Simulate a clean repo, then simulate changes to generated files
+ allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', "M #{snapshots_path}")
+ end
+
+ it 'raises an error', :unlimited_max_formatted_output_length do
+ expect { subject.process }.to raise_error(/following files were modified.*#{snapshots_path}/m)
+ end
+ end
+ end
+end
diff --git a/spec/services/bulk_imports/uploads_export_service_spec.rb b/spec/services/bulk_imports/uploads_export_service_spec.rb
index 39bcacfdc5e..ad6e005485c 100644
--- a/spec/services/bulk_imports/uploads_export_service_spec.rb
+++ b/spec/services/bulk_imports/uploads_export_service_spec.rb
@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe BulkImports::UploadsExportService do
- let_it_be(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
- let_it_be(:upload) { create(:upload, :with_file, :issuable_upload, uploader: FileUploader, model: project) }
let_it_be(:export_path) { Dir.mktmpdir }
+ let_it_be(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
+
+ let!(:upload) { create(:upload, :with_file, :issuable_upload, uploader: FileUploader, model: project) }
+ let(:exported_filepath) { File.join(export_path, upload.secret, upload.retrieve_uploader.filename) }
subject(:service) { described_class.new(project, export_path) }
@@ -15,10 +17,60 @@ RSpec.describe BulkImports::UploadsExportService do
describe '#execute' do
it 'exports project uploads and avatar' do
- subject.execute
+ service.execute
+
+ expect(File).to exist(File.join(export_path, 'avatar', 'rails_sample.png'))
+ expect(File).to exist(exported_filepath)
+ end
+
+ context 'when upload has underlying file missing' do
+ context 'with an upload missing its file' do
+ it 'does not cause errors' do
+ File.delete(upload.absolute_path)
+
+ expect { service.execute }.not_to raise_error
+
+ expect(File).not_to exist(exported_filepath)
+ end
+ end
+
+ context 'when upload is in object storage' do
+ before do
+ stub_uploads_object_storage(FileUploader)
+ end
+
+ shared_examples 'export with invalid upload' do
+ it 'ignores problematic upload and logs exception' do
+ allow(service).to receive(:download_or_copy_upload).and_raise(exception)
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:log_exception)
+ .with(
+ instance_of(exception), {
+ portable_id: project.id,
+ portable_class: 'Project',
+ upload_id: upload.id
+ }
+ )
+
+ service.execute
+
+ expect(File).not_to exist(exported_filepath)
+ end
+ end
+
+ context 'when filename is too long' do
+ let(:exception) { Errno::ENAMETOOLONG }
+
+ include_examples 'export with invalid upload'
+ end
+
+ context 'when network exception occurs' do
+ let(:exception) { Net::OpenTimeout }
- expect(File.exist?(File.join(export_path, 'avatar', 'rails_sample.png'))).to eq(true)
- expect(File.exist?(File.join(export_path, upload.secret, upload.retrieve_uploader.filename))).to eq(true)
+ include_examples 'export with invalid upload'
+ end
+ end
end
end
end
diff --git a/spec/services/ci/generate_kubeconfig_service_spec.rb b/spec/services/ci/generate_kubeconfig_service_spec.rb
index e3088ca6ea7..bfde39780dd 100644
--- a/spec/services/ci/generate_kubeconfig_service_spec.rb
+++ b/spec/services/ci/generate_kubeconfig_service_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe Ci::GenerateKubeconfigService do
let(:pipeline) { build.pipeline }
let(:agent1) { create(:cluster_agent, project: project) }
let(:agent2) { create(:cluster_agent) }
+ let(:authorization1) { create(:agent_project_authorization, agent: agent1) }
+ let(:authorization2) { create(:agent_project_authorization, agent: agent2) }
let(:template) { instance_double(Gitlab::Kubernetes::Kubeconfig::Template) }
@@ -16,7 +18,7 @@ RSpec.describe Ci::GenerateKubeconfigService do
before do
expect(Gitlab::Kubernetes::Kubeconfig::Template).to receive(:new).and_return(template)
- expect(pipeline).to receive(:authorized_cluster_agents).and_return([agent1, agent2])
+ expect(pipeline).to receive(:cluster_agent_authorizations).and_return([authorization1, authorization2])
end
it 'adds a cluster, and a user and context for each available agent' do
@@ -36,11 +38,13 @@ RSpec.describe Ci::GenerateKubeconfigService do
expect(template).to receive(:add_context).with(
name: "#{project.full_path}:#{agent1.name}",
+ namespace: 'production',
cluster: 'gitlab',
user: "agent:#{agent1.id}"
)
expect(template).to receive(:add_context).with(
name: "#{agent2.project.full_path}:#{agent2.name}",
+ namespace: 'production',
cluster: 'gitlab',
user: "agent:#{agent2.id}"
)
diff --git a/spec/support/cross_database_modification.rb b/spec/support/cross_database_modification.rb
deleted file mode 100644
index e0d91001c03..00000000000
--- a/spec/support/cross_database_modification.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.configure do |config|
- config.after do |example|
- [::ApplicationRecord, ::Ci::ApplicationRecord].each do |base_class|
- base_class.gitlab_transactions_stack.clear if base_class.respond_to?(:gitlab_transactions_stack)
- end
- end
-end
diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb
index 19fbf902d87..759e8316cc5 100644
--- a/spec/support/database/prevent_cross_database_modification.rb
+++ b/spec/support/database/prevent_cross_database_modification.rb
@@ -27,5 +27,9 @@ RSpec.configure do |config|
# Reset after execution to preferred state
config.after do |example_file|
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress_in_rspec = true
+
+ [::ApplicationRecord, ::Ci::ApplicationRecord].each do |base_class|
+ base_class.gitlab_transactions_stack.clear if base_class.respond_to?(:gitlab_transactions_stack)
+ end
end
end
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 91b6baac610..8a64efe9df5 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -50,8 +50,8 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
end
- let!(:label) { create(:label, project: project1) }
- let!(:label2) { create(:label, project: project1) }
+ let_it_be(:label) { create(:label, project: project1) }
+ let_it_be(:label2) { create(:label, project: project1) }
let!(:merge_request1) do
create(:merge_request, assignees: [user], author: user, reviewers: [user2],
@@ -87,13 +87,16 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
let!(:label_link) { create(:label_link, label: label, target: merge_request2) }
let!(:label_link2) { create(:label_link, label: label2, target: merge_request3) }
- before do
+ before_all do
project1.add_maintainer(user)
- project2.add_developer(user)
- project3.add_developer(user)
project4.add_developer(user)
project5.add_developer(user)
project6.add_developer(user)
+ end
+
+ before do
+ project2.add_developer(user)
+ project3.add_developer(user)
project2.add_developer(user2)
end
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index f4eea28b66f..6084dc194da 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
require_relative '../../../tooling/quality/test_level'