Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
18d5458781
commit
3bfb19d99e
|
|
@ -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"
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ba63ee19fb2dafe6f2ca5bca5d12a0b24837ce17
|
||||
4e200026c03189abe2c60b071204340c4af5cf35
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2935,7 +2935,7 @@
|
|||
:urgency: :high
|
||||
:resource_boundary: :cpu
|
||||
:weight: 2
|
||||
:idempotent: false
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: new_note
|
||||
:worker_name: NewNoteWorker
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
37cc2c2eeb910333a45a18820a569d4263eb614bc138a6a0fe11d037bae045c3
|
||||
|
|
@ -0,0 +1 @@
|
|||
3c6dd3b83bc6a1d9e94c93784e201d3e9114ef62070468a31abe9167ae111c35
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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.
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
This example shows reviewers and approval rules in a merge request sidebar:
|
||||
|
||||

|
||||

|
||||
|
||||
### Request a new review
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`, () => {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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' => {
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue