Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
487dd921ae
commit
4badf32da7
|
|
@ -64,7 +64,6 @@ Database/JsonbSizeLimit:
|
|||
- 'ee/app/models/package_metadata/affected_package.rb'
|
||||
- 'ee/app/models/package_metadata/package.rb'
|
||||
- 'ee/app/models/projects/xray_report.rb'
|
||||
- 'ee/app/models/remote_development/workspaces_agent_config.rb'
|
||||
- 'ee/app/models/sbom/occurrence.rb'
|
||||
- 'ee/app/models/sbom/source.rb'
|
||||
- 'ee/app/models/search/elastic/reindexing_task.rb'
|
||||
|
|
@ -82,7 +81,6 @@ Database/JsonbSizeLimit:
|
|||
- 'ee/app/models/security/vulnerability_management_policy_rule.rb'
|
||||
- 'ee/app/models/vulnerabilities/archived_record.rb'
|
||||
- 'ee/app/models/vulnerabilities/finding.rb'
|
||||
- 'ee/lib/remote_development/workspace_operations/desired_config.rb'
|
||||
- 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb'
|
||||
- 'lib/gitlab/background_migration/migrate_links_for_vulnerability_findings.rb'
|
||||
- 'lib/gitlab/background_migration/purge_stale_security_scans.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
7722e3619144b4e551afe08b52782278efd18920
|
||||
8ce0f90e5793c02397a896740d2317efbfd9cde3
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ export default {
|
|||
error: i18n.errors.playJob,
|
||||
mutation: playJobMutation,
|
||||
},
|
||||
stop: {
|
||||
dataName: 'jobPlay',
|
||||
error: i18n.errors.playJob,
|
||||
mutation: playJobMutation,
|
||||
},
|
||||
retry: {
|
||||
dataName: 'jobRetry',
|
||||
error: i18n.errors.retryJob,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { __, s__ } from '~/locale';
|
|||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
|
||||
import { isFinished } from '~/deployments/utils';
|
||||
import DeploymentStatusLink from './deployment_status_link.vue';
|
||||
import Commit from './commit.vue';
|
||||
|
||||
|
|
@ -99,7 +100,8 @@ export default {
|
|||
return this.ref?.refPath;
|
||||
},
|
||||
needsApproval() {
|
||||
return this.deployment.pendingApprovalCount > 0;
|
||||
const deploymentStatus = this.status ? this.status.toUpperCase() : '';
|
||||
return !isFinished({ status: deploymentStatus }) && this.deployment.pendingApprovalCount > 0;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export default {
|
|||
},
|
||||
i18n: {
|
||||
selectReviewer: __('Select reviewer'),
|
||||
unassign: __('Unassign'),
|
||||
unassign: __('Unassign all'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -21,10 +21,14 @@ module Groups
|
|||
respond_to do |format|
|
||||
format.json do
|
||||
serializer = GroupChildSerializer
|
||||
.new(current_user: current_user)
|
||||
.with_pagination(request, response)
|
||||
serializer.expand_hierarchy(parent) if params[:filter].present?
|
||||
render json: serializer.represent(children)
|
||||
.new(current_user: current_user)
|
||||
.with_pagination(request, response)
|
||||
|
||||
serializer.expand_hierarchy(parent) if expand_hierarchy?
|
||||
|
||||
render json: serializer.represent(children, {
|
||||
upto_preloaded_ancestors_only: expand_inactive_hierarchy?
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -58,9 +62,18 @@ module Groups
|
|||
archived: Gitlab::Utils.to_boolean(safe_params[:archived], default: safe_params[:archived]),
|
||||
not_aimed_for_deletion: Gitlab::Utils.to_boolean(safe_params[:not_aimed_for_deletion])
|
||||
)
|
||||
params_copy.delete(:active) unless filter_active?
|
||||
params_copy.delete(:active) unless Feature.enabled?(:group_descendants_active_filter, current_user)
|
||||
params_copy.compact
|
||||
end
|
||||
strong_memoize_attr :descendants_params
|
||||
|
||||
def expand_inactive_hierarchy?
|
||||
descendants_params[:active] == false
|
||||
end
|
||||
|
||||
def expand_hierarchy?
|
||||
descendants_params[:filter] || expand_inactive_hierarchy?
|
||||
end
|
||||
|
||||
def validate_per_page
|
||||
return unless params.key?(:per_page)
|
||||
|
|
@ -77,11 +90,5 @@ module Groups
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def filter_active?
|
||||
return false unless Feature.enabled?(:group_descendants_active_filter, current_user)
|
||||
|
||||
parent.active?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@
|
|||
# Used to find and filter all subgroups and projects of a passed parent group
|
||||
# visible to a specified user.
|
||||
#
|
||||
# When passing a `filter` param, the search is performed over all nested levels
|
||||
# of the `parent_group`. All ancestors for a search result are loaded
|
||||
#
|
||||
# Arguments:
|
||||
# current_user: The user for which the children should be visible
|
||||
# parent_group: The group to find children of
|
||||
|
|
@ -15,9 +12,14 @@
|
|||
# Supports all params that the `ProjectsFinder` and `GroupProjectsFinder`
|
||||
# support.
|
||||
#
|
||||
# active: boolean - filters for active descendants.
|
||||
# filter: string - is aliased to `search` for consistency with the frontend.
|
||||
# active: boolean - filters for active descendants. When `false`, the search is performed over
|
||||
# all nested levels of the `parent group` and all inactive ancestors are loaded.
|
||||
# filter: string - aliased to `search` for consistency with the frontend. When a filter is
|
||||
# passed, the search is performed over all nested levels of the `parent_group`.
|
||||
# All ancestors for a search result are loaded
|
||||
class GroupDescendantsFinder
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :current_user, :parent_group, :params
|
||||
|
||||
def initialize(parent_group:, current_user: nil, params: {})
|
||||
|
|
@ -34,7 +36,7 @@ class GroupDescendantsFinder
|
|||
.page(page)
|
||||
|
||||
preloaded_ancestors = []
|
||||
if params[:filter]
|
||||
if search_descendants?
|
||||
preloaded_ancestors |= ancestors_of_filtered_subgroups
|
||||
preloaded_ancestors |= ancestors_of_filtered_projects
|
||||
end
|
||||
|
|
@ -62,28 +64,11 @@ class GroupDescendantsFinder
|
|||
.execute
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def all_visible_descendant_groups
|
||||
groups_table = Group.arel_table
|
||||
visible_to_user = groups_table[:visibility_level]
|
||||
.in(Gitlab::VisibilityLevel.levels_for_user(current_user))
|
||||
|
||||
if current_user
|
||||
authorized_groups = GroupsFinder.new(current_user, all_available: false) # rubocop: disable CodeReuse/Finder
|
||||
.execute.arel.as('authorized')
|
||||
authorized_to_user = groups_table.project(1).from(authorized_groups)
|
||||
.where(authorized_groups[:id].eq(groups_table[:id]))
|
||||
.exists
|
||||
visible_to_user = visible_to_user.or(authorized_to_user)
|
||||
end
|
||||
|
||||
parent_group.descendants.where(visible_to_user)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def subgroups_matching_filter
|
||||
all_visible_descendant_groups
|
||||
.search(params[:filter])
|
||||
def descendant_groups
|
||||
descendants = parent_group.descendants
|
||||
descendants = by_visible_to_users(descendants)
|
||||
descendants = by_active(descendants)
|
||||
by_search(descendants)
|
||||
end
|
||||
|
||||
# When filtering we want all to preload all the ancestors upto the specified
|
||||
|
|
@ -97,7 +82,9 @@ class GroupDescendantsFinder
|
|||
# So when searching 'project', on the 'subgroup' page we want to preload
|
||||
# 'nested-group' but not 'subgroup' or 'root'
|
||||
def ancestors_of_groups(base_for_ancestors)
|
||||
Group.id_in(base_for_ancestors).self_and_ancestors(upto: parent_group.id)
|
||||
ancestors = Group.id_in(base_for_ancestors).self_and_ancestors(upto: parent_group.id)
|
||||
ancestors = ancestors.self_or_ancestors_inactive if inactive?
|
||||
ancestors
|
||||
end
|
||||
|
||||
def ancestors_of_filtered_projects
|
||||
|
|
@ -116,8 +103,8 @@ class GroupDescendantsFinder
|
|||
def subgroups
|
||||
# When filtering subgroups, we want to find all matches within the tree of
|
||||
# descendants to show to the user
|
||||
groups = if params[:filter]
|
||||
subgroups_matching_filter
|
||||
groups = if search_descendants?
|
||||
descendant_groups
|
||||
else
|
||||
direct_child_groups
|
||||
end
|
||||
|
|
@ -133,20 +120,22 @@ class GroupDescendantsFinder
|
|||
|
||||
# Finds all projects nested under `parent_group` or any of its descendant
|
||||
# groups
|
||||
def projects_matching_filter
|
||||
def descendant_projects
|
||||
projects_nested_in_group = Project.in_namespace(parent_group.self_and_descendants.as_ids)
|
||||
params_with_search = params.merge(search: params[:filter])
|
||||
|
||||
finder_params = params.dup
|
||||
finder_params[:search] = params[:filter] if params[:filter]
|
||||
|
||||
ProjectsFinder.new( # rubocop:disable CodeReuse/Finder
|
||||
params: params_with_search,
|
||||
params: finder_params,
|
||||
current_user: current_user,
|
||||
project_ids_relation: projects_nested_in_group
|
||||
).execute
|
||||
end
|
||||
|
||||
def projects
|
||||
projects = if params[:filter]
|
||||
projects_matching_filter
|
||||
projects = if search_descendants?
|
||||
descendant_projects
|
||||
else
|
||||
direct_child_projects
|
||||
end
|
||||
|
|
@ -177,4 +166,51 @@ class GroupDescendantsFinder
|
|||
def page
|
||||
params[:page].to_i
|
||||
end
|
||||
|
||||
# Filters group descendants to only include those visible to the current user.
|
||||
#
|
||||
# This method applies visibility filtering based on two criteria:
|
||||
# 1. Groups with visibility level accessible to the current user
|
||||
# 2. Groups where the user has explicit authorization (if authenticated)
|
||||
#
|
||||
# @param descendants [ActiveRecord::Relation<Group>] The collection of group descendants to filter
|
||||
# @return [ActiveRecord::Relation<Group>] Filtered descendants visible to the current user
|
||||
# rubocop: disable CodeReuse/ActiveRecord -- Needs specialized queries for optimization
|
||||
def by_visible_to_users(descendants)
|
||||
groups_table = Group.arel_table
|
||||
visible_to_user = groups_table[:visibility_level]
|
||||
.in(Gitlab::VisibilityLevel.levels_for_user(current_user))
|
||||
|
||||
if current_user
|
||||
authorized_groups = GroupsFinder.new(current_user, all_available: false) # rubocop: disable CodeReuse/Finder
|
||||
.execute.arel.as('authorized')
|
||||
authorized_to_user = groups_table.project(1).from(authorized_groups)
|
||||
.where(authorized_groups[:id].eq(groups_table[:id]))
|
||||
.exists
|
||||
visible_to_user = visible_to_user.or(authorized_to_user)
|
||||
end
|
||||
|
||||
descendants.where(visible_to_user)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def by_active(descendants)
|
||||
return descendants if params[:active].nil?
|
||||
|
||||
params[:active] ? descendants.self_and_ancestors_active : descendants.self_or_ancestors_inactive
|
||||
end
|
||||
|
||||
def by_search(descendants)
|
||||
return descendants unless params[:filter]
|
||||
|
||||
descendants.search(params[:filter])
|
||||
end
|
||||
|
||||
def inactive?
|
||||
params[:active] == false
|
||||
end
|
||||
|
||||
def search_descendants?
|
||||
params[:filter].present? || inactive?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -136,6 +136,12 @@ module Types
|
|||
null: true,
|
||||
description: 'Indicates the archived status of the project.'
|
||||
|
||||
field :marked_for_deletion, GraphQL::Types::Boolean,
|
||||
null: true,
|
||||
description: 'Indicates if the project or any ancestor is scheduled for deletion.',
|
||||
method: :scheduled_for_deletion_in_hierarchy_chain?,
|
||||
experiment: { milestone: '18.1' }
|
||||
|
||||
field :marked_for_deletion_on, ::Types::TimeType,
|
||||
null: true,
|
||||
description: 'Date when project was scheduled to be deleted.',
|
||||
|
|
|
|||
|
|
@ -4,20 +4,30 @@ module GroupDescendant
|
|||
# Returns the hierarchy of a project or group in the from of a hash upto a
|
||||
# given top.
|
||||
#
|
||||
# Options:
|
||||
# upto_preloaded_ancestors_only: boolean - When `true`, the hierarchy expansions stops at the
|
||||
# highest level preloaded ancestor. The hierarchy isn't
|
||||
# guaranteed to reach the `hierarchy_top`.
|
||||
#
|
||||
# > project.hierarchy
|
||||
# => { parent_group => { child_group => project } }
|
||||
def hierarchy(hierarchy_top = nil, preloaded = nil)
|
||||
def hierarchy(hierarchy_top = nil, preloaded = nil, opts = {})
|
||||
preloaded ||= ancestors_upto(hierarchy_top)
|
||||
expand_hierarchy_for_child(self, self, hierarchy_top, preloaded)
|
||||
expand_hierarchy_for_child(self, self, hierarchy_top, preloaded, opts)
|
||||
end
|
||||
|
||||
# Merges all hierarchies of the given groups or projects into an array of
|
||||
# hashes. All ancestors need to be loaded into the given `descendants` to avoid
|
||||
# queries down the line.
|
||||
#
|
||||
# Options:
|
||||
# upto_preloaded_ancestors_only: boolean - When `true`, the hierarchy expansions stops at the
|
||||
# highest level preloaded ancestor. The hierarchy isn't
|
||||
# guaranteed to reach the `hierarchy_top`.
|
||||
#
|
||||
# > GroupDescendant.merge_hierarchy([project, child_group, child_group2, parent])
|
||||
# => { parent => [{ child_group => project}, child_group2] }
|
||||
def self.build_hierarchy(descendants, hierarchy_top = nil)
|
||||
def self.build_hierarchy(descendants, hierarchy_top = nil, opts = {})
|
||||
descendants = Array.wrap(descendants).uniq
|
||||
return [] if descendants.empty?
|
||||
|
||||
|
|
@ -26,7 +36,7 @@ module GroupDescendant
|
|||
end
|
||||
|
||||
all_hierarchies = descendants.map do |descendant|
|
||||
descendant.hierarchy(hierarchy_top, descendants)
|
||||
descendant.hierarchy(hierarchy_top, descendants, opts)
|
||||
end
|
||||
|
||||
Gitlab::Utils::MergeHash.merge(all_hierarchies)
|
||||
|
|
@ -34,39 +44,50 @@ module GroupDescendant
|
|||
|
||||
private
|
||||
|
||||
def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded)
|
||||
def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded, opts = {})
|
||||
parent = hierarchy_top if hierarchy_top && child.parent_id == hierarchy_top.id
|
||||
parent ||= preloaded.detect do |possible_parent|
|
||||
possible_parent.is_a?(Group) && possible_parent.id == child.parent_id
|
||||
end
|
||||
|
||||
if parent.nil? && !child.parent_id.nil?
|
||||
parent = child.parent
|
||||
|
||||
exception = ArgumentError.new <<~MSG
|
||||
Parent was not preloaded for child when rendering group hierarchy.
|
||||
This error is not user facing, but causes a +1 query.
|
||||
MSG
|
||||
exception.set_backtrace(caller)
|
||||
|
||||
extras = {
|
||||
parent: parent.inspect,
|
||||
child: child.inspect,
|
||||
preloaded: preloaded.map(&:full_path),
|
||||
issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/49404'
|
||||
}
|
||||
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, extras)
|
||||
end
|
||||
|
||||
if parent.nil? && hierarchy_top.present?
|
||||
raise ArgumentError, _('specified top is not part of the tree')
|
||||
unless opts[:upto_preloaded_ancestors_only]
|
||||
parent ||= load_parent!(child, preloaded)
|
||||
validate_hierarchy_top_in_tree!(parent, hierarchy_top)
|
||||
end
|
||||
|
||||
if parent && parent != hierarchy_top
|
||||
expand_hierarchy_for_child(parent, { parent => hierarchy }, hierarchy_top, preloaded)
|
||||
expand_hierarchy_for_child(parent, { parent => hierarchy }, hierarchy_top, preloaded, opts)
|
||||
else
|
||||
hierarchy
|
||||
end
|
||||
end
|
||||
|
||||
def load_parent!(child, preloaded)
|
||||
return if child.parent_id.nil?
|
||||
|
||||
parent = child.parent
|
||||
|
||||
exception = ArgumentError.new <<~MSG
|
||||
Parent was not preloaded for child when rendering group hierarchy.
|
||||
This error is not user facing, but causes a +1 query.
|
||||
MSG
|
||||
exception.set_backtrace(caller)
|
||||
|
||||
extras = {
|
||||
parent: parent.inspect,
|
||||
child: child.inspect,
|
||||
preloaded: preloaded.map(&:full_path),
|
||||
issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/49404'
|
||||
}
|
||||
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, extras)
|
||||
|
||||
parent
|
||||
end
|
||||
|
||||
def validate_hierarchy_top_in_tree!(parent, hierarchy_top)
|
||||
return if parent.present? || hierarchy_top.nil?
|
||||
|
||||
raise ArgumentError, _('specified top is not part of the tree')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class GroupChildSerializer < BaseSerializer
|
|||
if children.is_a?(GroupDescendant)
|
||||
represent_hierarchy(children.hierarchy(hierarchy_root), opts).first
|
||||
else
|
||||
hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root)
|
||||
hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root, opts)
|
||||
# When an array was passed, we always want to represent an array.
|
||||
# Even if the hierarchy only contains one element
|
||||
represent_hierarchy(Array.wrap(hierarchies), opts)
|
||||
|
|
|
|||
|
|
@ -9,24 +9,7 @@ description: Used to store information of the exported files containing the data
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59976
|
||||
milestone: '13.12'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
project_id:
|
||||
references: projects
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: export_id
|
||||
table: bulk_import_exports
|
||||
sharding_key: project_id
|
||||
belongs_to: export
|
||||
group_id:
|
||||
references: namespaces
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: export_id
|
||||
table: bulk_import_exports
|
||||
sharding_key: group_id
|
||||
belongs_to: export
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
group_id: namespaces
|
||||
table_size: small
|
||||
desired_sharding_key_migration_job_name:
|
||||
- BackfillBulkImportExportUploadsProjectId
|
||||
- BackfillBulkImportExportUploadsGroupId
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ description: Persists information about on-call rotation participants
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49058
|
||||
milestone: '13.7'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
project_id:
|
||||
references: projects
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: oncall_rotation_id
|
||||
table: incident_management_oncall_rotations
|
||||
sharding_key: project_id
|
||||
belongs_to: rotation
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
desired_sharding_key_migration_job_name: BackfillIncidentManagementOncallParticipantsProjectId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIncidentManagementOncallParticipantsProjectIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :incident_management_oncall_participants, :project_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :incident_management_oncall_participants, :project_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMultiColumnNotNullConstraintToBulkImportExportUploads < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_multi_column_not_null_constraint(:bulk_import_export_uploads, :project_id, :group_id)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_multi_column_not_null_constraint(:bulk_import_export_uploads, :project_id, :group_id)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
406d7ba5413e155fcaa64ac630472b88ba75d6d9413a161f1cd4fb59521880af
|
||||
|
|
@ -0,0 +1 @@
|
|||
4c8cdcd364c8eb3fc83b0fde13039de700f0e58003c508ddbe662a0a76d9977d
|
||||
|
|
@ -10745,7 +10745,8 @@ CREATE TABLE bulk_import_export_uploads (
|
|||
batch_id bigint,
|
||||
project_id bigint,
|
||||
group_id bigint,
|
||||
CONSTRAINT check_5add76239d CHECK ((char_length(export_file) <= 255))
|
||||
CONSTRAINT check_5add76239d CHECK ((char_length(export_file) <= 255)),
|
||||
CONSTRAINT check_e1d215df28 CHECK ((num_nonnulls(group_id, project_id) = 1))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE bulk_import_export_uploads_id_seq
|
||||
|
|
@ -15637,7 +15638,8 @@ CREATE TABLE incident_management_oncall_participants (
|
|||
color_palette smallint NOT NULL,
|
||||
color_weight smallint NOT NULL,
|
||||
is_removed boolean DEFAULT false NOT NULL,
|
||||
project_id bigint
|
||||
project_id bigint,
|
||||
CONSTRAINT check_d53b689825 CHECK ((project_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE incident_management_oncall_participants_id_seq
|
||||
|
|
|
|||
|
|
@ -15140,6 +15140,29 @@ The connection type for [`ComplianceFramework`](#complianceframework).
|
|||
| <a id="complianceframeworkconnectionnodes"></a>`nodes` | [`[ComplianceFramework]`](#complianceframework) | A list of nodes. |
|
||||
| <a id="complianceframeworkconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
#### `ComplianceFrameworkCoverageDetailConnection`
|
||||
|
||||
The connection type for [`ComplianceFrameworkCoverageDetail`](#complianceframeworkcoveragedetail).
|
||||
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="complianceframeworkcoveragedetailconnectionedges"></a>`edges` | [`[ComplianceFrameworkCoverageDetailEdge]`](#complianceframeworkcoveragedetailedge) | A list of edges. |
|
||||
| <a id="complianceframeworkcoveragedetailconnectionnodes"></a>`nodes` | [`[ComplianceFrameworkCoverageDetail]`](#complianceframeworkcoveragedetail) | A list of nodes. |
|
||||
| <a id="complianceframeworkcoveragedetailconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
#### `ComplianceFrameworkCoverageDetailEdge`
|
||||
|
||||
The edge type for [`ComplianceFrameworkCoverageDetail`](#complianceframeworkcoveragedetail).
|
||||
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="complianceframeworkcoveragedetailedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||
| <a id="complianceframeworkcoveragedetailedgenode"></a>`node` | [`ComplianceFrameworkCoverageDetail`](#complianceframeworkcoveragedetail) | The item at the end of the edge. |
|
||||
|
||||
#### `ComplianceFrameworkEdge`
|
||||
|
||||
The edge type for [`ComplianceFramework`](#complianceframework).
|
||||
|
|
@ -24478,6 +24501,18 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="complianceframeworkpipelineexecutionschedulepoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="complianceframeworkpipelineexecutionschedulepoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
|
||||
|
||||
### `ComplianceFrameworkCoverageDetail`
|
||||
|
||||
Framework coverage details for a specific compliance framework.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="complianceframeworkcoveragedetailcoveredcount"></a>`coveredCount` | [`Int!`](#int) | Number of projects covered by the framework. |
|
||||
| <a id="complianceframeworkcoveragedetailid"></a>`id` | [`ID!`](#id) | ID of the framework. |
|
||||
| <a id="complianceframeworkcoveragedetailname"></a>`name` | [`String!`](#string) | Name of the framework. |
|
||||
|
||||
### `ComplianceFrameworkCoverageSummary`
|
||||
|
||||
Compliance framework Coverage summary for a group.
|
||||
|
|
@ -28198,6 +28233,7 @@ GPG signature for a signed commit.
|
|||
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
|
||||
| <a id="groupcicdsettings"></a>`ciCdSettings` {{< icon name="warning-solid" >}} | [`CiCdSettings`](#cicdsettings) | **Introduced** in GitLab 17.9. **Status**: Experiment. Namespace CI/CD settings for the namespace. |
|
||||
| <a id="groupcomplianceframeworkcoveragesummary"></a>`complianceFrameworkCoverageSummary` {{< icon name="warning-solid" >}} | [`ComplianceFrameworkCoverageSummary`](#complianceframeworkcoveragesummary) | **Introduced** in GitLab 18.1. **Status**: Experiment. Summary of compliance framework coverage in a group and its subgroups. |
|
||||
| <a id="groupcomplianceframeworkscoveragedetails"></a>`complianceFrameworksCoverageDetails` {{< icon name="warning-solid" >}} | [`ComplianceFrameworkCoverageDetailConnection`](#complianceframeworkcoveragedetailconnection) | **Introduced** in GitLab 18.1. **Status**: Experiment. Detailed compliance framework coverage for each framework in the group. |
|
||||
| <a id="groupcompliancerequirementcontrolcoverage"></a>`complianceRequirementControlCoverage` {{< icon name="warning-solid" >}} | [`RequirementControlCoverage`](#requirementcontrolcoverage) | **Introduced** in GitLab 18.1. **Status**: Experiment. Compliance control status summary showing count of passed, failed, and pending controls. |
|
||||
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
|
||||
| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
|
||||
|
|
@ -35983,6 +36019,7 @@ Project-level settings for product analytics provider.
|
|||
| <a id="projectlanguages"></a>`languages` | [`[RepositoryLanguage!]`](#repositorylanguage) | Programming languages used in the project. |
|
||||
| <a id="projectlastactivityat"></a>`lastActivityAt` | [`Time`](#time) | Timestamp of the project last activity. |
|
||||
| <a id="projectlfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if the project has Large File Storage (LFS) enabled. |
|
||||
| <a id="projectmarkedfordeletion"></a>`markedForDeletion` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Introduced** in GitLab 18.1. **Status**: Experiment. Indicates if the project or any ancestor is scheduled for deletion. |
|
||||
| <a id="projectmarkedfordeletionon"></a>`markedForDeletionOn` {{< icon name="warning-solid" >}} | [`Time`](#time) | **Introduced** in GitLab 16.10. **Status**: Experiment. Date when project was scheduled to be deleted. |
|
||||
| <a id="projectmaxaccesslevel"></a>`maxAccessLevel` | [`AccessLevel!`](#accesslevel) | Maximum access level of the current user in the project. |
|
||||
| <a id="projectmergecommittemplate"></a>`mergeCommitTemplate` | [`String`](#string) | Template used to create merge commit message in merge requests. |
|
||||
|
|
@ -43645,6 +43682,7 @@ AI features that can be configured through the Model Selection feature settings.
|
|||
| <a id="aimodelselectionfeaturesduo_chat_write_tests"></a>`DUO_CHAT_WRITE_TESTS` | Duo chat write test feature setting. |
|
||||
| <a id="aimodelselectionfeaturesgenerate_commit_message"></a>`GENERATE_COMMIT_MESSAGE` | Generate commit message feature setting. |
|
||||
| <a id="aimodelselectionfeaturesresolve_vulnerability"></a>`RESOLVE_VULNERABILITY` | Resolve vulnerability feature setting. |
|
||||
| <a id="aimodelselectionfeaturesreview_merge_request"></a>`REVIEW_MERGE_REQUEST` | Review merge request feature setting. |
|
||||
| <a id="aimodelselectionfeaturessummarize_new_merge_request"></a>`SUMMARIZE_NEW_MERGE_REQUEST` | Summarize new merge request feature setting. |
|
||||
| <a id="aimodelselectionfeaturessummarize_review"></a>`SUMMARIZE_REVIEW` | Summarize review feature setting. |
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ the most out of GitLab.
|
|||
| Topic | Description | Good for beginners |
|
||||
|-------|-------------|--------------------|
|
||||
| [Make your first Git commit](make_first_git_commit/_index.md) | Create a project, edit a file, and commit changes to a Git repository from the command line. | {{< icon name="star" >}} |
|
||||
| [Start using Git on the command line](../topics/git/commands.md) | Learn how to set up Git, clone repositories, and work with branches. | {{< icon name="star" >}} |
|
||||
| [Introduction to Git](https://university.gitlab.com/courses/introduction-to-git) | Learn the basics of Git in this self-paced course. | {{< icon name="star" >}} |
|
||||
| [Take advantage of Git rebase](https://about.gitlab.com/blog/2022/10/06/take-advantage-of-git-rebase/) | Learn how to use the `rebase` command in your workflow. | |
|
||||
| [Update Git commit messages](update_commit_messages/_index.md) | Learn how to update commit messages and push the changes to GitLab. | |
|
||||
| [Update Git remote URLs](update_git_remote_url/_index.md) | Learn how to update the Git remote URLs in your local project when they have changed. | |
|
||||
| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF of common Git commands. | |
|
||||
| [Git-ing started with Git](https://www.youtube.com/watch?v=Ce5nz5n41z4) | Git basics video tutorial. | |
|
||||
| [GitLab source code management walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM) | GitLab workflow video tutorial. | |
|
||||
|
|
|
|||
|
|
@ -95,3 +95,6 @@ Your code goes through a pre-scan security workflow when using GitLab Duo:
|
|||
|
||||
When you are using [GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md)
|
||||
and the self-hosted AI gateway, you do not share any data with GitLab.
|
||||
|
||||
GitLab Self-Managed administrators can use [Service Ping](../../administration/settings/usage_statistics.md#service-ping)
|
||||
to send usage statistics to GitLab. This is separate to the [telemetry data](#telemetry).
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ If you do not select a specific LLM, the AI-native features use the GitLab-selec
|
|||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
To maintain optimal performance and reliability, GitLab might change the default LLM without notifying the user.
|
||||
To maintain optimal performance and reliability, GitLab might change the default LLM without notifying the user. GitLab does not change non-default LLMs that have been explicitly selected.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -304,6 +304,14 @@ To install the Helm chart for the GitLab workspaces proxy:
|
|||
|
||||
1. Install the chart:
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Before chart version 0.1.16, the Helm chart installation created secrets automatically.
|
||||
If you're upgrading from a version earlier than 0.1.16,
|
||||
[create the required Kubernetes secrets](#create-kubernetes-secrets) before running the upgrade command.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
```shell
|
||||
helm repo update
|
||||
|
||||
|
|
|
|||
|
|
@ -65501,7 +65501,7 @@ msgstr ""
|
|||
msgid "Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unassign"
|
||||
msgid "Unassign all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unassign from commenting user"
|
||||
|
|
|
|||
|
|
@ -253,86 +253,103 @@ RSpec.describe Groups::ChildrenController, feature_category: :groups_and_project
|
|||
context 'with active parameter' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let_it_be(:active_child) { create(:group, parent: group) }
|
||||
let_it_be(:active_subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:active_project) { create(:project, :public, group: group) }
|
||||
|
||||
let_it_be(:marked_for_deletion_child) { create(:group_with_deletion_schedule, parent: group) }
|
||||
let_it_be(:marked_for_deletion_project) do
|
||||
create(:project, :public, group: group, marked_for_deletion_at: Date.current)
|
||||
end
|
||||
let_it_be(:inactive_subgroup) { create(:group, :archived, parent: group) }
|
||||
let_it_be(:inactive_project) { create(:project, :archived, :public, group: group) }
|
||||
|
||||
let_it_be(:archived_project) { create(:project, :archived, :public, group: group) }
|
||||
let_it_be(:archived_child) do
|
||||
create(:group, parent: group, namespace_settings: create(:namespace_settings, archived: true))
|
||||
end
|
||||
subject(:make_request) { get :index, params: { group_id: group.to_param, active: active_param }, format: :json }
|
||||
|
||||
shared_examples 'endpoint that returns all child' do
|
||||
it 'returns all child', :aggregate_failures do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response))
|
||||
.to contain_exactly(
|
||||
active_child.id,
|
||||
marked_for_deletion_child.id,
|
||||
marked_for_deletion_project.id,
|
||||
archived_child.id,
|
||||
archived_project.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when true' do
|
||||
subject(:make_request) { get :index, params: { group_id: group.to_param, active: true }, format: :json }
|
||||
|
||||
it 'returns active child', :aggregate_failures do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response)).to contain_exactly(active_child.id)
|
||||
end
|
||||
|
||||
context 'when group_descendants_active_filter flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(group_descendants_active_filter: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'endpoint that returns all child'
|
||||
end
|
||||
|
||||
context 'when group is inactive' do
|
||||
let_it_be(:deletion_schedule) { create(:group_deletion_schedule, group: group) }
|
||||
|
||||
it_behaves_like 'endpoint that returns all child'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when false' do
|
||||
subject(:make_request) { get :index, params: { group_id: group.to_param, active: false }, format: :json }
|
||||
|
||||
it 'returns inactive child', :aggregate_failures do
|
||||
shared_examples 'request with no parameter' do
|
||||
it 'returns direct child', :aggregate_failures do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response)).to contain_exactly(
|
||||
marked_for_deletion_child.id,
|
||||
marked_for_deletion_project.id,
|
||||
archived_child.id,
|
||||
archived_project.id
|
||||
active_subgroup.id,
|
||||
active_project.id,
|
||||
inactive_subgroup.id,
|
||||
inactive_project.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group_descendants_active_filter flag is disabled' do
|
||||
context 'when true' do
|
||||
let_it_be(:active_param) { true }
|
||||
|
||||
it 'returns active direct children', :aggregate_failures do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response)).to include(active_subgroup.id, active_project.id)
|
||||
expect(descendant_ids(json_response)).not_to include(inactive_subgroup.id, inactive_project.id)
|
||||
end
|
||||
|
||||
context 'when `group_descendants_active_filter` flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(group_descendants_active_filter: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'endpoint that returns all child'
|
||||
it_behaves_like 'request with no parameter'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when false' do
|
||||
let_it_be(:active_param) { false }
|
||||
|
||||
it 'returns inactive direct children', :aggregate_failures do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response)).to include(inactive_subgroup.id, inactive_project.id)
|
||||
expect(descendant_ids(json_response)).not_to include(active_subgroup.id, active_project.id)
|
||||
end
|
||||
|
||||
context 'when group is inactive' do
|
||||
let_it_be(:deletion_schedule) { create(:group_deletion_schedule, group: group) }
|
||||
context 'when active subgroup has children' do
|
||||
let_it_be(:active_descendant_group) { create(:group, parent: active_subgroup) }
|
||||
let_it_be(:active_descendant_project) { create(:project, :public, group: active_subgroup) }
|
||||
|
||||
it_behaves_like 'endpoint that returns all child'
|
||||
let_it_be(:inactive_descendant_group) { create(:group, :archived, parent: active_subgroup) }
|
||||
let_it_be(:inactive_descendant_project) { create(:project, :public, :archived, group: active_subgroup) }
|
||||
|
||||
it 'returns inactive descendants', :aggregate_failures do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response))
|
||||
.to include(inactive_descendant_group.id, inactive_descendant_project.id)
|
||||
expect(descendant_ids(json_response))
|
||||
.not_to include(active_descendant_group.id, active_descendant_project.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inactive subgroup has children' do
|
||||
let_it_be(:active_descendant_group) { create(:group, parent: inactive_subgroup) }
|
||||
let_it_be(:active_descendant_project) { create(:project, :public, group: inactive_subgroup) }
|
||||
|
||||
let_it_be(:inactive_descendant_group) { create(:group, :archived, parent: inactive_subgroup) }
|
||||
let_it_be(:inactive_descendant_project) { create(:project, :public, :archived, group: inactive_subgroup) }
|
||||
|
||||
it 'returns all descendants' do
|
||||
make_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(descendant_ids(json_response)).to include(
|
||||
active_descendant_group.id,
|
||||
active_descendant_project.id,
|
||||
inactive_descendant_group.id,
|
||||
inactive_descendant_project.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `group_descendants_active_filter` flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(group_descendants_active_filter: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'request with no parameter'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -75,25 +75,76 @@ RSpec.describe GroupDescendantsFinder, feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
context 'with active parameter' do
|
||||
let_it_be(:active_child) { create(:group, parent: group) }
|
||||
let_it_be(:active_subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:active_project) { create(:project, group: group) }
|
||||
|
||||
let_it_be(:inactive_child) { create(:group_with_deletion_schedule, parent: group) }
|
||||
let_it_be(:inactive_subgroup) { create(:group, :archived, parent: group) }
|
||||
let_it_be(:inactive_project) { create(:project, :archived, group: group) }
|
||||
|
||||
context 'when parameter is true' do
|
||||
subject { finder.execute }
|
||||
|
||||
context 'when true' do
|
||||
let(:params) { { active: true } }
|
||||
|
||||
it 'returns active children' do
|
||||
expect(finder.execute).to contain_exactly(active_child, active_project)
|
||||
is_expected.to contain_exactly(active_subgroup, active_project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when parameter is false' do
|
||||
context 'when false' do
|
||||
let(:params) { { active: false } }
|
||||
|
||||
it 'returns inactive children' do
|
||||
expect(finder.execute).to contain_exactly(inactive_child, inactive_project)
|
||||
is_expected.to include(inactive_subgroup, inactive_project)
|
||||
is_expected.not_to include(active_subgroup, active_project)
|
||||
end
|
||||
|
||||
context 'when subgroup is inactive' do
|
||||
let_it_be(:inactive_subgroup_subgroup) { create(:group, parent: inactive_subgroup) }
|
||||
let_it_be(:inactive_subgroup_project) { create(:project, group: inactive_subgroup) }
|
||||
|
||||
it 'returns all children' do
|
||||
is_expected.to include(inactive_subgroup_subgroup, inactive_subgroup_project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subgroup is active' do
|
||||
let_it_be(:active_subgroup_active_subgroup) { create(:group, parent: active_subgroup) }
|
||||
let_it_be(:active_subgroup_active_project) { create(:project, group: active_subgroup) }
|
||||
|
||||
let_it_be(:active_subgroup_inactive_subgroup) { create(:group, :archived, parent: active_subgroup) }
|
||||
let_it_be(:active_subgroup_inactive_project) { create(:project, :archived, group: active_subgroup) }
|
||||
|
||||
it 'returns inactive descendant', :aggregate_failures do
|
||||
is_expected.to include(active_subgroup_inactive_subgroup, active_subgroup_inactive_project)
|
||||
is_expected.not_to include(active_subgroup_active_subgroup, active_subgroup_active_project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when matched descendant has inactive ancestor' do
|
||||
let_it_be(:inactive_descendant) { create(:group, :archived, parent: inactive_subgroup) }
|
||||
|
||||
# Filter and page size params ensure only the leaf descendant matches the query,
|
||||
# so any ancestors must be there due to preloading, not because they happen to be
|
||||
# a part of the initial results.
|
||||
let(:params) { { active: false, filter: inactive_descendant.name, per_page: 1 } }
|
||||
|
||||
it 'preloads inactive ancestor' do
|
||||
is_expected.to contain_exactly(inactive_descendant, inactive_subgroup)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when matches descendant has active ancestor' do
|
||||
let_it_be(:inactive_descendant) { create(:group, :archived, parent: active_subgroup) }
|
||||
|
||||
# Filter and page size params ensure only the leaf descendant matches the query,
|
||||
# so any ancestors must be there due to preloading, not because they happen to be
|
||||
# a part of the initial results.
|
||||
let(:params) { { active: false, filter: inactive_descendant.name, per_page: 1 } }
|
||||
|
||||
it 'does not preload active ancestor' do
|
||||
is_expected.to contain_exactly(inactive_descendant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ describe('JobActionButton', () => {
|
|||
${'run'} | ${'play'} | ${'Run'} | ${1}
|
||||
${'retry'} | ${'retry'} | ${'Run again'} | ${2}
|
||||
${'unschedule'} | ${'time-out'} | ${'Unschedule'} | ${3}
|
||||
${'stop'} | ${'stop'} | ${'Stop'} | ${4}
|
||||
`('$action action', ({ icon, mockIndex, tooltip }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { jobAction: mockJobActions[mockIndex] } });
|
||||
|
|
@ -109,6 +110,7 @@ describe('JobActionButton', () => {
|
|||
${'run'} | ${1} | ${playJobMutation} | ${playMutationHandler} | ${i18n.errors.playJob}
|
||||
${'retry'} | ${2} | ${retryJobMutation} | ${retryMutationHandler} | ${i18n.errors.retryJob}
|
||||
${'unschedule'} | ${3} | ${unscheduleJobMutation} | ${unscheduleMutationHandler} | ${i18n.errors.unscheduleJob}
|
||||
${'stop'} | ${4} | ${playJobMutation} | ${playMutationHandler} | ${i18n.errors.playJob}
|
||||
`('$action action', ({ mockIndex, mutation, handler, errorMessage }) => {
|
||||
it('calls the correct mutation on button click', async () => {
|
||||
await createComponent({
|
||||
|
|
|
|||
|
|
@ -119,6 +119,14 @@ export const mockJobActions = [
|
|||
path: '/flightjs/Flight/-/jobs/1001/unschedule',
|
||||
title: 'Unschedule',
|
||||
},
|
||||
{
|
||||
__typename: 'StatusAction',
|
||||
confirmationMessage: null,
|
||||
id: 'Ci::Build-stop-1001',
|
||||
icon: 'stop',
|
||||
path: '/flightjs/Flight/-/jobs/1001/play',
|
||||
title: 'Stop',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockJobMutationResponse = (dataName) => ({
|
||||
|
|
|
|||
|
|
@ -67,12 +67,20 @@ describe('~/environments/components/deployment.vue', () => {
|
|||
describe('approval badge', () => {
|
||||
it('should show a badge if the deployment needs approval', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { deployment: { ...deployment, pendingApprovalCount: 5 } },
|
||||
propsData: { deployment: { ...deployment, pendingApprovalCount: 5, status: 'running' } },
|
||||
});
|
||||
|
||||
expect(findApprovalBadge().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show a badge if the deployment status is failed', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { deployment: { ...deployment, pendingApprovalCount: 5, status: 'failed' } },
|
||||
});
|
||||
|
||||
expect(findApprovalBadge().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show a badge if the deployment does not need approval', () => {
|
||||
wrapper = createWrapper();
|
||||
|
||||
|
|
|
|||
|
|
@ -1478,7 +1478,12 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:marked_for_deletion_at) { Time.new(2025, 5, 25) }
|
||||
let_it_be(:pending_delete_group) do
|
||||
create(:group_with_deletion_schedule, marked_for_deletion_on: marked_for_deletion_at, developers: user)
|
||||
end
|
||||
|
||||
let_it_be(:pending_delete_project) { create(:project, marked_for_deletion_at: marked_for_deletion_at) }
|
||||
let_it_be(:parent_pending_delete_project) { create(:project, group: pending_delete_group) }
|
||||
|
||||
let(:project_full_path) { pending_delete_project.full_path }
|
||||
|
||||
|
|
@ -1486,6 +1491,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
%(
|
||||
query {
|
||||
project(fullPath: "#{project_full_path}") {
|
||||
markedForDeletion
|
||||
markedForDeletionOn
|
||||
permanentDeletionDate
|
||||
}
|
||||
|
|
@ -1496,16 +1502,22 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
before do
|
||||
pending_delete_project.add_developer(user)
|
||||
project.add_developer(user)
|
||||
stub_application_setting(deletion_adjourned_period: 7)
|
||||
end
|
||||
|
||||
subject(:project_data) do
|
||||
result = GitlabSchema.execute(query, context: { current_user: user }).as_json
|
||||
{
|
||||
marked_for_deletion: result.dig('data', 'project', 'markedForDeletion'),
|
||||
marked_for_deletion_on: result.dig('data', 'project', 'markedForDeletionOn'),
|
||||
permanent_deletion_date: result.dig('data', 'project', 'permanentDeletionDate')
|
||||
}
|
||||
end
|
||||
|
||||
it 'marked_for_deletion returns true' do
|
||||
expect(project_data[:marked_for_deletion]).to be true
|
||||
end
|
||||
|
||||
it 'marked_for_deletion_on returns correct date' do
|
||||
marked_for_deletion_on_time = Time.zone.parse(project_data[:marked_for_deletion_on])
|
||||
|
||||
|
|
@ -1521,9 +1533,21 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
end
|
||||
end
|
||||
|
||||
context 'when parent is scheduled for deletion' do
|
||||
let(:project_full_path) { parent_pending_delete_project.full_path }
|
||||
|
||||
it 'marked_for_deletion returns true' do
|
||||
expect(project_data[:marked_for_deletion]).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is not scheduled for deletion' do
|
||||
let(:project_full_path) { project.full_path }
|
||||
|
||||
it 'marked_for_deletion returns false' do
|
||||
expect(project_data[:marked_for_deletion]).to be false
|
||||
end
|
||||
|
||||
it 'returns theoretical date project will be permanently deleted for permanent_deletion_date' do
|
||||
expect(project_data[:permanent_deletion_date])
|
||||
.to eq(::Gitlab::CurrentSettings.deletion_adjourned_period.days.since(Date.current).strftime('%F'))
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GroupDescendant do
|
||||
let(:parent) { create(:group) }
|
||||
let(:subgroup) { create(:group, parent: parent) }
|
||||
let(:subsub_group) { create(:group, parent: subgroup) }
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: parent) }
|
||||
let_it_be(:subsub_group) { create(:group, parent: subgroup) }
|
||||
|
||||
def all_preloaded_groups(*groups)
|
||||
groups + [parent, subgroup, subsub_group]
|
||||
|
|
@ -46,6 +48,27 @@ RSpec.describe GroupDescendant do
|
|||
expect { subsub_group.hierarchy(build_stubbed(:group)) }
|
||||
.to raise_error('specified top is not part of the tree')
|
||||
end
|
||||
|
||||
context 'with upto_preloaded_ancestors_only option' do
|
||||
where :top, :preloaded, :expected_hierarchy do
|
||||
nil | [] | ref(:subsub_group)
|
||||
nil | [ref(:parent)] | ref(:subsub_group)
|
||||
nil | [ref(:subgroup)] | { ref(:subgroup) => ref(:subsub_group) }
|
||||
nil | [ref(:parent), ref(:subgroup)] | { ref(:parent) => { ref(:subgroup) => ref(:subsub_group) } }
|
||||
ref(:parent) | [] | ref(:subsub_group)
|
||||
ref(:parent) | [ref(:parent)] | ref(:subsub_group)
|
||||
ref(:parent) | [ref(:subgroup)] | { ref(:subgroup) => ref(:subsub_group) }
|
||||
ref(:parent) | [ref(:parent), ref(:subgroup)] | { ref(:subgroup) => ref(:subsub_group) }
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject { subsub_group.hierarchy(top, preloaded, { upto_preloaded_ancestors_only: true }) }
|
||||
|
||||
it 'builds hierarchy upto preloaded ancestors only' do
|
||||
is_expected.to eq(expected_hierarchy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.build_hierarchy' do
|
||||
|
|
@ -95,7 +118,7 @@ RSpec.describe GroupDescendant do
|
|||
described_class.build_hierarchy([subsub_group])
|
||||
|
||||
expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception)
|
||||
.at_least(:once) do |exception, _|
|
||||
.at_least(:once) do |exception, _|
|
||||
expect(exception.backtrace).to be_present
|
||||
end
|
||||
end
|
||||
|
|
@ -114,11 +137,24 @@ RSpec.describe GroupDescendant do
|
|||
expect { described_class.build_hierarchy([subsub_group]) }
|
||||
.to raise_error(/was not preloaded/)
|
||||
end
|
||||
|
||||
context 'with upto_preloaded_ancestors_only option' do
|
||||
let_it_be(:other_subgroup) { create(:group, parent: parent) }
|
||||
let_it_be(:descendants) { [subgroup, other_subgroup, subsub_group] }
|
||||
|
||||
subject do
|
||||
described_class.build_hierarchy(descendants, nil, { upto_preloaded_ancestors_only: true })
|
||||
end
|
||||
|
||||
it "builds descendant's hierarchies with the preloaded ancestors only" do
|
||||
is_expected.to match_array([{ subgroup => subsub_group }, other_subgroup])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project' do
|
||||
let(:project) { create(:project, namespace: subsub_group) }
|
||||
let_it_be(:project) { create(:project, namespace: subsub_group) }
|
||||
|
||||
describe '#hierarchy' do
|
||||
it 'builds a hierarchy for a project' do
|
||||
|
|
@ -132,6 +168,27 @@ RSpec.describe GroupDescendant do
|
|||
|
||||
expect(project.hierarchy(subgroup)).to eq(expected_hierarchy)
|
||||
end
|
||||
|
||||
context 'with upto_preloaded_ancestors_only option' do
|
||||
where :top, :preloaded, :expected_hierarchy do
|
||||
nil | [] | ref(:project)
|
||||
nil | [ref(:subgroup)] | ref(:project)
|
||||
nil | [ref(:subsub_group)] | { ref(:subsub_group) => ref(:project) }
|
||||
nil | [ref(:subgroup), ref(:subsub_group)] | { ref(:subgroup) => { ref(:subsub_group) => ref(:project) } }
|
||||
ref(:subgroup) | [] | ref(:project)
|
||||
ref(:subgroup) | [ref(:subgroup)] | ref(:project)
|
||||
ref(:subgroup) | [ref(:subsub_group)] | { ref(:subsub_group) => ref(:project) }
|
||||
ref(:subgroup) | [ref(:subgroup), ref(:subsub_group)] | { ref(:subsub_group) => ref(:project) }
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject { project.hierarchy(top, preloaded, { upto_preloaded_ancestors_only: true }) }
|
||||
|
||||
it 'builds hierarchy upto preloaded ancestors only' do
|
||||
is_expected.to eq(expected_hierarchy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.build_hierarchy' do
|
||||
|
|
@ -192,6 +249,19 @@ RSpec.describe GroupDescendant do
|
|||
|
||||
expect(actual_hierarchy).to eq(expected_hierarchy)
|
||||
end
|
||||
|
||||
context 'with upto_preloaded_ancestors_only option' do
|
||||
let_it_be(:other_subgroup) { create(:group, parent: parent) }
|
||||
let_it_be(:descendants) { [subgroup, other_subgroup, subsub_group, project] }
|
||||
|
||||
subject do
|
||||
described_class.build_hierarchy(descendants, nil, { upto_preloaded_ancestors_only: true })
|
||||
end
|
||||
|
||||
it "builds descendant's hierarchies with the preloaded ancestors only" do
|
||||
is_expected.to match_array([{ subgroup => { subsub_group => project } }, other_subgroup])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
|
|||
Users::Internal.support_bot_id
|
||||
end
|
||||
|
||||
shared_examples 'work items resolver without N + 1 queries' do
|
||||
shared_examples 'work items resolver without N + 1 queries' do |threshold: 0|
|
||||
it 'avoids N+1 queries', :use_sql_query_cache do
|
||||
post_graphql(query, current_user: current_user) # warm-up
|
||||
|
||||
|
|
@ -63,7 +63,10 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
|
|||
author: reporter
|
||||
)
|
||||
|
||||
expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control)
|
||||
expect do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end.not_to exceed_all_query_limit(control).with_threshold(threshold)
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
|
@ -79,7 +82,8 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
|
|||
|
||||
describe 'N + 1 queries' do
|
||||
context 'when querying root fields' do
|
||||
it_behaves_like 'work items resolver without N + 1 queries'
|
||||
# Issue to fix N+1 - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
|
||||
it_behaves_like 'work items resolver without N + 1 queries', threshold: 3
|
||||
end
|
||||
|
||||
# We need a separate example since all_graphql_fields_for will not fetch fields from types
|
||||
|
|
|
|||
|
|
@ -124,9 +124,10 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
|
|||
# There is an N+1 query related to custom roles - https://gitlab.com/gitlab-org/gitlab/-/issues/515675
|
||||
# There is an N+1 query for duo_features_enabled cascading setting - https://gitlab.com/gitlab-org/gitlab/-/issues/442164
|
||||
# There is an N+1 query related to pipelines - https://gitlab.com/gitlab-org/gitlab/-/issues/515677
|
||||
# There is an N+1 query related to marked_for_deletion - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
|
||||
expect do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end.not_to exceed_all_query_limit(control).with_threshold(8)
|
||||
end.not_to exceed_all_query_limit(control).with_threshold(12)
|
||||
end
|
||||
|
||||
it 'returns the expected projects' do
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ RSpec.describe 'find work items by reference', feature_category: :portfolio_mana
|
|||
extra_work_items = create_list(:work_item, 2, :task, project: project2)
|
||||
refs = references + extra_work_items.map { |item| item.to_reference(full: true) }
|
||||
|
||||
# Issue to fix N+1 - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
|
||||
expect do
|
||||
post_graphql(query(refs: refs), current_user: current_user)
|
||||
end.not_to exceed_all_query_limit(control_count)
|
||||
end.not_to exceed_all_query_limit(control_count).with_threshold(2)
|
||||
expect(graphql_data_at('workItemsByReference', 'nodes').size).to eq(3)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue