Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-01 21:11:40 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-01 21:11:40 +0300
commit3bfb19d99e3508b2a42c49d09e5a3236d2ce3a29 (patch)
treedc3a6a664e81caaa99530260ad56821479a8a939
parent18d5458781b21dee4dbb8854c72c064e9bd808ed (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/Synchronous Database Index.md11
-rw-r--r--.rubocop_todo/gitlab/strong_memoize_attr.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/rspec/context_wording.yml1
-rw-r--r--.rubocop_todo/rspec/factory_bot/avoid_create.yml1
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue3
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue4
-rw-r--r--app/assets/javascripts/releases/release_notification_service.js23
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/merge_request_menu.vue40
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue27
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss18
-rw-r--r--app/controllers/concerns/analytics/cycle_analytics/value_stream_actions.rb32
-rw-r--r--app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb11
-rw-r--r--app/graphql/resolvers/issues_resolver.rb1
-rw-r--r--app/graphql/types/project_type.rb14
-rw-r--r--app/helpers/sidebars_helper.rb24
-rw-r--r--app/models/merge_request.rb8
-rw-r--r--app/models/wiki_directory.rb3
-rw-r--r--app/services/merge_requests/after_create_service.rb6
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/security/ci_configuration/base_create_service.rb10
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml5
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/new_merge_request_worker.rb4
-rw-r--r--config/feature_flags/development/add_prepared_state_to_mr.yml8
-rw-r--r--config/initializers/memory_watchdog.rb2
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--db/migrate/20230125090315_add_prepared_at_to_merge_request.rb17
-rw-r--r--db/migrate/20230130204743_remove_protected_environment_default_access_level.rb7
-rw-r--r--db/schema_migrations/202301250903151
-rw-r--r--db/schema_migrations/202301302047431
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/inactive_project_deletion.md78
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/group_epic_boards.md171
-rw-r--r--doc/api/settings.md15
-rw-r--r--doc/development/database/adding_database_indexes.md20
-rw-r--r--doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v13_8.pngbin42245 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.pngbin0 -> 14640 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v13_8.pngbin38840 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.pngbin0 -> 10917 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_4.pngbin20617 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_9.pngbin0 -> 7957 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/index.md8
-rw-r--r--locale/gitlab.pot12
-rw-r--r--qa/qa/page/component/wiki_sidebar.rb5
-rw-r--r--qa/qa/resource/api_fabricator.rb1
-rw-r--r--qa/qa/resource/project_web_hook.rb19
-rw-r--r--qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb41
-rw-r--r--spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb21
-rw-r--r--spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb14
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb66
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js11
-rw-r--r--spec/frontend/releases/components/app_show_spec.js7
-rw-r--r--spec/frontend/releases/release_notification_service_spec.js57
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js5
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js46
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js11
-rw-r--r--spec/frontend/super_sidebar/mock_data.js21
-rw-r--r--spec/graphql/types/project_type_spec.rb11
-rw-r--r--spec/helpers/sidebars_helper_spec.rb26
-rw-r--r--spec/initializers/memory_watchdog_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb29
-rw-r--r--spec/models/wiki_directory_spec.rb7
-rw-r--r--spec/requests/api/graphql/issues_spec.rb12
-rw-r--r--spec/services/google_cloud/fetch_google_ip_list_service_spec.rb2
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb13
-rw-r--r--spec/services/security/ci_configuration/sast_create_service_spec.rb48
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb15
-rw-r--r--spec/uploaders/object_storage/cdn/google_cdn_spec.rb5
-rw-r--r--spec/uploaders/object_storage/cdn_spec.rb2
-rw-r--r--spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb2
-rw-r--r--spec/workers/new_merge_request_worker_spec.rb71
79 files changed, 1060 insertions, 137 deletions
diff --git a/.gitlab/issue_templates/Synchronous Database Index.md b/.gitlab/issue_templates/Synchronous Database Index.md
new file mode 100644
index 00000000000..c61cf7abf0c
--- /dev/null
+++ b/.gitlab/issue_templates/Synchronous Database Index.md
@@ -0,0 +1,11 @@
+<!-- Title suggestion: <async-index-name> synchronous database index(es) addition/removal -->
+
+## Summary
+
+This issue is to add a migration(s) to create/destroy the `<async-index-name>` database index(es) synchronously after it has been created/destroyed on GitLab.com.
+
+The asynchronous index(es) was introduced in <!-- Link to MR that introduced the asynchronous index -->.
+
+/assign me
+/due in 2 weeks
+/label ~database ~"type::maintenance" ~"maintenance::scalability"
diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index e0a243aec8e..97e5db6567f 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -326,7 +326,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/app/helpers/ee/trial_helper.rb'
- 'ee/app/helpers/ee/welcome_helper.rb'
- 'ee/app/helpers/license_monitoring_helper.rb'
- - 'ee/app/helpers/paid_feature_callout_helper.rb'
- 'ee/app/helpers/subscriptions_helper.rb'
- 'ee/app/helpers/trial_status_widget_helper.rb'
- 'ee/app/models/approval_merge_request_rule.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 5cc1faaf78f..a77d44b1827 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -1912,7 +1912,6 @@ Layout/LineLength:
- 'ee/spec/helpers/license_helper_spec.rb'
- 'ee/spec/helpers/license_monitoring_helper_spec.rb'
- 'ee/spec/helpers/notes_helper_spec.rb'
- - 'ee/spec/helpers/paid_feature_callout_helper_spec.rb'
- 'ee/spec/helpers/projects/on_demand_scans_helper_spec.rb'
- 'ee/spec/helpers/projects/project_members_helper_spec.rb'
- 'ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index ab9031509c5..ae17e4d1f8d 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -284,7 +284,6 @@ RSpec/ContextWording:
- 'ee/spec/helpers/groups/security_features_helper_spec.rb'
- 'ee/spec/helpers/license_helper_spec.rb'
- 'ee/spec/helpers/license_monitoring_helper_spec.rb'
- - 'ee/spec/helpers/paid_feature_callout_helper_spec.rb'
- 'ee/spec/helpers/projects/security/discover_helper_spec.rb'
- 'ee/spec/helpers/projects_helper_spec.rb'
- 'ee/spec/helpers/roadmaps_helper_spec.rb'
diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml
index a857841f17a..0a4ea883cbe 100644
--- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml
+++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml
@@ -66,7 +66,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/helpers/manual_quarterly_co_term_banner_helper_spec.rb'
- 'ee/spec/helpers/markup_helper_spec.rb'
- 'ee/spec/helpers/notes_helper_spec.rb'
- - 'ee/spec/helpers/paid_feature_callout_helper_spec.rb'
- 'ee/spec/helpers/path_locks_helper_spec.rb'
- 'ee/spec/helpers/prevent_forking_helper_spec.rb'
- 'ee/spec/helpers/projects/on_demand_scans_helper_spec.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index e5b0b67caab..3cc6a613303 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -625,7 +625,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/helpers/nav/new_dropdown_helper_spec.rb'
- 'ee/spec/helpers/nav/top_nav_helper_spec.rb'
- 'ee/spec/helpers/notes_helper_spec.rb'
- - 'ee/spec/helpers/paid_feature_callout_helper_spec.rb'
- 'ee/spec/helpers/path_locks_helper_spec.rb'
- 'ee/spec/helpers/preferences_helper_spec.rb'
- 'ee/spec/helpers/prevent_forking_helper_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 6d84c4f035d..2dd600c5f52 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-ba63ee19fb2dafe6f2ca5bca5d12a0b24837ce17
+4e200026c03189abe2c60b071204340c4af5cf35
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index a2bf6e67178..ff92cdd42c6 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -13,6 +13,7 @@ import { isSameOriginUrl, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
+import { putCreateReleaseNotification } from '~/releases/release_notification_service';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import AssetLinksForm from './asset_links_form.vue';
import ConfirmDeleteModal from './confirm_delete_modal.vue';
@@ -49,6 +50,7 @@ export default {
'newMilestonePath',
'manageMilestonesPath',
'projectId',
+ 'projectPath',
'groupId',
'groupMilestonesAvailable',
'tagNotes',
@@ -150,6 +152,7 @@ export default {
submitForm() {
if (!this.isFormSubmissionDisabled) {
this.saveRelease();
+ putCreateReleaseNotification(this.projectPath, this.release.name);
}
},
},
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index 7147cfa01c8..544f2de5132 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,6 +1,7 @@
<script>
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
+import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import oneReleaseQuery from '../graphql/queries/one_release.query.graphql';
import { convertGraphQLRelease } from '../util';
import ReleaseBlock from './release_block.vue';
@@ -49,6 +50,9 @@ export default {
},
},
},
+ mounted() {
+ popCreateReleaseNotification(this.fullPath);
+ },
methods: {
showFlash(error) {
createAlert({
diff --git a/app/assets/javascripts/releases/release_notification_service.js b/app/assets/javascripts/releases/release_notification_service.js
new file mode 100644
index 00000000000..a4f926d7561
--- /dev/null
+++ b/app/assets/javascripts/releases/release_notification_service.js
@@ -0,0 +1,23 @@
+import { s__, sprintf } from '~/locale';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
+
+const createReleaseSessionKey = (projectPath) => `createRelease:${projectPath}`;
+
+export const putCreateReleaseNotification = (projectPath, releaseName) => {
+ window.sessionStorage.setItem(createReleaseSessionKey(projectPath), releaseName);
+};
+
+export const popCreateReleaseNotification = (projectPath) => {
+ const key = createReleaseSessionKey(projectPath);
+ const createdRelease = window.sessionStorage.getItem(key);
+
+ if (createdRelease) {
+ createAlert({
+ message: sprintf(s__('Release|Release %{createdRelease} has been successfully created.'), {
+ createdRelease,
+ }),
+ variant: VARIANT_SUCCESS,
+ });
+ window.sessionStorage.removeItem(key);
+ }
+};
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 7828efc358a..3ebd21609a6 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
@@ -51,6 +52,7 @@ export default {
UserCalloutDismisser,
TrainingProviderList,
},
+ directives: { SafeHtml },
inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
props: {
augmentedSecurityFeatures: {
@@ -143,7 +145,7 @@ export default {
variant="danger"
@dismiss="dismissAlert"
>
- {{ errorMessage }}
+ <span v-safe-html="errorMessage"></span>
</gl-alert>
<local-storage-sync
v-model="autoDevopsEnabledAlertDismissedProjects"
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
index d790e61ca31..62a1e5a6b20 100644
--- a/app/assets/javascripts/super_sidebar/components/counter.vue
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -40,9 +40,9 @@ export default {
:is="component"
:aria-label="ariaLabel"
:href="href"
- class="counter gl-relative gl-display-inline-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-black-normal gl-border gl-border-gray-a-08 gl-font-sm gl-font-weight-bold"
+ class="counter gl-display-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border gl-border-gray-a-08 gl-font-sm gl-hover-text-gray-900 gl-hover-text-decoration-none"
>
<gl-icon aria-hidden="true" :name="icon" />
- <span aria-hidden="true">{{ count }}</span>
+ <span v-if="count" aria-hidden="true" class="gl-ml-1">{{ count }}</span>
</component>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
new file mode 100644
index 00000000000..edc13e305cf
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlBadge,
+ GlDisclosureDropdown,
+ },
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ navigate() {
+ this.$refs.link.click();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown :items="items" placement="center" @action="navigate">
+ <template #toggle>
+ <slot></slot>
+ </template>
+ <template #list-item="{ item }">
+ <a
+ ref="link"
+ class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900"
+ :href="item.href"
+ tabindex="-1"
+ >
+ {{ item.text }}
+ <gl-badge pill size="sm" variant="neutral">{{ item.count || 0 }}</gl-badge>
+ </a>
+ </template>
+ </gl-disclosure-dropdown>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 22ef58eb302..ee72e8eafb4 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -1,11 +1,12 @@
<script>
-import { GlAvatar, GlDropdown, GlIcon } from '@gitlab/ui';
+import { GlAvatar, GlDropdown, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import logo from '../../../../views/shared/_logo.svg';
import CreateMenu from './create_menu.vue';
import Counter from './counter.vue';
+import MergeRequestMenu from './merge_request_menu.vue';
export default {
logo,
@@ -16,6 +17,7 @@ export default {
CreateMenu,
NewNavToggle,
Counter,
+ MergeRequestMenu,
},
i18n: {
createNew: __('Create new...'),
@@ -24,6 +26,7 @@ export default {
todoList: __('To-Do list'),
},
directives: {
+ GlTooltip: GlTooltipDirective,
SafeHtml,
},
inject: ['rootPath', 'toggleNewNavEndpoint'],
@@ -55,17 +58,29 @@ export default {
</div>
<div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2">
<counter
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
+ class="gl-flex-basis-third"
icon="issues"
:count="sidebarData.assigned_open_issues_count"
:href="sidebarData.issues_dashboard_path"
:label="$options.i18n.issues"
/>
+ <merge-request-menu
+ class="gl-flex-basis-third gl-display-block!"
+ :items="sidebarData.merge_request_menu"
+ >
+ <counter
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.mergeRequests"
+ class="gl-w-full"
+ tabindex="-1"
+ icon="merge-request-open"
+ :count="sidebarData.total_merge_requests_count"
+ :label="$options.i18n.mergeRequests"
+ />
+ </merge-request-menu>
<counter
- icon="merge-request-open"
- :count="sidebarData.assigned_open_merge_requests_count"
- :label="$options.i18n.mergeRequests"
- />
- <counter
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
+ class="gl-flex-basis-third"
icon="todo-done"
:count="sidebarData.todos_pending_count"
href="/dashboard/todos"
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 575fbc03f46..90313aee5b8 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -11,6 +11,24 @@
}
}
+ .counter .gl-icon {
+ color: var(--gray-500, $gray-500);
+ }
+
+ .counter:hover,
+ .counter:focus,
+ .gl-dropdown-custom-toggle:hover .counter,
+ .gl-dropdown-custom-toggle:focus .counter,
+ .gl-dropdown-custom-toggle[aria-expanded='true'] .counter {
+ background-color: $gray-50;
+ border-color: transparent;
+ mix-blend-mode: multiply;
+
+ .gl-icon {
+ color: var(--gray-700, $gray-700);
+ }
+ }
+
.context-switcher-toggle {
&[aria-expanded='true'] {
background-color: $t-gray-a-08;
diff --git a/app/controllers/concerns/analytics/cycle_analytics/value_stream_actions.rb b/app/controllers/concerns/analytics/cycle_analytics/value_stream_actions.rb
new file mode 100644
index 00000000000..f10b23d1664
--- /dev/null
+++ b/app/controllers/concerns/analytics/cycle_analytics/value_stream_actions.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module ValueStreamActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize
+ end
+
+ def index
+ # FOSS users can only see the default value stream
+ value_streams = [Analytics::CycleAnalytics::ValueStream.build_default_value_stream(namespace)]
+
+ render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
+ end
+
+ private
+
+ def namespace
+ raise NotImplementedError
+ end
+
+ def authorize
+ authorize_read_cycle_analytics!
+ end
+ end
+ end
+end
+
+Analytics::CycleAnalytics::ValueStreamActions.prepend_mod_with('Analytics::CycleAnalytics::ValueStreamActions')
diff --git a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
index 60bcd1d7238..f58730f1d33 100644
--- a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
+++ b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
@@ -1,17 +1,16 @@
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::ApplicationController
+ include ::Analytics::CycleAnalytics::ValueStreamActions
+
respond_to :json
feature_category :planning_analytics
urgency :low
- before_action :authorize_read_cycle_analytics!
-
- def index
- # FOSS users can only see the default value stream
- value_streams = [Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)]
+ private
- render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
+ def namespace
+ project.project_namespace
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 718ca5a7b57..cfb596306b0 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -351,10 +351,20 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
+ # NOTE: Remove this disable with add_prepared_state_to_mr FF removal
+ # rubocop: disable Metrics/AbcSize
def show_merge_request
close_merge_request_if_no_source_project
@merge_request.check_mergeability(async: true)
+ # NOTE: Remove the created_at check when removing the FF check
+ if ::Feature.enabled?(:add_prepared_state_to_mr, @merge_request.project) &&
+ @merge_request.created_at < 5.minutes.ago &&
+ !@merge_request.prepared?
+
+ @merge_request.prepare
+ end
+
respond_to do |format|
format.html do
# use next to appease Rubocop
@@ -396,6 +406,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
end
+ # rubocop: enable Metrics/AbcSize
def render_html_page
preload_assignees_for_render(@merge_request)
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index a783990b33f..bbf45efa33e 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -17,6 +17,7 @@ module Resolvers
before_connection_authorization do |nodes, current_user|
projects = nodes.map(&:project)
::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+ ::Preloaders::GroupPolicyPreloader.new(projects.filter_map(&:group), current_user).execute
end
def ready?(**args)
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index def16887c53..2fe9ac7e155 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -636,6 +636,11 @@ module Types
def sast_ci_configuration
return unless Ability.allowed?(current_user, :read_code, object)
+ if project.repository.empty?
+ raise Gitlab::Graphql::Errors::MutationError,
+ _(format('You must %s before using Security features.', add_file_docs_link.html_safe)).html_safe
+ end
+
::Security::CiConfiguration::SastParserService.new(object).configuration
end
@@ -654,6 +659,15 @@ module Types
def project
@project ||= object.respond_to?(:sync) ? object.sync : object
end
+
+ def add_file_docs_link
+ ActionController::Base.helpers.link_to _('add at least one file to the repository'),
+ Rails.application.routes.url_helpers.help_page_url(
+ 'user/project/repository/index.md',
+ anchor: 'add-files-to-a-repository'),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ end
end
end
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 2ca6a46906c..08306c53a4c 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
module SidebarsHelper
+ include MergeRequestsHelper
include Nav::NewDropdownHelper
def sidebar_tracking_attributes_by_object(object)
@@ -39,10 +40,11 @@ module SidebarsHelper
username: user.username,
avatar_url: user.avatar_url,
assigned_open_issues_count: user.assigned_open_issues_count,
- assigned_open_merge_requests_count: user.assigned_open_merge_requests_count,
todos_pending_count: user.todos_pending_count,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
+ total_merge_requests_count: user_merge_requests_counts[:total],
create_new_menu_groups: create_new_menu_groups(group: group, project: project),
+ merge_request_menu: create_merge_request_menu(user),
support_path: support_url,
display_whats_new: display_whats_new?
}
@@ -66,6 +68,26 @@ module SidebarsHelper
end
end
+ def create_merge_request_menu(user)
+ [
+ {
+ name: _('Merge requests'),
+ items: [
+ {
+ text: _('Assigned'),
+ href: merge_requests_dashboard_path(assignee_username: user.username),
+ count: user_merge_requests_counts[:assigned]
+ },
+ {
+ text: _('Review requests'),
+ href: merge_requests_dashboard_path(reviewer_username: user.username),
+ count: user_merge_requests_counts[:review_requested]
+ }
+ ]
+ }
+ ]
+ end
+
def sidebar_attributes_for_object(object)
case object
when Project
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a91c55e858b..3bc60ee1f8e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -2021,6 +2021,14 @@ class MergeRequest < ApplicationRecord
Feature.enabled?(:diffs_batch_cache_with_max_age, project)
end
+ def prepared?
+ prepared_at.present?
+ end
+
+ def prepare
+ NewMergeRequestWorker.perform_async(id, author_id)
+ end
+
private
attr_accessor :skip_fetch_ref
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
index 3a2613e15d9..f5d00013622 100644
--- a/app/models/wiki_directory.rb
+++ b/app/models/wiki_directory.rb
@@ -6,7 +6,7 @@ class WikiDirectory
attr_accessor :slug, :entries
validates :slug, presence: true
-
+ alias_method :to_param, :slug
# Groups a list of wiki pages into a nested collection of WikiPage and WikiDirectory objects,
# preserving the order of the passed pages.
#
@@ -25,6 +25,7 @@ class WikiDirectory
parent = File.dirname(path)
parent = '' if parent == '.'
directories[parent].entries << directory
+ directories[parent].entries.delete_if { |item| item.is_a?(WikiPage) && item.slug == directory.slug }
end
end
end
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index 9e39aa94246..11251e56ee3 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -9,6 +9,8 @@ module MergeRequests
prepare_for_mergeability(merge_request)
prepare_merge_request(merge_request)
+
+ mark_merge_request_as_prepared(merge_request)
end
private
@@ -53,6 +55,10 @@ module MergeRequests
merge_request.mark_as_unchecked
merge_request.check_mergeability(async: true)
end
+
+ def mark_merge_request_as_prepared(merge_request)
+ merge_request.update!(prepared_at: Time.current)
+ end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index e057e6aba51..94cc4700a49 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -197,7 +197,7 @@ module Projects
end
def create_sast_commit
- ::Security::CiConfiguration::SastCreateService.new(@project, current_user, {}, commit_on_default: true).execute
+ ::Security::CiConfiguration::SastCreateService.new(@project, current_user, { initialize_with_sast: true }, commit_on_default: true).execute
end
def readme_content
diff --git a/app/services/security/ci_configuration/base_create_service.rb b/app/services/security/ci_configuration/base_create_service.rb
index aaa850fde39..3e8865d3dff 100644
--- a/app/services/security/ci_configuration/base_create_service.rb
+++ b/app/services/security/ci_configuration/base_create_service.rb
@@ -12,6 +12,16 @@ module Security
end
def execute
+ if project.repository.empty? && !(@params && @params[:initialize_with_sast])
+ docs_link = ActionController::Base.helpers.link_to _('add at least one file to the repository'),
+ Rails.application.routes.url_helpers.help_page_url('user/project/repository/index.md',
+ anchor: 'add-files-to-a-repository'),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ raise Gitlab::Graphql::Errors::MutationError,
+ _(format('You must %s before using Security features.', docs_link.html_safe)).html_safe
+ end
+
project.repository.add_branch(current_user, branch_name, project.default_branch)
attributes_for_commit = attributes
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index a492d1e5aa0..a29e6ba7a85 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,4 +1,5 @@
-%li{ data: { qa_selector: 'wiki_directory_content' } }
- = wiki_directory.title
+%li{ class: active_when(params[:id] == wiki_directory.slug), data: { qa_selector: 'wiki_directory_content' } }
+ = link_to wiki_page_path(@wiki, wiki_directory), data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do
+ = wiki_directory.title
%ul
= render wiki_directory.entries, context: context
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index d1078c4bf92..e5d78aa9039 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2935,7 +2935,7 @@
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: new_note
:worker_name: NewNoteWorker
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index d6e8d517b5a..a32a414c0ba 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -8,6 +8,9 @@ class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
include NewIssuable
+ idempotent!
+ deduplicate :until_executed
+
feature_category :code_review_workflow
urgency :high
worker_resource_boundary :cpu
@@ -15,6 +18,7 @@ class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
def perform(merge_request_id, user_id)
return unless objects_found?(merge_request_id, user_id)
+ return if issuable.prepared?
MergeRequests::AfterCreateService
.new(project: issuable.target_project, current_user: user)
diff --git a/config/feature_flags/development/add_prepared_state_to_mr.yml b/config/feature_flags/development/add_prepared_state_to_mr.yml
new file mode 100644
index 00000000000..49db6d92ae0
--- /dev/null
+++ b/config/feature_flags/development/add_prepared_state_to_mr.yml
@@ -0,0 +1,8 @@
+---
+name: add_prepared_state_to_mr
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109967
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389249
+milestone: '15.9'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/initializers/memory_watchdog.rb b/config/initializers/memory_watchdog.rb
index 99c5d61293f..27d9bf8b8f8 100644
--- a/config/initializers/memory_watchdog.rb
+++ b/config/initializers/memory_watchdog.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
return unless Gitlab::Runtime.application?
-return unless Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'], default: Gitlab::Runtime.puma?)
+return unless Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'], default: true)
Gitlab::Cluster::LifecycleEvents.on_worker_start do
watchdog = Gitlab::Memory::Watchdog.new
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 58441b83c7d..cf8df814990 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -33,7 +33,7 @@ queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE
enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
enable_sidekiq_memory_killer = ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'].to_i.nonzero? &&
- !Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'])
+ !Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'], default: true)
Sidekiq.configure_server do |config|
config[:strict] = false
diff --git a/db/migrate/20230125090315_add_prepared_at_to_merge_request.rb b/db/migrate/20230125090315_add_prepared_at_to_merge_request.rb
new file mode 100644
index 00000000000..4e4b4ccf671
--- /dev/null
+++ b/db/migrate/20230125090315_add_prepared_at_to_merge_request.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddPreparedAtToMergeRequest < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ add_column :merge_requests, 'prepared_at', :datetime_with_timezone
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :merge_requests, 'prepared_at'
+ end
+ end
+end
diff --git a/db/migrate/20230130204743_remove_protected_environment_default_access_level.rb b/db/migrate/20230130204743_remove_protected_environment_default_access_level.rb
new file mode 100644
index 00000000000..d01fd6b90f3
--- /dev/null
+++ b/db/migrate/20230130204743_remove_protected_environment_default_access_level.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class RemoveProtectedEnvironmentDefaultAccessLevel < Gitlab::Database::Migration[2.1]
+ def change
+ change_column_default :protected_environment_deploy_access_levels, :access_level, from: 40, to: nil
+ end
+end
diff --git a/db/schema_migrations/20230125090315 b/db/schema_migrations/20230125090315
new file mode 100644
index 00000000000..aefe04923e7
--- /dev/null
+++ b/db/schema_migrations/20230125090315
@@ -0,0 +1 @@
+37cc2c2eeb910333a45a18820a569d4263eb614bc138a6a0fe11d037bae045c3 \ No newline at end of file
diff --git a/db/schema_migrations/20230130204743 b/db/schema_migrations/20230130204743
new file mode 100644
index 00000000000..dcb1725a6e2
--- /dev/null
+++ b/db/schema_migrations/20230130204743
@@ -0,0 +1 @@
+3c6dd3b83bc6a1d9e94c93784e201d3e9114ef62070468a31abe9167ae111c35 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a7e230d4cf1..6321bfa3ab0 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17898,6 +17898,7 @@ CREATE TABLE merge_requests (
sprint_id bigint,
merge_ref_sha bytea,
draft boolean DEFAULT false NOT NULL,
+ prepared_at timestamp with time zone,
CONSTRAINT check_970d272570 CHECK ((lock_version IS NOT NULL))
);
@@ -20968,7 +20969,7 @@ CREATE TABLE protected_environment_deploy_access_levels (
id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
- access_level integer DEFAULT 40,
+ access_level integer,
protected_environment_id integer NOT NULL,
user_id integer,
group_id integer,
diff --git a/doc/administration/inactive_project_deletion.md b/doc/administration/inactive_project_deletion.md
index ea5658bef84..ed75373448e 100644
--- a/doc/administration/inactive_project_deletion.md
+++ b/doc/administration/inactive_project_deletion.md
@@ -8,70 +8,52 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0 [with a flag](../administration/feature_flags.md) named `inactive_projects_deletion`. Disabled by default.
> - [Feature flag `inactive_projects_deletion`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96803) removed in GitLab 15.4.
+> - Configuration through GitLab UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85575) in GitLab 15.1.
Administrators of large GitLab instances can find that over time, projects become inactive and are no longer used.
-These projects take up unnecessary disk space. With inactive project deletion, you can identify these projects, warn
-the maintainers ahead of time, and then delete the projects if they remain inactive. When an inactive project is
-deleted, the action generates an audit event that it was performed by the @GitLab-Admin-Bot.
+These projects take up unnecessary disk space.
+
+With inactive project deletion, you can identify these projects, warn the maintainers ahead of time, and then delete the
+projects if they remain inactive. When an inactive project is deleted, the action generates an audit event that it was
+performed by the @GitLab-Admin-Bot.
For the default setting on GitLab.com, see the [GitLab.com settings page](../user/gitlab_com/index.md#inactive-project-deletion).
## Configure inactive project deletion
-You can configure inactive projects deletion or turn it off using either:
-
-- [The GitLab API](#using-the-api) (GitLab 15.0 and later).
-- [The GitLab UI](#using-the-gitlab-ui) (GitLab 15.1 and later).
-
-The following options are available:
-
-- **Delete inactive projects** (`delete_inactive_projects`): Enable or disable inactive project deletion.
-- **Delete inactive projects that exceed** (`inactive_projects_min_size_mb`): Minimum size (MB) of inactive projects to
- be considered for deletion. Projects smaller in size than this threshold aren't considered inactive.
-- **Delete project after** (`inactive_projects_delete_after_months`): Minimum duration (months) after which a project is
- scheduled for deletion if it continues be inactive.
-- **Send warning email** (`inactive_projects_send_warning_email_after_months`): Minimum duration (months) after which a
- deletion warning email is sent if a project continues to be inactive. The warning email is sent to users with the
- Owner and Maintainer roles of the inactive project. This duration must be less than the
- **Delete project after** (`inactive_projects_delete_after_months`) duration.
+To configure deletion of inactive projects:
-For example (using the API):
-
-- `delete_inactive_projects` enabled.
-- `inactive_projects_min_size_mb` set to `50`.
-- `inactive_projects_delete_after_months` set to `12`.
-- `inactive_projects_send_warning_email_after_months` set to `6`.
-
-In this scenario, when a project's size is:
-
-- Less than 50 MB, the project is not considered inactive.
-- Greater than 50 MB and it is inactive for:
- - More than 6 months, a deletion warning is email is sent to users with the Owner and Maintainer role on the project
- with the scheduled date of deletion.
- - More than 12 months, the project is scheduled for deletion.
+1. On the top bar, select **Main menu > Admin**.
+1. On the left sidebar, select **Settings > Repository**.
+1. Expand **Repository maintenance**.
+1. In the **Inactive project deletion** section, select **Delete inactive projects**.
+1. Configure the settings.
+ - The warning email is sent to users who have the Owner and Maintainer role for the inactive project.
+ - The email duration must be less than the **Delete project after** duration.
+1. Select **Save changes**.
-### Using the API
+### Configuration example
-You can use the [Application settings API](../api/settings.md#change-application-settings) to configure inactive projects.
+If you use these settings:
-### Using the GitLab UI
+- **Delete inactive projects** enabled.
+- **Delete inactive projects that exceed** set to `50`.
+- **Delete project after** set to `12`.
+- **Send warning email** set to `6`.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85575) in GitLab 15.1.
+If a project is less than 50 MB, the project is not considered inactive.
-To configure inactive projects with the GitLab UI:
+If a project is more than 50 MB and it is inactive for:
-1. On the top bar, select **Main menu > Admin**.
-1. On the left sidebar, select **Settings > Repository**.
-1. Expand **Repository maintenance**.
-1. In the **Inactive project deletion** section, configure the necessary options.
-1. Select **Save changes**.
+- More than 6 months: A deletion warning email is sent. This mail includes the date that the project will be deleted.
+- More than 12 months: The project is scheduled for deletion.
## Determine when a project was last active
You can view a project's activities and determine when the project was last active in the following ways:
-1. Go to the [activity page](../user/project/working_with_projects.md#view-project-activity) for the project and view
- the date of the latest event.
-1. View the `last_activity_at` attribute for the project using the [Projects API](../api/projects.md).
-1. List the visible events for the project using the [Events API](../api/events.md#list-a-projects-visible-events).
- View the `created_at` attribute of the latest event.
+- Go to the [activity page](../user/project/working_with_projects.md#view-project-activity) for the project and view
+ the date of the latest event.
+- View the `last_activity_at` attribute for the project using the [Projects API](../api/projects.md).
+- List the visible events for the project using the [Events API](../api/events.md#list-a-projects-visible-events).
+ View the `created_at` attribute of the latest event.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 419175f6553..994246b64e4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2836,6 +2836,7 @@ Input type: `EpicMoveListInput`
| <a id="mutationepicmovelistfromlistid"></a>`fromListId` | [`BoardsEpicListID`](#boardsepiclistid) | ID of the board list that the epic will be moved from. Required if moving between lists. |
| <a id="mutationepicmovelistmoveafterid"></a>`moveAfterId` | [`EpicID`](#epicid) | ID of epic that should be placed after the current epic. |
| <a id="mutationepicmovelistmovebeforeid"></a>`moveBeforeId` | [`EpicID`](#epicid) | ID of epic that should be placed before the current epic. |
+| <a id="mutationepicmovelistpositioninlist"></a>`positionInList` | [`Int`](#int) | Position of epics within the board list. Positions start at 0. Use -1 to move to the end of the list. |
| <a id="mutationepicmovelisttolistid"></a>`toListId` | [`BoardsEpicListID!`](#boardsepiclistid) | ID of the list the epic will be in after mutation. |
#### Fields
diff --git a/doc/api/group_epic_boards.md b/doc/api/group_epic_boards.md
new file mode 100644
index 00000000000..93be9431874
--- /dev/null
+++ b/doc/api/group_epic_boards.md
@@ -0,0 +1,171 @@
+---
+stage: Plan
+group: Product Planning
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Group epic boards API **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385903) in GitLab 15.9.
+
+Every API call to [group epic boards](../user/group/epics/epic_boards.md#epic-boards) must be authenticated.
+
+If a user is not a member of a group and the group is private, a `GET`
+request results in `404` status code.
+
+## List all epic boards in a group
+
+Lists epic boards in the given group.
+
+```plaintext
+GET /groups/:id/epic_boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) accessible by the authenticated user |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epic_boards"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "group epic board",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "hide_backlog_list": false,
+ "hide_closed_list": false,
+ "labels": [
+ {
+ "id": 1,
+ "title": "Board Label",
+ "color": "#c21e56",
+ "description": "label applied to the epic board",
+ "group_id": 5,
+ "project_id": null,
+ "template": false,
+ "text_color": "#FFFFFF",
+ "created_at": "2023-01-27T10:40:59.738Z",
+ "updated_at": "2023-01-27T10:40:59.738Z"
+ }
+ ],
+ "lists": [
+ {
+ "id": 1,
+ "label": {
+ "id": 69,
+ "name": "Testing",
+ "color": "#F0AD4E",
+ "description": null
+ },
+ "position": 1
+ },
+ {
+ "id": 2,
+ "label": {
+ "id": 70,
+ "name": "Ready",
+ "color": "#FF0000",
+ "description": null
+ },
+ "position": 2
+ },
+ {
+ "id": 3,
+ "label": {
+ "id": 71,
+ "name": "Production",
+ "color": "#FF5F00",
+ "description": null
+ },
+ "position": 3
+ }
+ ]
+ }
+]
+```
+
+## Single group epic board
+
+Gets a single group epic board.
+
+```plaintext
+GET /groups/:id/epic_boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) accessible by the authenticated user |
+| `board_id` | integer | yes | The ID of an epic board |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epic_boards/1"
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "name": "group epic board",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "labels": [
+ {
+ "id": 1,
+ "title": "Board Label",
+ "color": "#c21e56",
+ "description": "label applied to the epic board",
+ "group_id": 5,
+ "project_id": null,
+ "template": false,
+ "text_color": "#FFFFFF",
+ "created_at": "2023-01-27T10:40:59.738Z",
+ "updated_at": "2023-01-27T10:40:59.738Z"
+ }
+ ],
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "id": 69,
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "id": 70,
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "id": 71,
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 74dcf5b8fda..624aff7ff54 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -307,7 +307,6 @@ listed in the descriptions of the relevant settings.
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `delayed_project_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed project deletion by default in new groups. Default is `false`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), can only be enabled when `delayed_group_deletion` is true. |
| `delayed_group_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed group deletion. Default is `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352959) in GitLab 15.0. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), disables and locks the group-level setting for delayed protect deletion when set to `false`. |
-| `delete_inactive_projects` | boolean | no | Enable inactive project deletion feature. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational without feature flag](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96803) in GitLab 15.4. |
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between `1` and `90`. Defaults to `7`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), a hook on `deletion_adjourned_period` sets the period to `1` on every update, and sets both `delayed_project_deletion` and `delayed_group_deletion` to `false` if the period is `0`. |
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. |
| `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). |
@@ -389,9 +388,6 @@ listed in the descriptions of the relevant settings.
| `html_emails_enabled` | boolean | no | Enable HTML emails. |
| `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `bitbucket_server`, `gitlab`, `fogbugz`, `git`, `gitlab_project`, `gitea`, `manifest`, and `phabricator`. |
| `in_product_marketing_emails_enabled` | boolean | no | Enable [in-product marketing emails](../user/profile/notifications.md#global-notification-settings). Enabled by default. |
-| `inactive_projects_delete_after_months` | integer | no | If `delete_inactive_projects` is `true`, the time (in months) to wait before deleting inactive projects. Default is `2`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0. |
-| `inactive_projects_min_size_mb` | integer | no | If `delete_inactive_projects` is `true`, the minimum repository size for projects to be checked for inactivity. Default is `0`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0. |
-| `inactive_projects_send_warning_email_after_months` | integer | no | If `delete_inactive_projects` is `true`, sets the time (in months) to wait before emailing maintainers that the project is scheduled be deleted because it is inactive. Default is `1`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0. |
| `invisible_captcha_enabled` | boolean | no | Enable Invisible CAPTCHA spam detection during sign-up. Disabled by default. |
| `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Disabled by default.|
| `keep_latest_artifact` | boolean | no | Prevent the deletion of the artifacts from the most recent successful jobs, regardless of the expiry time. Enabled by default. |
@@ -526,6 +522,17 @@ listed in the descriptions of the relevant settings.
| `jira_connect_application_key` | String | no | Application ID of the OAuth application that should be used to authenticate with the GitLab for Jira Cloud app |
| `jira_connect_proxy_url` | String | no | URL of the GitLab instance that should be used as a proxy for the GitLab for Jira Cloud app |
+### Configure inactive project deletion
+
+You can configure inactive projects deletion or turn it off.
+
+| Attribute | Type | Required | Description |
+|------------------------------------------|------------------|:------------------------------------:|-------------|
+| `delete_inactive_projects` | boolean | no | Enable [inactive project deletion](../administration/inactive_project_deletion.md). Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational without feature flag](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96803) in GitLab 15.4. |
+| `inactive_projects_delete_after_months` | integer | no | If `delete_inactive_projects` is `true`, the time (in months) to wait before deleting inactive projects. Default is `2`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0. |
+| `inactive_projects_min_size_mb` | integer | no | If `delete_inactive_projects` is `true`, the minimum repository size for projects to be checked for inactivity. Default is `0`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0. |
+| `inactive_projects_send_warning_email_after_months` | integer | no | If `delete_inactive_projects` is `true`, sets the time (in months) to wait before emailing maintainers that the project is scheduled be deleted because it is inactive. Default is `1`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0. |
+
## Housekeeping fields
::Tabs
diff --git a/doc/development/database/adding_database_indexes.md b/doc/development/database/adding_database_indexes.md
index d909f66d6c8..1e3a1de9b69 100644
--- a/doc/development/database/adding_database_indexes.md
+++ b/doc/development/database/adding_database_indexes.md
@@ -310,8 +310,13 @@ index creation can proceed at a lower level of risk.
### Schedule the index to be created
-Create an MR with a post-deployment migration which prepares the index
-for asynchronous creation. An example of creating an index using
+1. Create a merge request containing a post-deployment migration, which prepares
+ the index for asynchronous creation.
+1. [Create a follow-up issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Synchronous%20Database%20Index)
+ to add a migration that creates the index synchronously.
+1. In the merge request that prepares the asynchronous index, add a comment mentioning the follow-up issue.
+
+An example of creating an index using
the asynchronous index helpers can be seen in the block below. This migration
enters the index name and definition into the `postgres_async_indexes`
table. The process that runs on weekends pulls indexes from this
@@ -322,6 +327,7 @@ table and attempt to create them.
INDEX_NAME = 'index_ci_builds_on_some_column'
+# TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
prepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end
@@ -405,8 +411,13 @@ index destruction can proceed at a lower level of risk.
### Schedule the index to be removed
-Create an MR with a post-deployment migration which prepares the index
-for asynchronous destruction. For example. to destroy an index using
+1. Create a merge request containing a post-deployment migration, which prepares
+ the index for asynchronous destruction.
+1. [Create a follow-up issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Synchronous%20Database%20Index)
+ to add a migration that destroys the index synchronously.
+1. In the merge request that prepares the asynchronous index removal, add a comment mentioning the follow-up issue.
+
+For example, to destroy an index using
the asynchronous index helpers:
```ruby
@@ -414,6 +425,7 @@ the asynchronous index helpers:
INDEX_NAME = 'index_ci_builds_on_some_column'
+# TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
prepare_async_index_removal :ci_builds, :some_column, name: INDEX_NAME
end
diff --git a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v13_8.png b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v13_8.png
deleted file mode 100644
index c2aa0689d65..00000000000
--- a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.png b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.png
new file mode 100644
index 00000000000..6839c675625
--- /dev/null
+++ b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v13_8.png b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v13_8.png
deleted file mode 100644
index 3828868965b..00000000000
--- a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.png b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.png
new file mode 100644
index 00000000000..c7942d1e36d
--- /dev/null
+++ b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_4.png b/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_4.png
deleted file mode 100644
index aae75b0736c..00000000000
--- a/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_9.png b/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_9.png
new file mode 100644
index 00000000000..80083e1819e
--- /dev/null
+++ b/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v15_9.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md
index bf25c7ef9bb..9a390364466 100644
--- a/doc/user/project/merge_requests/reviews/index.md
+++ b/doc/user/project/merge_requests/reviews/index.md
@@ -25,9 +25,9 @@ review merge requests in Visual Studio Code.
> [Introduced](https://gitlab.com/groups/gitlab-org/modelops/applied-ml/review-recommender/-/epics/3) in GitLab 15.4.
-GitLab can recommend reviewers with Suggested Reviewers. Using the changes in a merge request and a project's contribution graph, machine learning powered suggestions appear in the reviewer section of the right merge request sidebar.
+GitLab can suggest reviewers. Using the changes in a merge request and a project's contribution graph, machine learning suggestions appear in the reviewer section of the right sidebar.
-![Suggested Reviewers](img/suggested_reviewers_v15_4.png)
+![Suggested Reviewers](img/suggested_reviewers_v15_9.png)
This feature is currently in [Open Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta) behind a [feature flag](https://gitlab.com/gitlab-org/gitlab/-/issues/368356).
@@ -176,11 +176,11 @@ below the name of each suggested reviewer. [Code Owners](../../code_owners.md) a
This example shows reviewers and approval rules when creating a new merge request:
-![Reviewer approval rules in new/edit form](img/reviewer_approval_rules_form_v13_8.png)
+![Reviewer approval rules in new/edit form](img/reviewer_approval_rules_form_v15_9.png)
This example shows reviewers and approval rules in a merge request sidebar:
-![Reviewer approval rules in sidebar](img/reviewer_approval_rules_sidebar_v13_8.png)
+![Reviewer approval rules in sidebar](img/reviewer_approval_rules_sidebar_v15_9.png)
### Request a new review
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 05c1f491aee..b63d583f3d6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5540,6 +5540,9 @@ msgid_plural "%d Assignees"
msgstr[0] ""
msgstr[1] ""
+msgid "Assignee (optional)"
+msgstr ""
+
msgid "Assignee has no permissions"
msgstr ""
@@ -35109,6 +35112,9 @@ msgstr ""
msgid "Release|More information"
msgstr ""
+msgid "Release|Release %{createdRelease} has been successfully created."
+msgstr ""
+
msgid "Release|Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software."
msgstr ""
@@ -46370,9 +46376,6 @@ msgstr ""
msgid "ValueStreamEvent|Stop"
msgstr ""
-msgid "ValueStream|The Default Value Stream cannot be deleted"
-msgstr ""
-
msgid "Variable"
msgstr ""
@@ -49304,6 +49307,9 @@ msgstr[1] ""
msgid "access:"
msgstr ""
+msgid "add at least one file to the repository"
+msgstr ""
+
msgid "added %{emails}"
msgstr ""
diff --git a/qa/qa/page/component/wiki_sidebar.rb b/qa/qa/page/component/wiki_sidebar.rb
index dfb912a1d0b..7543b9655f9 100644
--- a/qa/qa/page/component/wiki_sidebar.rb
+++ b/qa/qa/page/component/wiki_sidebar.rb
@@ -20,6 +20,7 @@ module QA
base.view 'app/views/shared/wikis/_wiki_directory.html.haml' do
element :wiki_directory_content
+ element :wiki_dir_page_link
end
end
@@ -42,6 +43,10 @@ module QA
def has_directory?(directory)
has_element?(:wiki_directory_content, text: directory)
end
+
+ def has_dir_page?(dir_page)
+ has_element?(:wiki_dir_page_link, page_name: dir_page)
+ end
end
end
end
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index 76a1be647c3..d7a220bc83f 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -19,6 +19,7 @@ module QA
(respond_to?(:api_put_path) && respond_to?(:api_put_body))
end
+ # @return [String] the resource web url
def fabricate_via_api!
unless api_support?
raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
diff --git a/qa/qa/resource/project_web_hook.rb b/qa/qa/resource/project_web_hook.rb
index 8b806c42030..86e662932e1 100644
--- a/qa/qa/resource/project_web_hook.rb
+++ b/qa/qa/resource/project_web_hook.rb
@@ -16,7 +16,11 @@ module QA
confidential_note
].freeze
- attr_accessor :url, :enable_ssl, :id
+ attr_accessor :url, :enable_ssl
+
+ attribute :disabled_until
+ attribute :id
+ attribute :alert_status
attribute :project do
Project.fabricate_via_api! do |resource|
@@ -33,19 +37,28 @@ module QA
def initialize
@id = nil
@enable_ssl = false
+ @alert_status = nil
@url = nil
end
+ def fabricate_via_api!
+ resource_web_url = super
+
+ @id = api_response[:id]
+
+ resource_web_url
+ end
+
def resource_web_url(resource)
"/project/#{project.name}/~/hooks/##{resource[:id]}/edit"
end
def api_get_path
- "/projects/#{project.id}/hooks"
+ "#{api_post_path}/#{api_response[:id]}"
end
def api_post_path
- api_get_path
+ "/projects/#{project.id}/hooks"
end
def api_post_body
diff --git a/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb b/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb
index fb530967073..8439b881ed7 100644
--- a/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb
@@ -125,11 +125,48 @@ module QA
end
end
+ context 'when hook fails' do
+ let(:fail_mock) do
+ <<~YAML
+ - request:
+ method: POST
+ path: /default
+ response:
+ status: 404
+ headers:
+ Content-Type: text/plain
+ body: 'webhook failed'
+ YAML
+ end
+
+ let(:hook_trigger_times) { 5 }
+ let(:disabled_after) { 4 }
+
+ it 'hook is auto-disabled',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/389595' do
+ setup_webhook(fail_mock, issues: true) do |webhook, smocker|
+ hook_trigger_times.times do
+ Resource::Issue.fabricate_via_api! do |issue_init|
+ issue_init.project = webhook.project
+ end
+ end
+
+ expect { smocker.history(session).size }.to eventually_eq(disabled_after)
+ .within(max_duration: 30, sleep_interval: 2),
+ -> { "Should have #{disabled_after} events, got: #{smocker.history(session).size}" }
+
+ webhook.reload!
+
+ expect(webhook.alert_status).to eql('disabled')
+ end
+ end
+ end
+
private
- def setup_webhook(**event_args)
+ def setup_webhook(mock = Vendor::Smocker::SmockerApi::DEFAULT_MOCK, **event_args)
Service::DockerRun::Smocker.init(wait: 10) do |smocker|
- smocker.register(session: session)
+ smocker.register(mock, session: session)
webhook = Resource::ProjectWebHook.fabricate_via_api! do |hook|
hook.url = smocker.url
diff --git a/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
new file mode 100644
index 00000000000..246119a8118
--- /dev/null
+++ b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ValueStreamActions, type: :controller,
+feature_category: :planning_analytics do
+ subject(:controller) do
+ Class.new(ApplicationController) do
+ include Analytics::CycleAnalytics::ValueStreamActions
+
+ def call_namespace
+ namespace
+ end
+ end
+ end
+
+ describe '#namespace' do
+ it 'raises NotImplementedError' do
+ expect { controller.new.call_namespace }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb
index 5b434eb2011..a3a86138f18 100644
--- a/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb
+++ b/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb
@@ -30,6 +30,20 @@ RSpec.describe Projects::Analytics::CycleAnalytics::ValueStreamsController do
expect(json_response.first['name']).to eq('default')
end
+
+ # testing the authorize method within ValueStreamActions
+ context 'when issues and merge requests are disabled' do
+ it 'renders 404' do
+ project.project_feature.update!(
+ issues_access_level: ProjectFeature::DISABLED,
+ merge_requests_access_level: ProjectFeature::DISABLED
+ )
+
+ get :index, params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
context 'when user is not member of the project' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 86ca4d3bf1f..ceb3f803db5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -68,6 +68,72 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
end
+ context 'when add_prepared_state_to_mr feature flag on' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: true)
+ end
+
+ context 'when the merge request is not prepared' do
+ before do
+ merge_request.update!(prepared_at: nil, created_at: 10.minutes.ago)
+ end
+
+ it 'prepares the merge request' do
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+
+ go
+ end
+
+ context 'when the merge request was created less than 5 minutes ago' do
+ it 'does not prepare the merge request again' do
+ travel_to(4.minutes.from_now) do
+ merge_request.update!(created_at: Time.current - 4.minutes)
+
+ expect(NewMergeRequestWorker).not_to receive(:perform_async)
+
+ go
+ end
+ end
+ end
+
+ context 'when the merge request was created 5 minutes ago' do
+ it 'prepares the merge request' do
+ travel_to(6.minutes.from_now) do
+ merge_request.update!(created_at: Time.current - 6.minutes)
+
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+
+ go
+ end
+ end
+ end
+ end
+
+ context 'when the merge request is prepared' do
+ before do
+ merge_request.update!(prepared_at: Time.current, created_at: 10.minutes.ago)
+ end
+
+ it 'prepares the merge request' do
+ expect(NewMergeRequestWorker).not_to receive(:perform_async)
+
+ go
+ end
+ end
+ end
+
+ context 'when add_prepared_state_to_mr feature flag is off' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: false)
+ end
+
+ it 'does not prepare the merge request again' do
+ expect(NewMergeRequestWorker).not_to receive(:perform_async)
+
+ go
+ end
+ end
+
describe 'as html' do
it 'sets the endpoint_metadata_url' do
go
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 5c4eb6912c8..d7d8e634569 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -10,6 +10,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
+import { putCreateReleaseNotification } from '~/releases/release_notification_service';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
@@ -19,6 +20,8 @@ const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.r
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
const upcomingReleaseDocsPath = 'path/to/upcoming/release/docs';
+const projectPath = 'project/path';
+jest.mock('~/releases/release_notification_service');
describe('Release edit/new component', () => {
let wrapper;
@@ -32,6 +35,7 @@ describe('Release edit/new component', () => {
state = {
release,
isExistingRelease: true,
+ projectPath,
markdownDocsPath: 'path/to/markdown/docs',
releasesPagePath,
projectId: '8',
@@ -163,6 +167,13 @@ describe('Release edit/new component', () => {
expect(actions.saveRelease).toHaveBeenCalledTimes(1);
});
+
+ it('sets release created notification when the form is submitted', () => {
+ findForm().trigger('submit');
+ const releaseName = originalOneReleaseForEditingQueryResponse.data.project.release.name;
+ expect(putCreateReleaseNotification).toHaveBeenCalledTimes(1);
+ expect(putCreateReleaseNotification).toHaveBeenCalledWith(projectPath, releaseName);
+ });
});
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index c5cb8589ee8..efe72e8000a 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -5,12 +5,14 @@ import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/quer
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
+import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql';
jest.mock('~/flash');
+jest.mock('~/releases/release_notification_service');
Vue.use(VueApollo);
@@ -88,6 +90,11 @@ describe('Release show component', () => {
createComponent({ apolloProvider });
});
+ it('shows info notification on mount', () => {
+ expect(popCreateReleaseNotification).toHaveBeenCalledTimes(1);
+ expect(popCreateReleaseNotification).toHaveBeenCalledWith(MOCK_FULL_PATH);
+ });
+
it('builds a GraphQL with the expected variables', () => {
expect(queryHandler).toHaveBeenCalledTimes(1);
expect(queryHandler).toHaveBeenCalledWith({
diff --git a/spec/frontend/releases/release_notification_service_spec.js b/spec/frontend/releases/release_notification_service_spec.js
new file mode 100644
index 00000000000..2344d4b929a
--- /dev/null
+++ b/spec/frontend/releases/release_notification_service_spec.js
@@ -0,0 +1,57 @@
+import {
+ popCreateReleaseNotification,
+ putCreateReleaseNotification,
+} from '~/releases/release_notification_service';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('~/releases/release_notification_service', () => {
+ const projectPath = 'test-project-path';
+ const releaseName = 'test-release-name';
+
+ const storageKey = `createRelease:${projectPath}`;
+
+ describe('prepareCreateReleaseFlash', () => {
+ it('should set the session storage with project path key and release name value', () => {
+ putCreateReleaseNotification(projectPath, releaseName);
+
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(releaseName);
+ });
+ });
+
+ describe('showNotificationsIfPresent', () => {
+ describe('if notification is prepared', () => {
+ beforeEach(() => {
+ window.sessionStorage.setItem(storageKey, releaseName);
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should remove storage key', () => {
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(null);
+ });
+
+ it('should create a flash message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Release ${releaseName} has been successfully created.`,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+ });
+
+ describe('if notification is not prepared', () => {
+ beforeEach(() => {
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should not create a flash message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index eeee6747349..ca3b2d5f734 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -23,6 +23,8 @@ jest.mock('~/api/tags_api');
jest.mock('~/flash');
+jest.mock('~/releases/release_notification_service');
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
@@ -41,9 +43,12 @@ describe('Release edit/new actions', () => {
let releaseResponse;
let error;
+ const projectPath = 'test/project-path';
+
const setupState = (updates = {}) => {
state = {
...createState({
+ projectPath,
projectId: '18',
isExistingRelease: true,
tagName: releaseResponse.tag_name,
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
new file mode 100644
index 00000000000..fe87c4be9c3
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -0,0 +1,46 @@
+import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
+import { mergeRequestMenuGroup } from '../mock_data';
+
+describe('MergeRequestMenu component', () => {
+ let wrapper;
+
+ const findGlBadge = (at) => wrapper.findAllComponents(GlBadge).at(at);
+ const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findLink = () => wrapper.findByRole('link');
+
+ const createWrapper = () => {
+ wrapper = mountExtended(MergeRequestMenu, {
+ propsData: {
+ items: mergeRequestMenuGroup,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes the items to the disclosure dropdown', () => {
+ expect(findGlDisclosureDropdown().props('items')).toBe(mergeRequestMenuGroup);
+ });
+
+ it('renders item text and count in link', () => {
+ const { text, href, count } = mergeRequestMenuGroup[0].items[0];
+ expect(findLink().text()).toContain(text);
+ expect(findLink().text()).toContain(String(count));
+ expect(findLink().attributes('href')).toBe(href);
+ });
+
+ it('renders item count string in badge', () => {
+ const { count } = mergeRequestMenuGroup[0].items[0];
+ expect(findGlBadge(0).text()).toBe(String(count));
+ });
+
+ it('renders 0 string when count is empty', () => {
+ expect(findGlBadge(1).text()).toBe(String(0));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index d7e658a1451..eceb792c3db 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,6 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { sidebarData } from '../mock_data';
@@ -10,6 +11,7 @@ describe('UserBar component', () => {
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
+ const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(UserBar, {
@@ -33,12 +35,21 @@ describe('UserBar component', () => {
expect(findCreateMenu().props('groups')).toBe(sidebarData.create_new_menu_groups);
});
+ it('passes the "Merge request" menu groups to the merge_request_menu component', () => {
+ expect(findMergeRequestMenu().props('items')).toBe(sidebarData.merge_request_menu);
+ });
+
it('renders issues counter', () => {
expect(findCounter(0).props('count')).toBe(sidebarData.assigned_open_issues_count);
expect(findCounter(0).props('href')).toBe(sidebarData.issues_dashboard_path);
expect(findCounter(0).props('label')).toBe(__('Issues'));
});
+ it('renders merge requests counter', () => {
+ expect(findCounter(1).props('count')).toBe(sidebarData.total_merge_requests_count);
+ expect(findCounter(1).props('label')).toBe(__('Merge requests'));
+ });
+
it('renders todos counter', () => {
expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
expect(findCounter(2).props('href')).toBe('/dashboard/todos');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 379e4c2bffb..0194360cb57 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -39,15 +39,34 @@ export const createNewMenuGroups = [
},
];
+export const mergeRequestMenuGroup = [
+ {
+ name: 'Merge requests',
+ items: [
+ {
+ text: 'Assigned',
+ href: '/dashboard/merge_requests?assignee_username=root',
+ count: 4,
+ },
+ {
+ text: 'Review requests',
+ href: '/dashboard/merge_requests?reviewer_username=root',
+ count: 0,
+ },
+ ],
+ },
+];
+
export const sidebarData = {
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img_administrator',
assigned_open_issues_count: 1,
- assigned_open_merge_requests_count: 2,
todos_pending_count: 3,
issues_dashboard_path: 'path/to/issues',
+ total_merge_requests_count: 4,
create_new_menu_groups: createNewMenuGroups,
+ merge_request_menu: mergeRequestMenuGroup,
support_path: '/support',
display_whats_new: true,
};
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 4151789372b..ea8018d4413 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -285,6 +285,17 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ context 'with empty repository' do
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ it 'raises an error' do
+ expect(subject['errors'][0]['message']).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ 'href="http://localhost/help/user/project/repository/index.md#' \
+ 'add-files-to-a-repository">add at least one file to the ' \
+ 'repository</a> before using Security features.')
+ end
+ end
end
describe 'issue field' do
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index ecbc1597bdf..0622e73936d 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -54,8 +54,10 @@ RSpec.describe SidebarsHelper do
before do
allow(helper).to receive(:current_user) { user }
Rails.cache.write(['users', user.id, 'assigned_open_issues_count'], 1)
- Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 2)
+ Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 4)
+ Rails.cache.write(['users', user.id, 'review_requested_open_merge_requests_count'], 0)
Rails.cache.write(['users', user.id, 'todos_pending_count'], 3)
+ Rails.cache.write(['users', user.id, 'total_merge_requests_count'], 4)
end
it 'returns sidebar values from user', :use_clean_rails_memory_store_caching do
@@ -64,14 +66,34 @@ RSpec.describe SidebarsHelper do
username: user.username,
avatar_url: user.avatar_url,
assigned_open_issues_count: 1,
- assigned_open_merge_requests_count: 2,
todos_pending_count: 3,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
+ total_merge_requests_count: 4,
support_path: helper.support_url,
display_whats_new: helper.display_whats_new?
})
end
+ it 'returns "Merge requests" menu', :use_clean_rails_memory_store_caching do
+ expect(subject[:merge_request_menu]).to eq([
+ {
+ name: _('Merge requests'),
+ items: [
+ {
+ text: _('Assigned'),
+ href: merge_requests_dashboard_path(assignee_username: user.username),
+ count: 4
+ },
+ {
+ text: _('Review requests'),
+ href: merge_requests_dashboard_path(reviewer_username: user.username),
+ count: 0
+ }
+ ]
+ }
+ ])
+ end
+
it 'returns "Create new" menu groups without headers', :use_clean_rails_memory_store_caching do
expect(subject[:create_new_menu_groups]).to eq([
{
diff --git a/spec/initializers/memory_watchdog_spec.rb b/spec/initializers/memory_watchdog_spec.rb
index 92834c889c2..ef24da0071b 100644
--- a/spec/initializers/memory_watchdog_spec.rb
+++ b/spec/initializers/memory_watchdog_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe 'memory watchdog' do
+RSpec.describe 'memory watchdog', feature_category: :application_performance do
shared_examples 'starts configured watchdog' do |configure_monitor_method|
shared_examples 'configures and starts watchdog' do
it "correctly configures and starts watchdog", :aggregate_failures do
@@ -104,11 +104,7 @@ RSpec.describe 'memory watchdog' do
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
end
- it 'does not register life-cycle hook' do
- expect(Gitlab::Cluster::LifecycleEvents).not_to receive(:on_worker_start)
-
- run_initializer
- end
+ it_behaves_like 'starts configured watchdog', :configure_for_sidekiq
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 9fe8b960c8b..fd5d33a9b6f 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -5537,4 +5537,33 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
end
+
+ describe '#prepared?' do
+ subject(:merge_request) { build_stubbed(:merge_request, prepared_at: prepared_at) }
+
+ context 'when prepared_at is nil' do
+ let(:prepared_at) { nil }
+
+ it 'returns false' do
+ expect(merge_request.prepared?).to be_falsey
+ end
+ end
+
+ context 'when prepared_at is not nil' do
+ let(:prepared_at) { Time.current }
+
+ it 'returns true' do
+ expect(merge_request.prepared?).to be_truthy
+ end
+ end
+ end
+
+ describe 'prepare' do
+ it 'calls NewMergeRequestWorker' do
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+ .with(subject.id, subject.author_id)
+
+ subject.prepare
+ end
+ end
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index 44c6f6c9c1a..90ff42998ae 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -13,15 +13,20 @@ RSpec.describe WikiDirectory do
let_it_be(:toplevel1) { build(:wiki_page, title: 'aaa-toplevel1') }
let_it_be(:toplevel2) { build(:wiki_page, title: 'zzz-toplevel2') }
let_it_be(:toplevel3) { build(:wiki_page, title: 'zzz-toplevel3') }
+ let_it_be(:parent1) { build(:wiki_page, title: 'parent1') }
+ let_it_be(:parent2) { build(:wiki_page, title: 'parent2') }
let_it_be(:child1) { build(:wiki_page, title: 'parent1/child1') }
let_it_be(:child2) { build(:wiki_page, title: 'parent1/child2') }
let_it_be(:child3) { build(:wiki_page, title: 'parent2/child3') }
+ let_it_be(:subparent) { build(:wiki_page, title: 'parent1/subparent') }
let_it_be(:grandchild1) { build(:wiki_page, title: 'parent1/subparent/grandchild1') }
let_it_be(:grandchild2) { build(:wiki_page, title: 'parent1/subparent/grandchild2') }
it 'returns a nested array of entries' do
entries = described_class.group_pages(
- [toplevel1, toplevel2, toplevel3, child1, child2, child3, grandchild1, grandchild2].sort_by(&:title)
+ [toplevel1, toplevel2, toplevel3,
+ parent1, parent2, child1, child2, child3,
+ subparent, grandchild1, grandchild2].sort_by(&:title)
)
expect(entries).to match(
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
index 3dc79ef4d81..e437e1bbcb0 100644
--- a/spec/requests/api/graphql/issues_spec.rb
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -175,15 +175,21 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
end
context 'when fetching issues from multiple projects' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
post_query # warm-up
- control = ActiveRecord::QueryRecorder.new { post_query }
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_query }
+ expect_graphql_errors_to_be_empty
new_private_project = create(:project, :private).tap { |project| project.add_developer(current_user) }
create(:issue, project: new_private_project)
- expect { post_query }.not_to exceed_query_limit(control)
+ private_group = create(:group, :private).tap { |group| group.add_developer(current_user) }
+ private_project = create(:project, :private, group: private_group)
+ create(:issue, project: private_project)
+
+ expect { post_query }.not_to exceed_all_query_limit(control)
+ expect_graphql_errors_to_be_empty
end
end
diff --git a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
index ef77958fa60..e5f06824b9f 100644
--- a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
+++ b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GoogleCloud::FetchGoogleIpListService, :use_clean_rails_memory_store_caching,
-:clean_gitlab_redis_rate_limiting, feature_category: :continuous_integration do
+:clean_gitlab_redis_rate_limiting, feature_category: :build_artifacts do
include StubRequests
let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index f477b2166d9..f2823b1f0c7 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AfterCreateService do
+RSpec.describe MergeRequests::AfterCreateService, feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request) }
subject(:after_create_service) do
@@ -126,6 +126,17 @@ RSpec.describe MergeRequests::AfterCreateService do
end
end
+ it 'updates the prepared_at' do
+ # Need to reset the `prepared_at` since it can be already set in preceding tests.
+ merge_request.update!(prepared_at: nil)
+
+ freeze_time do
+ expect { execute_service }.to change { merge_request.prepared_at }
+ .from(nil)
+ .to(Time.current)
+ end
+ end
+
it 'increments the usage data counter of create event' do
counter = Gitlab::UsageDataCounters::MergeRequestCounter
diff --git a/spec/services/security/ci_configuration/sast_create_service_spec.rb b/spec/services/security/ci_configuration/sast_create_service_spec.rb
index 1e6dc367146..39c32567f3c 100644
--- a/spec/services/security/ci_configuration/sast_create_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb
@@ -24,7 +24,45 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_
include_examples 'services security ci configuration create service'
- context "when committing to the default branch", :aggregate_failures do
+ RSpec.shared_examples_for 'commits directly to the default branch' do
+ it 'commits directly to the default branch' do
+ expect(project).to receive(:default_branch).twice.and_return('master')
+
+ expect(result.status).to eq(:success)
+ expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
+ expect(result.payload[:branch]).to eq('master')
+ end
+ end
+
+ context 'when the repository is empty' do
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ context 'when initialize_with_sast is false' do
+ before do
+ project.add_developer(user)
+ end
+
+ let(:params) { { initialize_with_sast: false } }
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError)
+ end
+ end
+
+ context 'when initialize_with_sast is true' do
+ let(:params) { { initialize_with_sast: true } }
+
+ subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'commits directly to the default branch'
+ end
+ end
+
+ context 'when committing to the default branch', :aggregate_failures do
subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
let(:params) { {} }
@@ -33,17 +71,13 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_
project.add_developer(user)
end
- it "doesn't try to remove that branch on raised exceptions" do
+ it 'does not try to remove that branch on raised exceptions' do
expect(Files::MultiService).to receive(:new).and_raise(StandardError, '_exception_')
expect(project.repository).not_to receive(:rm_branch)
expect { result }.to raise_error(StandardError, '_exception_')
end
- it "commits directly to the default branch" do
- expect(result.status).to eq(:success)
- expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
- expect(result.payload[:branch]).to eq('master')
- end
+ it_behaves_like 'commits directly to the default branch'
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 54ac7f5548e..58411fc3c4f 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -1006,7 +1006,6 @@
- './ee/spec/helpers/nav/new_dropdown_helper_spec.rb'
- './ee/spec/helpers/nav/top_nav_helper_spec.rb'
- './ee/spec/helpers/notes_helper_spec.rb'
-- './ee/spec/helpers/paid_feature_callout_helper_spec.rb'
- './ee/spec/helpers/path_locks_helper_spec.rb'
- './ee/spec/helpers/preferences_helper_spec.rb'
- './ee/spec/helpers/prevent_forking_helper_spec.rb'
diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
index 716be8c6210..209be09c807 100644
--- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
@@ -160,6 +160,21 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
end
end
end
+
+ context 'when the project is empty' do
+ let(:params) { nil }
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ it 'returns an error' do
+ expect { result }.to raise_error { |error|
+ expect(error).to be_a(Gitlab::Graphql::Errors::MutationError)
+ expect(error.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ 'href="http://localhost/help/user/project/repository/index.md' \
+ '#add-files-to-a-repository">add at least one file to the repository' \
+ '</a> before using Security features.')
+ }
+ end
+ end
end
end
end
diff --git a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
index 96755b7292b..184c664f6dc 100644
--- a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
@@ -3,7 +3,10 @@
require 'spec_helper'
RSpec.describe ObjectStorage::CDN::GoogleCDN,
- :use_clean_rails_memory_store_caching, :use_clean_rails_redis_caching, :sidekiq_inline do
+ :use_clean_rails_memory_store_caching,
+ :use_clean_rails_redis_caching,
+ :sidekiq_inline,
+ feature_category: :build_artifacts do # the google cdn is currently only used by build artifacts
include StubRequests
let(:key) { SecureRandom.hex }
diff --git a/spec/uploaders/object_storage/cdn_spec.rb b/spec/uploaders/object_storage/cdn_spec.rb
index 2a447921a19..a64e7000855 100644
--- a/spec/uploaders/object_storage/cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ObjectStorage::CDN do
+RSpec.describe ObjectStorage::CDN, feature_category: :build_artifacts do
let(:cdn_options) do
{
'object_store' => {
diff --git a/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb b/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
index c0b32515d15..bdafc076465 100644
--- a/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
+++ b/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::FetchGoogleIpListWorker do
+RSpec.describe GoogleCloud::FetchGoogleIpListWorker, feature_category: :build_artifacts do
describe '#perform' do
it 'returns success' do
allow_next_instance_of(GoogleCloud::FetchGoogleIpListService) do |service|
diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb
index 358939a963a..a8e1c3f4bf1 100644
--- a/spec/workers/new_merge_request_worker_spec.rb
+++ b/spec/workers/new_merge_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NewMergeRequestWorker do
+RSpec.describe NewMergeRequestWorker, feature_category: :code_review_workflow do
describe '#perform' do
let(:worker) { described_class.new }
@@ -71,19 +71,64 @@ RSpec.describe NewMergeRequestWorker do
it_behaves_like 'a new merge request where the author cannot trigger notifications'
end
- context 'when everything is ok' do
+ include_examples 'an idempotent worker' do
let(:user) { create(:user) }
-
- it 'creates a new event record' do
- expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
- end
-
- it 'creates a notification for the mentioned user' do
- expect(Notify).to receive(:new_merge_request_email)
- .with(mentioned.id, merge_request.id, NotificationReason::MENTIONED)
- .and_return(double(deliver_later: true))
-
- worker.perform(merge_request.id, user.id)
+ let(:job_args) { [merge_request.id, user.id] }
+
+ context 'when everything is ok' do
+ it 'creates a new event record' do
+ expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
+ end
+
+ it 'creates a notification for the mentioned user' do
+ expect(Notify).to receive(:new_merge_request_email)
+ .with(mentioned.id, merge_request.id, NotificationReason::MENTIONED)
+ .and_return(double(deliver_later: true))
+
+ worker.perform(merge_request.id, user.id)
+ end
+
+ context 'when add_prepared_state_to_mr feature flag is off' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: false)
+ end
+
+ it 'calls the create service' do
+ expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+
+ context 'when add_prepared_state_to_mr feature flag is on' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: true)
+ end
+
+ context 'when the merge request is prepared' do
+ before do
+ merge_request.update!(prepared_at: Time.current)
+ end
+
+ it 'does not call the create service' do
+ expect(MergeRequests::AfterCreateService).not_to receive(:new)
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+
+ context 'when the merge request is not prepared' do
+ it 'calls the create service' do
+ expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+ end
end
end
end