Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-01 18:11:40 +00:00
parent 18d5458781
commit 3bfb19d99e
79 changed files with 1059 additions and 136 deletions

View File

@ -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"

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1 +1 @@
ba63ee19fb2dafe6f2ca5bca5d12a0b24837ce17
4e200026c03189abe2c60b071204340c4af5cf35

View File

@ -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);
}
},
},

View File

@ -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({

View File

@ -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);
}
};

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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;

View File

@ -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')

View File

@ -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!
private
def index
# FOSS users can only see the default value stream
value_streams = [Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)]
render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
def namespace
project.project_namespace
end
end

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -2935,7 +2935,7 @@
:urgency: :high
:resource_boundary: :cpu
:weight: 2
:idempotent: false
:idempotent: true
:tags: []
- :name: new_note
:worker_name: NewNoteWorker

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
37cc2c2eeb910333a45a18820a569d4263eb614bc138a6a0fe11d037bae045c3

View File

@ -0,0 +1 @@
3c6dd3b83bc6a1d9e94c93784e201d3e9114ef62070468a31abe9167ae111c35

View File

@ -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,

View File

@ -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.
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.
### Using the API
You can use the [Application settings API](../api/settings.md#change-application-settings) to configure inactive projects.
### Using the GitLab UI
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85575) in GitLab 15.1.
To configure inactive projects with the GitLab UI:
To configure deletion of inactive projects:
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. 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**.
### Configuration example
If you use these settings:
- **Delete inactive projects** enabled.
- **Delete inactive projects that exceed** set to `50`.
- **Delete project after** set to `12`.
- **Send warning email** set to `6`.
If a project is less than 50 MB, the project is not considered inactive.
If a project is more than 50 MB and it is inactive for:
- 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.

View File

@ -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

View File

@ -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
}
]
}
```

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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!"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`, () => {

View File

@ -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({

View File

@ -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);
});
});
});
});

View File

@ -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,

View File

@ -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));
});
});
});

View File

@ -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');

View File

@ -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,
};

View File

@ -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

View File

@ -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([
{

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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')) }

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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 }

View File

@ -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' => {

View File

@ -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|

View File

@ -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) }
let(:job_args) { [merge_request.id, user.id] }
it 'creates a new event record' do
expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
end
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))
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)
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