Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-20 18:12:55 +00:00
parent 07959a9d0d
commit cbd6e780ec
73 changed files with 758 additions and 203 deletions

View File

@ -254,6 +254,10 @@ variables:
BUILD_ASSETS_IMAGE: "true" # Set it to "false" to disable assets image building, used in `build-assets-image`
SIMPLECOV: "true"
# Temporary variable to enable new CSS compiler
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/438278
USE_NEW_CSS_PIPELINE: "true"
include:
- local: .gitlab/ci/_skip.yml
rules:

View File

@ -10,9 +10,6 @@ export const BV_SHOW_TOOLTIP = 'bv::show::tooltip';
export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
export const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
// We set the drawer's z-index to 252 to clear flash messages that might
// be displayed in the page and that have a z-index of 251.
export const DRAWER_Z_INDEX = 252;

View File

@ -1,14 +1,4 @@
import { convertToSnakeCase, convertToCamelCase } from '~/lib/utils/text_utility';
import { DEFAULT_TH_CLASSES } from './constants';
/**
* Deprecated: use thWidthPercent instead
* Generates the table header classes to be used for GlTable fields.
*
* @param {Number} width - The column width as a percentage.
* @returns {String} The classes to be used in GlTable fields object.
*/
export const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`;
/**
* Generates the table header class for width to be used for GlTable fields.

View File

@ -1,6 +1,5 @@
<script>
import { GlIcon, GlLink, GlCard } from '@gitlab/ui';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
export default {
@ -16,7 +15,7 @@ export default {
required: true,
},
count: {
type: Number,
type: String,
required: true,
},
linkHref: {
@ -29,11 +28,6 @@ export default {
default: __('View all'),
},
},
computed: {
formattedCount() {
return numberToMetricPrefix(this.count, 0);
},
},
};
</script>
@ -48,7 +42,7 @@ export default {
</div>
<span
class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-mt-2 gl-display-block"
>{{ formattedCount }}</span
>{{ count }}</span
>
</gl-card>
</template>

View File

@ -8,6 +8,8 @@ export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
export const SCOPE_MILESTONES = 'milestones';
export const SCOPE_WIKI_BLOBS = 'wiki_blobs';
export const SCOPE_EPICS = 'epics';
export const SCOPE_USERS = 'users';
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',

View File

@ -9,6 +9,8 @@ import {
ADVANCED_SEARCH_TYPE,
BASIC_SEARCH_TYPE,
SEARCH_LEVEL_PROJECT,
SEARCH_LEVEL_GLOBAL,
SEARCH_LEVEL_GROUP,
} from '~/search/store/constants';
import {
ZOEKT_HELP_PAGE,
@ -17,6 +19,8 @@ import {
ZOEKT_HELP_PAGE_SYNTAX_ANCHOR,
} from '../constants';
import { SCOPE_BLOB } from '../../sidebar/constants';
export default {
name: 'SearchTypeIndicator',
directives: {
@ -40,7 +44,7 @@ export default {
GlLink,
},
computed: {
...mapState(['searchType', 'defaultBranchName', 'query', 'searchLevel']),
...mapState(['searchType', 'defaultBranchName', 'query', 'searchLevel', 'query']),
zoektHelpUrl() {
return helpPagePath(ZOEKT_HELP_PAGE);
},
@ -58,17 +62,39 @@ export default {
});
},
isZoekt() {
return this.searchType === ZOEKT_SEARCH_TYPE;
return this.searchType === ZOEKT_SEARCH_TYPE && this.query.scope === SCOPE_BLOB;
},
isAdvancedSearch() {
return this.searchType === ADVANCED_SEARCH_TYPE;
return (
this.searchType === ADVANCED_SEARCH_TYPE ||
(this.searchType === ZOEKT_SEARCH_TYPE && this.query.scope !== SCOPE_BLOB)
);
},
isEnabled() {
if (this.searchLevel !== SEARCH_LEVEL_PROJECT) {
return true;
searchTypeTestId() {
if (this.isZoekt) {
return ZOEKT_SEARCH_TYPE;
}
if (this.isAdvancedSearch) {
return ADVANCED_SEARCH_TYPE;
}
return !this.query.repository_ref || this.query.repository_ref === this.defaultBranchName;
return BASIC_SEARCH_TYPE;
},
isEnabled() {
const repoRef = this.query.repository_ref;
switch (this.searchLevel) {
case SEARCH_LEVEL_GLOBAL:
case SEARCH_LEVEL_GROUP:
return true;
case SEARCH_LEVEL_PROJECT: {
if (this.query.scope !== SCOPE_BLOB) {
return true;
}
return !repoRef || repoRef === this.defaultBranchName;
}
default:
return false;
}
},
isBasicSearch() {
return this.searchType === BASIC_SEARCH_TYPE;
@ -94,14 +120,14 @@ export default {
<template>
<div class="gl-text-gray-600">
<div v-if="isBasicSearch" data-testid="basic">&nbsp;</div>
<div v-else-if="isEnabled" :data-testid="`${searchType}-enabled`">
<div v-else-if="isEnabled" :data-testid="`${searchTypeTestId}-enabled`">
<gl-sprintf :message="enabledMessage">
<template #link="{ content }">
<gl-link :href="helpUrl" target="_blank" data-testid="docs-link">{{ content }} </gl-link>
</template>
</gl-sprintf>
</div>
<div v-else :data-testid="`${searchType}-disabled`">
<div v-else :data-testid="`${searchTypeTestId}-disabled`">
<gl-sprintf :message="disabledMessage">
<template #link="{ content }">
<gl-link :href="helpUrl" target="_blank" data-testid="docs-link">{{ content }} </gl-link>

View File

@ -21,6 +21,7 @@
position: relative;
max-height: var(--read-more-height, #{$fallback - $height});
scroll-padding-top: 1000vh; // Fix anchor scroll, keep it up top
overflow: hidden;
}
// only appears when size is > $height.

View File

@ -3,7 +3,7 @@
module Mutations
module SavedReplies
class Base < BaseMutation
field :saved_reply, Types::SavedReplyType,
field :saved_reply, ::Types::Users::SavedReplyType,
null: true,
description: 'Saved reply after mutation.'

View File

@ -9,7 +9,7 @@ module Mutations
argument :id, Types::GlobalIDType[::Users::SavedReply],
required: true,
description: copy_field_description(Types::SavedReplyType, :id)
description: copy_field_description(::Types::Users::SavedReplyType, :id)
def resolve(id:)
saved_reply = authorized_find!(id: id)

View File

@ -9,15 +9,15 @@ module Mutations
argument :id, Types::GlobalIDType[::Users::SavedReply],
required: true,
description: copy_field_description(Types::SavedReplyType, :id)
description: copy_field_description(::Types::Users::SavedReplyType, :id)
argument :name, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::SavedReplyType, :name)
description: copy_field_description(::Types::SavedReplyType, :name)
argument :content, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::SavedReplyType, :content)
description: copy_field_description(::Types::SavedReplyType, :content)
def resolve(id:, name:, content:)
saved_reply = authorized_find!(id: id)

View File

@ -17,7 +17,7 @@ module Resolvers
type Types::CustomEmojiType, null: true
def resolve(**args)
Groups::CustomEmojiFinder.new(object, args).execute
::Groups::CustomEmojiFinder.new(object, args).execute
end
end
end

View File

@ -2,7 +2,7 @@
module Resolvers
class SavedReplyResolver < BaseResolver
type Types::SavedReplyType, null: true
type ::Types::Users::SavedReplyType, null: true
alias_method :target, :object
@ -11,11 +11,7 @@ module Resolvers
description: 'ID of a saved reply.'
def resolve(id:)
saved_reply = ::Users::SavedReply.find_saved_reply(user_id: current_user.id, id: id.model_id)
return unless saved_reply
saved_reply
::Users::SavedReply.find_saved_reply(user_id: current_user.id, id: id.model_id)
end
end
end

View File

@ -26,7 +26,7 @@ module Resolvers
private
def resolve_groups(**args)
Groups::UserGroupsFinder.new(current_user, object, args).execute
::Groups::UserGroupsFinder.new(current_user, object, args).execute
end
end
end

View File

@ -2,16 +2,10 @@
module Types
class SavedReplyType < BaseObject
graphql_name 'SavedReply'
connection_type_class Types::CountableConnectionType
authorize :read_saved_replies
field :id, Types::GlobalIDType[::Users::SavedReply],
null: false,
description: 'Global ID of the saved reply.'
field :content, GraphQL::Types::String,
null: false,
description: 'Content of the saved reply.'

View File

@ -137,7 +137,7 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
field :saved_replies,
Types::SavedReplyType.connection_type,
::Types::Users::SavedReplyType.connection_type,
null: true,
description: 'Saved replies authored by the user.'

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Types
module Users
class SavedReplyType < ::Types::SavedReplyType
graphql_name 'SavedReply'
authorize :read_saved_replies
field :id, Types::GlobalIDType[::Users::SavedReply],
null: false,
description: 'Global ID of the user saved reply.'
end
end
end

View File

@ -4,6 +4,7 @@ module Integrations
class Teamcity < BaseCi
include PushDataValidations
include ReactivelyCached
include HasAvatar
prepend EnableSslVerification
TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i
@ -91,6 +92,10 @@ module Integrations
original_value.nil? ? (new_record? || url_is_saas?) : original_value
end
def attribution_notice
'Copyright © 2024 JetBrains s.r.o. JetBrains TeamCity and the JetBrains TeamCity logo are registered trademarks of JetBrains s.r.o.'
end
private
def url_is_saas?

View File

@ -3,6 +3,7 @@
module Integrations
class Youtrack < BaseIssueTracker
include Integrations::HasIssueTrackerFields
include HasAvatar
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
@ -15,16 +16,16 @@ module Integrations
end
def self.title
'YouTrack'
'JetBrains YouTrack'
end
def self.description
s_("IssueTracker|Use YouTrack as this project's issue tracker.")
s_("IssueTracker|Use JetBrains YouTrack as this project's issue tracker.")
end
def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
s_("IssueTracker|Use JetBrains YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@ -34,5 +35,9 @@ module Integrations
def self.fields
super.select { %w[project_url issues_url].include?(_1.name) }
end
def attribution_notice
'Copyright © 2024 JetBrains s.r.o. JetBrains YouTrack and the JetBrains YouTrack logo are registered trademarks of JetBrains s.r.o.'
end
end
end

View File

@ -652,7 +652,7 @@ class Note < ApplicationRecord
end
def resource_parent
project
noteable.try(:resource_parent) || project
end
def user_mentions
@ -943,4 +943,4 @@ class Note < ApplicationRecord
end
end
Note.prepend_mod_with('Note')
Note.prepend_mod

View File

@ -66,7 +66,7 @@ module WorkItems
original_description = description_param.fetch(:description, work_item.description)
description, command_params = QuickActions::InterpretService
.new(work_item.project, current_user, {})
.new(container: work_item.resource_parent, current_user: current_user)
.execute(original_description, work_item)
description_param[:description] = description if description && description != original_description

View File

@ -41,7 +41,7 @@ module Groups
def commands(noteable)
return [] unless noteable
QuickActions::InterpretService.new(nil, current_user).available_commands(noteable)
QuickActions::InterpretService.new(container: group, current_user: current_user).available_commands(noteable)
end
end
end

View File

@ -210,9 +210,11 @@ class IssuableBaseService < ::BaseContainerService
def merge_quick_actions_into_params!(issuable, only: nil)
original_description = params.fetch(:description, issuable.description)
description, command_params =
QuickActions::InterpretService.new(project, current_user, quick_action_options)
.execute(original_description, issuable, only: only)
description, command_params = QuickActions::InterpretService.new(
container: container,
current_user: current_user,
params: quick_action_options
).execute(original_description, issuable, only: only)
# Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if description && description != original_description

View File

@ -34,7 +34,11 @@ module Notes
def execute(note, options = {})
return [note.note, {}] unless supported?(note)
@interpret_service = QuickActions::InterpretService.new(project, current_user, options)
@interpret_service = QuickActions::InterpretService.new(
container: note.resource_parent,
current_user: current_user,
params: options
)
interpret_service.execute(note.note, note.noteable)
end
@ -90,4 +94,4 @@ module Notes
end
end
Notes::QuickActionsService.prepend_mod_with('Notes::QuickActionsService')
Notes::QuickActionsService.prepend_mod

View File

@ -23,7 +23,7 @@ class PreviewMarkdownService < BaseContainerService
def explain_quick_actions(text)
return text, [] unless quick_action_types.include?(target_type)
quick_actions_service = QuickActions::InterpretService.new(project, current_user)
quick_actions_service = QuickActions::InterpretService.new(container: container, current_user: current_user)
quick_actions_service.explain(text, find_commands_target, keep_actions: params[:render_quick_actions])
end

View File

@ -26,7 +26,7 @@ module Projects
def commands(noteable)
return [] unless noteable && current_user
QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
QuickActions::InterpretService.new(container: project, current_user: current_user).available_commands(noteable)
end
def snippets

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QuickActions
class InterpretService < BaseService
class InterpretService < BaseContainerService
include Gitlab::Utils::StrongMemoize
include Gitlab::QuickActions::Dsl
include Gitlab::QuickActions::IssueActions
@ -109,12 +109,6 @@ module QuickActions
project || group
end
def group
strong_memoize(:group) do
quick_action_target.group if quick_action_target.respond_to?(:group)
end
end
def find_labels(labels_params = nil)
extract_references(labels_params, :label) | find_labels_by_name_no_tilde(labels_params)
end
@ -218,4 +212,4 @@ module QuickActions
end
end
QuickActions::InterpretService.prepend_mod_with('QuickActions::InterpretService')
QuickActions::InterpretService.prepend_mod

View File

@ -4,7 +4,7 @@
%aside.project-page-sidebar
.project-page-sidebar-block.home-panel-home-desc.gl-py-4.gl-border-b.gl-border-gray-50{ class: 'gl-pt-2!' }
.gl-display-flex.gl-justify-content-space-between
%p.gl-font-weight-bold.gl-text-gray-900.gl-m-0.gl-mb-1= s_('ProjectPage|Project information')
%h2.gl-font-base.gl-font-weight-bold.gl-reset-line-height.gl-text-gray-900.gl-m-0.gl-mb-1= s_('ProjectPage|Project information')
-# Project settings
- if can?(current_user, :admin_project, @project)
= render Pajamas::ButtonComponent.new(href: edit_project_path(@project),

View File

@ -0,0 +1,5 @@
.info-well
.well-segment
= markdown integration.help
%p
%em= integration.attribution_notice

View File

@ -0,0 +1,5 @@
.info-well
.well-segment
= markdown integration.help
%p
%em= integration.attribution_notice

View File

@ -260,7 +260,7 @@ module Gitlab
# For more context, see: https://gitlab.com/gitlab-org/gitlab/-/issues/438278
# Need to be loaded before initializers
config.before_configuration do
if Gitlab::Utils.to_boolean(ENV["USE_NEW_CSS_PIPELINE"])
if Gitlab::Utils.to_boolean(ENV["USE_NEW_CSS_PIPELINE"], default: true)
require 'cssbundling-rails'
else
require 'fileutils'

View File

@ -1,8 +0,0 @@
---
name: use_sync_service_token_worker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136078
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431608
milestone: '16.9'
type: development
group: group::cloud connector
default_enabled: true

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddAncestorsColumnToSbomOccurrences < Gitlab::Database::Migration[2.2]
milestone '16.10'
enable_lock_retries!
def up
add_column :sbom_occurrences, :ancestors, :jsonb, default: [], null: false
end
def down
remove_column :sbom_occurrences, :ancestors
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class ReplaceOldFkCiBuildTraceMetadataToCiJobArtifacts < Gitlab::Database::Migration[2.2]
milestone '16.10'
disable_ddl_transaction!
TABLE_NAME = :ci_build_trace_metadata
REFERENCED_TABLE_NAME = :ci_job_artifacts
FK_NAME = :fk_21d25cac1a_p
TMP_FK_NAME = :tmp_fk_21d25cac1a_p
def up
with_lock_retries do
remove_foreign_key_if_exists(TABLE_NAME, REFERENCED_TABLE_NAME, name: FK_NAME, reverse_lock_order: true)
rename_constraint(TABLE_NAME, TMP_FK_NAME, FK_NAME)
end
end
def down
add_concurrent_foreign_key(TABLE_NAME,
REFERENCED_TABLE_NAME,
name: TMP_FK_NAME,
column: [:partition_id, :trace_artifact_id],
target_column: [:partition_id, :id],
on_update: :cascade,
on_delete: :cascade,
validate: true,
reverse_lock_order: true
)
switch_constraint_names(TABLE_NAME, FK_NAME, TMP_FK_NAME)
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class ReplaceOldFkCiJobArtifactStatesToCiJobArtifacts < Gitlab::Database::Migration[2.2]
milestone '16.10'
disable_ddl_transaction!
TABLE_NAME = :ci_job_artifact_states
REFERENCED_TABLE_NAME = :ci_job_artifacts
FK_NAME = :fk_rails_80a9cba3b2_p
TMP_FK_NAME = :tmp_fk_rails_80a9cba3b2_p
def up
with_lock_retries do
remove_foreign_key_if_exists(TABLE_NAME, REFERENCED_TABLE_NAME, name: FK_NAME, reverse_lock_order: true)
rename_constraint(TABLE_NAME, TMP_FK_NAME, FK_NAME)
end
end
def down
add_concurrent_foreign_key(TABLE_NAME,
REFERENCED_TABLE_NAME,
name: TMP_FK_NAME,
column: [:partition_id, :job_artifact_id],
target_column: [:partition_id, :id],
on_update: :cascade,
on_delete: :cascade,
validate: true,
reverse_lock_order: true
)
switch_constraint_names(TABLE_NAME, FK_NAME, TMP_FK_NAME)
end
end

View File

@ -0,0 +1 @@
83cb4871102a1096c688ef1f414c654581416479c1d082ba3540ae774d02ecfa

View File

@ -0,0 +1 @@
ee6b6d16269e1d4c1addac4c7d403f196f7b339f0bb593677f874f652e14b4ed

View File

@ -0,0 +1 @@
0c728d47b305f6ac23d902de40560fcb689ed293e249aa2e2d17de72ef9c57dd

View File

@ -15171,6 +15171,7 @@ CREATE TABLE sbom_occurrences (
source_package_id bigint,
archived boolean DEFAULT false NOT NULL,
traversal_ids bigint[] DEFAULT '{}'::bigint[] NOT NULL,
ancestors jsonb DEFAULT '[]'::jsonb NOT NULL,
CONSTRAINT check_3f2d2c7ffc CHECK ((char_length(package_manager) <= 255)),
CONSTRAINT check_9b29021fa8 CHECK ((char_length(component_name) <= 255)),
CONSTRAINT check_e6b8437cfe CHECK ((char_length(input_file_path) <= 1024))
@ -29327,7 +29328,7 @@ ALTER TABLE ONLY namespace_settings
ADD CONSTRAINT fk_20cf0eb2f9 FOREIGN KEY (default_compliance_framework_id) REFERENCES compliance_management_frameworks(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_build_trace_metadata
ADD CONSTRAINT fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ADD CONSTRAINT fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES p_ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY users_star_projects
ADD CONSTRAINT fk_22cd27ddfc FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@ -31295,7 +31296,7 @@ ALTER TABLE ONLY dependency_proxy_manifest_states
ADD CONSTRAINT fk_rails_806cf07a3c FOREIGN KEY (dependency_proxy_manifest_id) REFERENCES dependency_proxy_manifests(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_artifact_states
ADD CONSTRAINT fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ADD CONSTRAINT fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES p_ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY approval_merge_request_rules_users
ADD CONSTRAINT fk_rails_80e6801803 FOREIGN KEY (approval_merge_request_rule_id) REFERENCES approval_merge_request_rules(id) ON DELETE CASCADE;
@ -32230,12 +32231,6 @@ ALTER TABLE issue_search_data
ALTER TABLE issue_search_data
ADD CONSTRAINT issue_search_data_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_build_trace_metadata
ADD CONSTRAINT tmp_fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES p_ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_artifact_states
ADD CONSTRAINT tmp_fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES p_ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY user_follow_users
ADD CONSTRAINT user_follow_users_followee_id_fkey FOREIGN KEY (followee_id) REFERENCES users(id) ON DELETE CASCADE;

View File

@ -7043,7 +7043,7 @@ Input type: `SavedReplyDestroyInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsavedreplydestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsavedreplydestroyid"></a>`id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the saved reply. |
| <a id="mutationsavedreplydestroyid"></a>`id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the user saved reply. |
#### Fields
@ -7063,7 +7063,7 @@ Input type: `SavedReplyUpdateInput`
| ---- | ---- | ----------- |
| <a id="mutationsavedreplyupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsavedreplyupdatecontent"></a>`content` | [`String!`](#string) | Content of the saved reply. |
| <a id="mutationsavedreplyupdateid"></a>`id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the saved reply. |
| <a id="mutationsavedreplyupdateid"></a>`id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the user saved reply. |
| <a id="mutationsavedreplyupdatename"></a>`name` | [`String!`](#string) | Name of the saved reply. |
#### Fields
@ -11389,6 +11389,30 @@ The edge type for [`GroupMember`](#groupmember).
| <a id="groupmemberedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="groupmemberedgenode"></a>`node` | [`GroupMember`](#groupmember) | The item at the end of the edge. |
#### `GroupSavedReplyConnection`
The connection type for [`GroupSavedReply`](#groupsavedreply).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupsavedreplyconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="groupsavedreplyconnectionedges"></a>`edges` | [`[GroupSavedReplyEdge]`](#groupsavedreplyedge) | A list of edges. |
| <a id="groupsavedreplyconnectionnodes"></a>`nodes` | [`[GroupSavedReply]`](#groupsavedreply) | A list of nodes. |
| <a id="groupsavedreplyconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `GroupSavedReplyEdge`
The edge type for [`GroupSavedReply`](#groupsavedreply).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupsavedreplyedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="groupsavedreplyedgenode"></a>`node` | [`GroupSavedReply`](#groupsavedreply) | The item at the end of the edge. |
#### `GroupWikiRepositoryRegistryConnection`
The connection type for [`GroupWikiRepositoryRegistry`](#groupwikirepositoryregistry).
@ -19925,6 +19949,7 @@ GPG signature for a signed commit.
| <a id="grouprequestaccessenabled"></a>`requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request access to namespace. |
| <a id="grouprequiretwofactorauthentication"></a>`requireTwoFactorAuthentication` | [`Boolean`](#boolean) | Indicates if all users in this group are required to set up two-factor authentication. |
| <a id="grouprootstoragestatistics"></a>`rootStorageStatistics` | [`RootStorageStatistics`](#rootstoragestatistics) | Aggregated storage statistics of the namespace. Only available for root namespaces. |
| <a id="groupsavedreplies"></a>`savedReplies` **{warning-solid}** | [`GroupSavedReplyConnection`](#groupsavedreplyconnection) | **Introduced** in 16.10. **Status**: Experiment. Saved replies available to the group. Available only when feature flag `group_saved_replies_flag` is enabled. This field can only be resolved for one group in any single request. |
| <a id="groupsecuritypolicyproject"></a>`securityPolicyProject` | [`Project`](#project) | Security policy project assigned to the namespace. |
| <a id="groupsharewithgrouplock"></a>`shareWithGroupLock` | [`Boolean`](#boolean) | Indicates if sharing a project with another group within this group is prevented. |
| <a id="groupsharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
@ -20807,6 +20832,22 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="grouprunnersupgradestatus"></a>`upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Filter by upgrade status. |
| <a id="grouprunnersversionprefix"></a>`versionPrefix` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.6. **Status**: Experiment. Filter runners by version. Runners that contain runner managers with the version at the start of the search term are returned. For example, the search term '14.' returns runner managers with versions '14.11.1' and '14.2.3'. |
##### `Group.savedReply`
Saved reply in the group. Available only when feature flag `group_saved_replies_flag` is enabled. This field can only be resolved for one group in any single request.
NOTE:
**Introduced** in 16.10.
**Status**: Experiment.
Returns [`GroupSavedReply`](#groupsavedreply).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupsavedreplyid"></a>`id` | [`GroupsSavedReplyID!`](#groupssavedreplyid) | Global ID of a saved reply. |
##### `Group.scanExecutionPolicies`
Scan Execution Policies of the namespace.
@ -21118,6 +21159,16 @@ Contains release-related statistics about a group.
| <a id="groupreleasestatsreleasescount"></a>`releasesCount` | [`Int`](#int) | Total number of releases in all descendant projects of the group. |
| <a id="groupreleasestatsreleasespercentage"></a>`releasesPercentage` | [`Int`](#int) | Percentage of the group's descendant projects that have at least one release. |
### `GroupSavedReply`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupsavedreplycontent"></a>`content` | [`String!`](#string) | Content of the saved reply. |
| <a id="groupsavedreplyid"></a>`id` | [`GroupsSavedReplyID!`](#groupssavedreplyid) | Global ID of the group saved reply. |
| <a id="groupsavedreplyname"></a>`name` | [`String!`](#string) | Name of the saved reply. |
### `GroupSecurityPolicySource`
Represents the source of a security policy belonging to a group.
@ -27624,7 +27675,7 @@ Represents an entity for options in SAST CI configuration.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="savedreplycontent"></a>`content` | [`String!`](#string) | Content of the saved reply. |
| <a id="savedreplyid"></a>`id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the saved reply. |
| <a id="savedreplyid"></a>`id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the user saved reply. |
| <a id="savedreplyname"></a>`name` | [`String!`](#string) | Name of the saved reply. |
### `Scan`
@ -33581,6 +33632,12 @@ A `GroupID` is a global ID. It is encoded as a string.
An example `GroupID` is: `"gid://gitlab/Group/1"`.
### `GroupsSavedReplyID`
A `GroupsSavedReplyID` is a global ID. It is encoded as a string.
An example `GroupsSavedReplyID` is: `"gid://gitlab/Groups::SavedReply/1"`.
### `ID`
Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) input value will be accepted as an ID.

View File

@ -0,0 +1,115 @@
---
stage: none
group: Contributor Success
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# GitLab Beta program
The GitLab Beta program provides GitLab Beta program members with early access to exclusive features.
This page lists features available for testing as part of the program.
WARNING:
The GitLab Beta Program is not operational yet. This page is in draft & in preparation for an upcoming launch.
These features may not be ready for production use and follow the [Experimental or Beta policy](../../policy/experiment-beta-support.md) of GitLab.
## Git suggestions
- [GitLab CLI feature documentation](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-commands)
- [Feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/409636)
<!-- Copied from ../../editor_extensions/gitlab_cli/index.md#gitlab-duo-commands -->
Use `glab ask` to ask questions about `git` commands. It can help you remember a
command you forgot, or provide suggestions on how to run commands to perform other tasks.
**Get started:**
1. To install GLab, see [installation instructions](https://gitlab.com/gitlab-org/cli/#installation).
1. Set up [GitLab ClI Authentication](https://gitlab.com/gitlab-org/cli/#authentication).
1. Use `glab ask git` to generate a Git command with AI in your command line:
```shell
glab ask git <your_question>
```
Examples:
- `glab ask git how do I know the branch I'm on`
- `glab ask git how to create a new branch with only a few particular commits`
- `glab ask git how to find commits from removed branches`
After it replies, you can execute the command it generates.
## Code explanation
- [Code explanation feature documentation](../../user/ai_features.md#explain-code-in-the-web-ui-with-code-explanation)
- [Feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/407285)
<!-- from ../../user/ai_features.md#explain-code-in-the-web-ui-with-code-explanation -->
With the help of a large language model, GitLab Duo can explain code in natural language.
**Get started:**
1. On the left sidebar, select **Search or go to** and find your project.
1. Select any file in your project that contains code.
1. On the file, select the lines that you want to have explained.
1. On the left side, select the question mark (**{question}**). You might have to scroll to the
first line of your selection to view it. This sends the selected code, together with a prompt,
to provide an explanation to the large language model.
1. A drawer is displayed on the right side of the page. Wait a moment for the explanation to be generated.
## GitLab Duo Chat
- [GitLab Duo Chat feature documentation](../../user/gitlab_duo_chat.md)
- [Feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/430124)
<!-- from ../../user/gitlab_duo_chat.md -->
GitLab Duo Chat is your personal AI-powered assistant for boosting productivity.
It can assist various tasks of your daily work with the AI-generated content.
**Get started:**
1. On the left sidebar, select **Help** (**{question-o}**) > **GitLab Duo Chat**.
1. GitLab Duo Chat opens in the right sidebar. Enter your question or try one of these examples:
- `Where to find docs for CI job artifacts configuration?`
- `Explain the concept of a 'fork' in a concise manner.`
- `Provide step-by-step instructions on how to reset a user's password.`
## Generate issue description
- [Generate issue description feature documentation](../../user/ai_features.md#summarize-an-issue-with-issue-description-generation)
- [Feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/409844)
<!-- from ../../user/ai_features.md#summarize-an-issue-with-issue-description-generation -->
Write a short summary of an issue and GitLab Duo generates a description for you.
**Get started:**
1. Create a new issue.
1. Above the **Description** field, select **AI actions > Generate issue description**. Hint: AI actions can be found next to **Preview**
1. Write a short description and select **Submit**.
GitLab Duo replaces the issue description with AI-generated text.
## Test generation
- [Tests generation feature documentation](../../user/gitlab_duo_chat.md#write-tests-in-the-ide)
- [Feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/430124)
<!-- ../../user/gitlab_duo_chat.md#write-tests-in-the-ide -->
`/tests` is a special command to generate a testing suggestion for the selected code in your editor.
This feature is available in VS Code and the Web IDE only.
**Get started:**
1. On the left sidebar, select **Search or go to** and find your project.
1. Go to your file or directory.
1. Select **Edit > Open in Web IDE**.
1. Select code inside file.
1. On the left sidebar of Web IDE select the **GitLab Duo Chat** icon.
1. Enter `/tests` in AI dialog.
You can add additional instructions:
- `/tests using RSpec framework`
- `/tests markdown syntax`
1. GitLab Duo Chat returns a code block with an example RSpec test you can use for the code you selected.

View File

@ -0,0 +1,33 @@
---
stage: none
group: Contributor Success
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# GitLab Beta Program status
NOTE:
Last status update 2024-02-20.
These features may not be ready for production use and follow the [Experimental or Beta policy](../../policy/experiment-beta-support.md) of GitLab
WARNING:
The GitLab Beta Program is not operational yet. This page is in draft & in preparation for an upcoming launch.
## Features currently enrolled in the GitLab Beta Program
| Name | Status | Enrolled at |
|-----------------------------------------------------------------------------------|------------|---------------|
| [GitLab Duo - Git suggestions](features.md#git-suggestions) | [Experiment](../../policy/experiment-beta-support.md#experiment) | 2024-03-01 |
| [GitLab Duo - Code explanation](features.md#code-explanation) | [Experiment](../../policy/experiment-beta-support.md#experiment) | 2024-03-01 |
| [GitLab Duo - Chat](features.md#gitlab-duo-chat) | [Beta](../../policy/experiment-beta-support.md#beta) | 2024-03-01 |
| [GitLab Duo - Generate issue description](features.md#generate-issue-description) | [Experiment](../../policy/experiment-beta-support.md#experiment) | 2024-03-01 |
| [GitLab Duo - Test generation](features.md#test-generation) | [Beta](../../policy/experiment-beta-support.md#beta) | 2024-03-01 |
<!--
## Features previously enrolled
| Name | Status | Enrolled at | Removed at |
|-----------------------------------------------------------------------------------|------------|---------------| -------------|
| | | | |
-->

View File

@ -158,7 +158,7 @@ module Keeps
def each_feature_flag
all_feature_flag_files.map do |f|
yield(
Feature::Definition.new(f, YAML.load_file(f, permitted_classes: [Symbol], symbolize_names: true))
Feature::Definition.new(f, YAML.safe_load_file(f, permitted_classes: [Symbol], symbolize_names: true))
)
end
end

View File

@ -15,6 +15,7 @@ module Gitlab
def parse
::Gitlab::Ci::Reports::Sbom::Component.new(
ref: data['bom-ref'],
type: data['type'],
name: data['name'],
purl: purl,

View File

@ -39,6 +39,8 @@ module Gitlab
def parse_report
parse_metadata_properties
parse_components
parse_dependencies
report.ensure_ancestors!
end
def parse_metadata_properties
@ -64,6 +66,15 @@ module Gitlab
report.add_error("/components/#{index}/purl is invalid")
end
end
def parse_dependencies
data['dependencies']&.each do |dependency_data|
ref = dependency_data['ref']
dependency_data['dependsOn']&.each do |dependency_ref|
report.add_dependency(ref, dependency_ref)
end
end
end
end
end
end

View File

@ -7,16 +7,18 @@ module Gitlab
class Component
include Gitlab::Utils::StrongMemoize
attr_reader :component_type, :version, :path
attr_accessor :properties, :purl, :source_package_name
attr_reader :ref, :component_type, :version, :path
attr_accessor :properties, :purl, :source_package_name, :ancestors
def initialize(type:, name:, purl:, version:, properties: nil, source_package_name: nil)
def initialize(ref:, type:, name:, purl:, version:, properties: nil, source_package_name: nil)
@ref = ref
@component_type = type
@name = name
@purl = purl
@version = version
@properties = properties
@source_package_name = source_package_name
@ancestors = []
end
def <=>(other)

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Sbom
class DependencyAdjacencyList
def initialize
@adjacency_list = Hash.new { |hash, key| hash[key] = [] }
@component_info = {}
end
def add_edge(parent, child)
adjacency_list[child] << parent
end
def add_component_info(ref, name, version)
component_info[ref] = { name: name, version: version }
end
def ancestors_for(child)
ancestors_ref_for(child).filter_map do |ancestor_ref|
component_info[ancestor_ref]
end
end
private
def ancestors_ref_for(child)
adjacency_list[child]
end
attr_reader :adjacency_list, :component_info
end
end
end
end
end

View File

@ -23,6 +23,7 @@ module Gitlab
}
@components = []
@metadata = ::Gitlab::Ci::Reports::Sbom::Metadata.new
@dependencies = DependencyAdjacencyList.new
@errors = []
end
@ -40,11 +41,27 @@ module Gitlab
def add_component(component)
components << component
dependencies.add_component_info(component.ref, component.name, component.version)
end
def add_dependency(parent, child)
dependencies.add_edge(parent, child)
end
def ensure_ancestors!
components.each do |component|
component.ancestors = ancestors_for(component.ref)
end
end
private
def ancestors_for(ref)
dependencies.ancestors_for(ref)
end
attr_writer :source
attr_reader :dependencies
end
end
end

View File

@ -110,9 +110,7 @@ module Search
end
def show_wiki_search_tab?
return true if tab_enabled_for_project?(:wiki_blobs)
project.nil? && show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_wiki_tab)
tab_enabled_for_project?(:wiki_blobs)
end
def show_commits_search_tab?
@ -147,6 +145,7 @@ module Search
project.nil? || tab_enabled_for_project?(:milestones)
end
# deprecated - this method is being refactored and will eventually be removed
def feature_flag_tab_enabled?(flag)
group.present? || Feature.enabled?(flag, user, type: :ops)
end

View File

@ -52,6 +52,7 @@ module Tasks
puts "Generating the SHA256 hash for #{asset_files.size} Webpack-related assets..." if verbose
assets_sha256 = asset_files.map { |asset_file| Digest::SHA256.file(asset_file).hexdigest }.join
assets_sha256 += ENV.fetch('USE_NEW_CSS_PIPELINE', 'true')
Digest::SHA256.hexdigest(assets_sha256).tap { |sha256| puts "=> SHA256 generated in #{Time.now - start_time}: #{sha256}" if verbose }
end

View File

@ -27246,18 +27246,18 @@ msgstr ""
msgid "IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use JetBrains YouTrack as this project's issue tracker."
msgstr ""
msgid "IssueTracker|Use JetBrains YouTrack as this project's issue tracker. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use Redmine as the issue tracker. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use Redmine as this project's issue tracker."
msgstr ""
msgid "IssueTracker|Use YouTrack as this project's issue tracker."
msgstr ""
msgid "IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}"
msgstr ""
msgid "IssueTracker|Use a custom issue tracker as this project's issue tracker."
msgstr ""
@ -32788,9 +32788,6 @@ msgstr ""
msgid "No credit card data for matching"
msgstr ""
msgid "No credit card required."
msgstr ""
msgid "No data"
msgstr ""
@ -52785,7 +52782,7 @@ msgstr ""
msgid "Try again?"
msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgid "Try all GitLab has to offer for 30 days. No credit card required."
msgstr ""
msgid "Try changing or removing filters."

View File

@ -172,6 +172,11 @@ export async function compileAllStyles({ shouldWatch = false }) {
const sassCompilerOptions = {
loadPaths: resolveLoadPaths(),
logger: Logger.silent,
// For now we compress CSS directly with SASS if we do not watch
// We probably want to change this later if there are more
// post-processing steps, because we would compress
// _after_ things like auto-prefixer, etc. happened
style: shouldWatch ? 'expanded' : 'compressed',
};
let fileWatcher = null;
@ -218,8 +223,20 @@ export async function compileAllStyles({ shouldWatch = false }) {
return fileWatcher;
}
function shouldUseNewPipeline() {
return /^(true|t|yes|y|1|on)$/i.test(`${env.USE_NEW_CSS_PIPELINE}`);
// Mirroring gems/gitlab-utils/lib/gitlab/utils.rb
function shouldUseNewPipeline(defaultValue = true) {
if ([true, false, 0, 1].includes(env?.USE_NEW_CSS_PIPELINE)) {
return env.USE_NEW_CSS_PIPELINE;
}
if (/^(true|t|yes|y|1|on)$/i.test(`${env.USE_NEW_CSS_PIPELINE}`)) {
return true;
}
if (/^(false|f|no|n|0|off)$/i.test(`${env.USE_NEW_CSS_PIPELINE}`)) {
return false;
}
return defaultValue;
}
export function viteCSSCompilerPlugin({ shouldWatch = true }) {

View File

@ -49,6 +49,7 @@ variables:
QA_SUITES: "$QA_SUITES"
QA_TESTS: "$QA_TESTS"
KNAPSACK_TEST_FILE_PATTERN: "$KNAPSACK_TEST_FILE_PATTERN"
USE_NEW_CSS_PIPELINE: "true"
YML
)

View File

@ -4,6 +4,7 @@ FactoryBot.define do
factory :ci_reports_sbom_component, class: '::Gitlab::Ci::Reports::Sbom::Component' do
type { "library" }
sequence(:ref) { |n| "ref-#{n}" }
sequence(:name) { |n| "component-#{n}" }
sequence(:version) { |n| "v0.0.#{n}" }
@ -42,6 +43,7 @@ FactoryBot.define do
initialize_with do
::Gitlab::Ci::Reports::Sbom::Component.new(
ref: ref,
type: type,
name: name,
purl: purl,

View File

@ -65,10 +65,8 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
wait_for_requests
# rubocop:disable Capybara/TestidFinders -- Helper within_testid doesn't cover use case
expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-group-settings"]')
expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-group-settings"]') # rubocop:todo Capybara/TestidFinders -- Doesn't cover use case, see https://gitlab.com/gitlab-org/gitlab/-/issues/442224
.skipping :'link-in-text-block', :'heading-order'
# rubocop:enable Capybara/TestidFinders
end
it 'has a Duplicate packages section', :js do

View File

@ -110,12 +110,10 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
visit project_issue_path(project, issue)
wait_for_all_requests
# rubocop:disable Capybara/TestidFinders -- within_testid does not work here
expect(page).to be_axe_clean.within(locked_badge)
expect(page).to be_axe_clean.within(issuable_note_warning)
more_dropdown.click
expect(page).to be_axe_clean.within('[data-testid="lock-issue-toggle"] button')
# rubocop:enable Capybara/TestidFinders
expect(page).to be_axe_clean.within('[data-testid="lock-issue-toggle"] button') # rubocop:todo Capybara/TestidFinders -- Doesn't cover use case, see https://gitlab.com/gitlab-org/gitlab/-/issues/442224
end
end

View File

@ -221,34 +221,28 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio
nav_item = find_by_testid('nav-item', text: nav_item_title)
scroll_to(nav_item)
nav_item.hover
within(nav_item) do
pin_button = find_by_testid('nav-item-pin')
pin_button.click
wait_for_requests
end
find_by_testid('nav-item-pin', context: nav_item).click
wait_for_requests
end
def remove_pin(nav_item_title)
nav_item = find_by_testid('nav-item', text: nav_item_title)
scroll_to(nav_item)
nav_item.hover
within(nav_item) do
unpin_button = find_by_testid('nav-item-unpin')
unpin_button.click
wait_for_requests
end
find_by_testid('nav-item-unpin', context: nav_item).click
wait_for_requests
end
def drag_item(item, to:)
item.hover
within(item) do
drag_handle = find_by_testid('grip-icon')
# Reduce delay to make it less likely for draggables to
# change position during drag operation, which reduces
# flakiness.
drag_handle.drag_to(to, delay: 0.01)
wait_for_requests
end
# Reduce delay to make it less likely for draggables to
# change position during drag operation, which reduces
# flakiness.
find_by_testid('grip-icon', context: item).drag_to(to, delay: 0.01)
wait_for_requests
end
end

View File

@ -1,14 +1,6 @@
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
import * as tableUtils from '~/lib/utils/table_utility';
describe('table_utility', () => {
describe('thWidthClass', () => {
it('returns the width class including default table header classes', () => {
const width = 50;
expect(tableUtils.thWidthClass(width)).toBe(`gl-w-${width}p ${DEFAULT_TH_CLASSES}`);
});
});
describe('thWidthPercent', () => {
it('returns the width class including default table header classes', () => {
const width = 50;

View File

@ -8,7 +8,7 @@ describe('AssociationCountCard', () => {
const defaultPropsData = {
title: 'Groups',
iconName: 'group',
count: 1050,
count: '1000+',
linkHref: '/-/organizations/default/groups_and_projects?display=groups',
};
@ -28,7 +28,7 @@ describe('AssociationCountCard', () => {
const link = findLink();
expect(card.text()).toContain(defaultPropsData.title);
expect(card.text()).toContain('1k');
expect(card.text()).toContain('1000+');
expect(link.text()).toBe('View all');
expect(link.attributes('href')).toBe(defaultPropsData.linkHref);
});

View File

@ -7,9 +7,9 @@ describe('AssociationCounts', () => {
const defaultPropsData = {
associationCounts: {
groups: 10,
projects: 5,
users: 6,
groups: '10',
projects: '5',
users: '1000+',
},
groupsAndProjectsOrganizationPath: '/-/organizations/default/groups_and_projects',
usersOrganizationPath: '/-/organizations/default/users',

View File

@ -42,20 +42,27 @@ describe('SearchTypeIndicator', () => {
// all possible combinations
describe.each`
searchType | searchLevel | repository | showSearchTypeIndicator
${'advanced'} | ${'project'} | ${'master'} | ${'advanced-enabled'}
${'advanced'} | ${'project'} | ${'v0.1'} | ${'advanced-disabled'}
${'advanced'} | ${'group'} | ${'master'} | ${'advanced-enabled'}
${'advanced'} | ${'global'} | ${'master'} | ${'advanced-enabled'}
${'zoekt'} | ${'project'} | ${'master'} | ${'zoekt-enabled'}
${'zoekt'} | ${'project'} | ${'v0.1'} | ${'zoekt-disabled'}
${'zoekt'} | ${'group'} | ${'master'} | ${'zoekt-enabled'}
searchType | searchLevel | repository | scope | showSearchTypeIndicator
${'advanced'} | ${'project'} | ${'master'} | ${'blobs'} | ${'advanced-enabled'}
${'advanced'} | ${'project'} | ${'v0.1'} | ${'blobs'} | ${'advanced-disabled'}
${'advanced'} | ${'group'} | ${'master'} | ${'blobs'} | ${'advanced-enabled'}
${'advanced'} | ${'global'} | ${'master'} | ${'blobs'} | ${'advanced-enabled'}
${'zoekt'} | ${'project'} | ${'master'} | ${'blobs'} | ${'zoekt-enabled'}
${'zoekt'} | ${'project'} | ${'v0.1'} | ${'blobs'} | ${'zoekt-disabled'}
${'zoekt'} | ${'group'} | ${'master'} | ${'blobs'} | ${'zoekt-enabled'}
${'advanced'} | ${'project'} | ${'master'} | ${'issues'} | ${'advanced-enabled'}
${'advanced'} | ${'project'} | ${'v0.1'} | ${'issues'} | ${'advanced-enabled'}
${'advanced'} | ${'group'} | ${'master'} | ${'issues'} | ${'advanced-enabled'}
${'advanced'} | ${'global'} | ${'master'} | ${'issues'} | ${'advanced-enabled'}
${'zoekt'} | ${'project'} | ${'master'} | ${'issues'} | ${'advanced-enabled'}
${'zoekt'} | ${'project'} | ${'v0.1'} | ${'issues'} | ${'advanced-enabled'}
${'zoekt'} | ${'group'} | ${'master'} | ${'issues'} | ${'advanced-enabled'}
`(
'search type indicator for $searchType $searchLevel',
({ searchType, repository, showSearchTypeIndicator, searchLevel }) => {
'search type indicator for $searchType $searchLevel $scope',
({ searchType, repository, showSearchTypeIndicator, scope, searchLevel }) => {
beforeEach(() => {
createComponent({
query: { repository_ref: repository },
query: { repository_ref: repository, scope },
searchType,
searchLevel,
defaultBranchName: 'master',
@ -76,8 +83,9 @@ describe('SearchTypeIndicator', () => {
({ searchType, repository, showSearchTypeIndicator }) => {
beforeEach(() => {
createComponent({
query: { repository_ref: repository },
query: { repository_ref: repository, scope: 'blobs' },
searchType,
searchLevel: 'project',
defaultBranchName: 'master',
});
});
@ -97,7 +105,7 @@ describe('SearchTypeIndicator', () => {
`('documentation link for $searchType', ({ searchType, docsLink }) => {
beforeEach(() => {
createComponent({
query: { repository_ref: 'master' },
query: { repository_ref: 'master', scope: 'blobs' },
searchType,
searchLevel: 'project',
defaultBranchName: 'master',
@ -115,7 +123,7 @@ describe('SearchTypeIndicator', () => {
`('Syntax documentation $searchType', ({ searchType, syntaxdocsLink }) => {
beforeEach(() => {
createComponent({
query: { repository_ref: '000' },
query: { repository_ref: '000', scope: 'blobs' },
searchType,
searchLevel: 'project',
defaultBranchName: 'master',

View File

@ -27,6 +27,7 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Component, feature_category: :dependen
expect(component.purl.name).to eq("activesupport")
expect(component.properties).to be_nil
expect(component.source_package_name).to be_nil
expect(component.ref).to eq("pkg:gem/activesupport@5.1.4")
end
end

View File

@ -327,4 +327,22 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependen
end
end
end
context 'when cyclonedx report has no dependencies' do
it 'skips component processing' do
expect(report).not_to receive(:add_dependency)
parse!
end
end
context 'when report has dependencies' do
let(:report_data) { base_report_data.merge({ 'dependencies' => [{ ref: 'ref', dependsOn: ['dependency_ref'] }] }) }
it 'passes dependencies to report' do
expect(report).to receive(:add_dependency).with('ref', 'dependency_ref')
parse!
end
end
end

View File

@ -9,9 +9,11 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen
let(:purl) { Sbom::PackageUrl.new(type: purl_type, name: name, version: version) }
let(:version) { 'v0.0.1' }
let(:source_package_name) { 'source-component' }
let(:ref) { 'ref' }
subject(:component) do
described_class.new(
ref: ref,
type: component_type,
name: name,
purl: purl,
@ -168,6 +170,7 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen
with_them do
specify do
a = described_class.new(
ref: ref,
name: a_name,
type: a_type,
purl: a_purl,
@ -175,6 +178,7 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen
)
b = described_class.new(
ref: ref,
name: b_name,
type: b_type,
purl: b_purl,

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Sbom::DependencyAdjacencyList, feature_category: :dependency_management do
subject(:dependency_list) { described_class.new }
let(:child) { 'child-ref' }
describe 'without any data' do
it 'does not return any ancestor' do
expect(dependency_list.ancestors_for(child)).to be_empty
end
end
describe 'with only relationship data' do
let(:parent) { 'parent-ref' }
before do
dependency_list.add_edge(parent, child)
end
it 'does not return any ancestor' do
expect(dependency_list.ancestors_for(child)).to be_empty
end
context 'with component data' do
let(:component_data) { { name: 'component_name', version: 'component_version' } }
before do
dependency_list.add_component_info(parent, component_data[:name], component_data[:version])
end
it 'returns the ancestor' do
expect(dependency_list.ancestors_for(child)).to eq([component_data])
end
context 'with multiple ancestors' do
let(:component_data_2) { { name: 'component_name_2', version: 'component_version_2' } }
let(:parent_2) { 'parent_2-ref' }
before do
dependency_list.add_component_info(parent_2, component_data_2[:name], component_data_2[:version])
dependency_list.add_edge(parent_2, child)
end
it 'returns the ancestor' do
expect(dependency_list.ancestors_for(child)).to contain_exactly(component_data, component_data_2)
end
end
end
end
end

View File

@ -48,4 +48,20 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Report, feature_category: :dependency_
expect(report.components).to match_array(components)
end
end
describe 'ensure_ancestors!' do
let_it_be(:components) { create_list(:ci_reports_sbom_component, 3) }
let_it_be(:component_first) { components.first }
let_it_be(:component_last) { components.last }
let_it_be(:expected_value) { { name: component_first.name, version: component_first.version } }
it 'stores hierachies' do
components.each { |component| report.add_component(component) }
report.add_dependency(component_first.ref, component_last.ref)
report.ensure_ancestors!
expect(component_last.ancestors).to match_array([expected_value])
end
end
end

View File

@ -141,23 +141,19 @@ RSpec.describe Search::Navigation, feature_category: :global_search do
end
context 'for wiki tab' do
where(:feature_flag_enabled, :show_elasticsearch_tabs, :project, :tab_enabled, :condition) do
false | false | nil | true | true
false | false | nil | false | false
false | false | ref(:project_double) | false | false
false | true | nil | false | false
false | true | ref(:project_double) | false | false
true | false | nil | false | false
true | true | ref(:project_double) | false | false
where(:project, :group, :tab_enabled_for_project, :condition) do
nil | nil | false | false
nil | ref(:group_double) | false | false
ref(:project_double) | nil | true | true
ref(:project_double) | nil | false | false
end
with_them do
let(:options) { { show_elasticsearch_tabs: show_elasticsearch_tabs } }
let(:options) { {} }
it 'data item condition is set correctly' do
allow(search_navigation).to receive(:feature_flag_tab_enabled?)
.with(:global_search_wiki_tab).and_return(feature_flag_enabled)
allow(search_navigation).to receive(:tab_enabled_for_project?).with(:wiki_blobs).and_return(tab_enabled)
allow(search_navigation).to receive(:tab_enabled_for_project?)
.with(:wiki_blobs).and_return(tab_enabled_for_project)
expect(tabs[:wiki_blobs][:condition]).to eq(condition)
end

View File

@ -2,13 +2,15 @@
require 'spec_helper'
RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching, feature_category: :integrations do
include ReactiveCachingHelpers
include StubRequests
let(:teamcity_url) { 'https://gitlab.teamcity.com' }
let(:teamcity_full_url) { 'https://gitlab.teamcity.com/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' }
let_it_be(:project) { create(:project) }
let(:teamcity_full_url) { 'https://gitlab.teamcity.com/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' }
let(:teamcity_url) { 'https://gitlab.teamcity.com' }
it_behaves_like Integrations::HasAvatar
subject(:integration) do
described_class.create!(
@ -305,6 +307,14 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
end
end
describe '#attribution_notice' do
it do
expect(subject.attribution_notice)
.to eq('Copyright © 2024 JetBrains s.r.o. JetBrains TeamCity and the JetBrains TeamCity logo are registered ' \
'trademarks of JetBrains s.r.o.')
end
end
def stub_post_to_build_queue(branch:)
teamcity_full_url = "#{teamcity_url}/httpAuth/app/rest/buildQueue"
body ||= %(<build branchName=\"#{branch}\"><buildType id=\"foo\"/></build>)

View File

@ -2,7 +2,9 @@
require 'spec_helper'
RSpec.describe Integrations::Youtrack do
RSpec.describe Integrations::Youtrack, feature_category: :integrations do
it_behaves_like Integrations::HasAvatar
describe 'Validations' do
context 'when integration is active' do
before do
@ -43,4 +45,12 @@ RSpec.describe Integrations::Youtrack do
expect(subject.fields.pluck(:name)).to eq(%w[project_url issues_url])
end
end
describe '#attribution_notice' do
it do
expect(subject.attribution_notice)
.to eq('Copyright © 2024 JetBrains s.r.o. JetBrains YouTrack and the JetBrains YouTrack logo are registered ' \
'trademarks of JetBrains s.r.o.')
end
end
end

View File

@ -220,6 +220,12 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:mutation_work_item) { group_work_item }
it_behaves_like 'mutation updating work item labels'
context 'with quick action' do
let(:input) { { 'descriptionWidget' => { 'description' => "/remove_label ~\"#{existing_label.name}\"" } } }
it_behaves_like 'mutation updating work item labels'
end
end
end
@ -241,6 +247,14 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:mutation_work_item) { group_work_item }
it_behaves_like 'mutation updating work item labels'
context 'with quick action' do
let(:input) do
{ 'descriptionWidget' => { 'description' => "/labels ~\"#{label1.name}\" ~\"#{label2.name}\"" } }
end
it_behaves_like 'mutation updating work item labels'
end
end
end

View File

@ -113,7 +113,8 @@ RSpec.describe Packages::Pypi::CreatePackageService, :aggregate_failures, featur
context 'with an invalid metadata' do
let(:requires_python) { 'x' * 256 }
it_behaves_like 'returning an error service response', /Pypi package metadata invalid/ do
it_behaves_like 'returning an error service response',
message: 'Validation failed: Required python is too long (maximum is 255 characters)' do
it { is_expected.to have_attributes(reason: :invalid_parameter) }
end
end
@ -142,7 +143,8 @@ RSpec.describe Packages::Pypi::CreatePackageService, :aggregate_failures, featur
params[:md5_digest] = md5
end
it_behaves_like 'returning an error service response', /File name has already been taken/ do
it_behaves_like 'returning an error service response',
message: 'Validation failed: File name has already been taken' do
it { is_expected.to have_attributes(reason: :invalid_parameter) }
end

View File

@ -20,8 +20,9 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:commit) { create(:commit, project: project) }
let(:current_user) { developer }
let(:container) { project }
subject(:service) { described_class.new(project, current_user) }
subject(:service) { described_class.new(container: container, current_user: current_user) }
before_all do
public_project.add_developer(developer)
@ -860,8 +861,14 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'merge command' do
let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }
let(:merge_request) { create(:merge_request, source_project: repository_project) }
let(:service) do
described_class.new(
container: project,
current_user: developer,
params: { merge_request_diff_head_sha: merge_request.diff_head_sha }
)
end
it_behaves_like 'merge immediately command' do
let(:content) { '/merge' }
@ -881,7 +888,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'can not be merged when logged user does not have permissions' do
let(:service) { described_class.new(project, create(:user)) }
let(:service) { described_class.new(container: project, current_user: create(:user)) }
it_behaves_like 'failed command', 'Could not apply merge command.' do
let(:content) { "/merge" }
@ -890,7 +897,13 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'can not be merged when sha does not match' do
let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) }
let(:service) do
described_class.new(
container: project,
current_user: developer,
params: { merge_request_diff_head_sha: 'othersha' }
)
end
it_behaves_like 'failed command', 'Branch has been updated since the merge was requested.' do
let(:content) { "/merge" }
@ -900,7 +913,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'when sha is missing' do
let(:project) { repository_project }
let(:service) { described_class.new(project, developer, {}) }
let(:service) { described_class.new(container: project, current_user: developer) }
it_behaves_like 'failed command', 'The `/merge` quick action requires the SHA of the head of the branch.' do
let(:content) { "/merge" }
@ -1607,7 +1620,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'when non-member is creating a new issue' do
let(:service) { described_class.new(project, create(:user)) }
let(:service) { described_class.new(container: project, current_user: create(:user)) }
it_behaves_like 'confidential command' do
let(:content) { '/confidential' }
@ -1697,6 +1710,33 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
context '/label command' do
context 'when target is a group level work item' do
let_it_be(:new_group) { create(:group).tap { |g| g.add_developer(developer) } }
let_it_be(:group_level_work_item) { create(:work_item, :group_level, namespace: new_group) }
# this label should not be show on the list as belongs to another group
let_it_be(:invalid_label) { create(:group_label, title: 'not_from_group', group: group) }
let(:container) { new_group }
# This spec was introduced just to validate that the label finder scopes que query to a single group.
# The command checks that labels are available as part of the condition.
# Query was timing out in .com https://gitlab.com/gitlab-org/gitlab/-/issues/441123
it 'is not available when there are no labels associated with the group' do
expect(service.available_commands(group_level_work_item)).not_to include(a_hash_including(name: :label))
end
context 'when a label exists at the group level' do
before do
create(:group_label, group: new_group)
end
it 'is available' do
expect(service.available_commands(group_level_work_item)).to include(a_hash_including(name: :label))
end
end
end
end
context '/copy_metadata command' do
let(:todo_label) { create(:label, project: project, title: 'To Do') }
let(:inreview_label) { create(:label, project: project, title: 'In Review') }
@ -1707,7 +1747,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'when the user does not have permission' do
let(:guest) { create(:user) }
let(:service) { described_class.new(project, guest) }
let(:service) { described_class.new(container: project, current_user: guest) }
it 'is not available' do
expect(service.available_commands(issue)).not_to include(a_hash_including(name: :copy_metadata))
@ -1814,7 +1854,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'when current_user cannot :admin_issue' do
let(:visitor) { create(:user) }
let(:issue) { create(:issue, project: project, author: visitor) }
let(:service) { described_class.new(project, visitor) }
let(:service) { described_class.new(container: project, current_user: visitor) }
it_behaves_like 'failed command', 'Could not apply assign command.' do
let(:content) { "/assign @#{developer.username}" }
@ -1959,7 +1999,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context '/target_branch command' do
let(:non_empty_project) { create(:project, :repository) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
let(:service) { described_class.new(non_empty_project, developer) }
let(:service) { described_class.new(container: non_empty_project, current_user: developer) }
it 'updates target_branch if /target_branch command is executed' do
_, updates, _ = service.execute('/target_branch merge-test', merge_request)
@ -2164,7 +2204,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'when logged user cannot push code to the project' do
let(:project) { create(:project, :private) }
let(:service) { described_class.new(project, create(:user)) }
let(:service) { described_class.new(container: project, current_user: create(:user)) }
it_behaves_like 'failed command', 'Could not apply create_merge_request command.'
end
@ -2892,7 +2932,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
describe '#explain' do
let(:service) { described_class.new(project, developer) }
let(:service) { described_class.new(container: project, current_user: developer) }
let(:merge_request) { create(:merge_request, source_project: project) }
describe 'close command' do
@ -3477,7 +3517,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'when user cannot admin link' do
subject(:service) { described_class.new(project, create(:user)) }
subject(:service) { described_class.new(container: project, current_user: create(:user)) }
it_behaves_like 'command is not available'
end
@ -3526,7 +3566,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'when user cannot admin link' do
subject(:service) { described_class.new(project, create(:user)) }
subject(:service) { described_class.new(container: project, current_user: create(:user)) }
it_behaves_like 'command is not available'
end
@ -3573,7 +3613,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
context 'when user cannot admin link' do
subject(:service) { described_class.new(project, create(:user)) }
subject(:service) { described_class.new(container: project, current_user: create(:user)) }
it_behaves_like 'command is not available'
end
@ -3591,7 +3631,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
let_it_be(:guest) { create(:user) }
let(:issue) { build(:issue, project: public_project) }
let(:service) { described_class.new(project, guest) }
let(:service) { described_class.new(container: project, current_user: guest) }
before_all do
public_project.add_guest(guest)

View File

@ -226,7 +226,7 @@ RSpec.configure do |config|
# Gradually stop using rspec-retry
# See https://gitlab.com/gitlab-org/gitlab/-/issues/438388
%i[
lib mailers metrics_server
graphql haml_lint helpers initializers keeps lib mailers metrics_server
migrations models policies presenters rack_servers requests
routing rubocop scripts serializers services sidekiq sidekiq_cluster
spam support_specs tasks tooling uploaders validators views workers

View File

@ -2,16 +2,16 @@
module Features
module DomHelpers
def has_testid?(testid, **kwargs)
page.has_selector?("[data-testid='#{testid}']", **kwargs)
def has_testid?(testid, context: page, **kwargs)
context.has_selector?("[data-testid='#{testid}']", **kwargs)
end
def find_by_testid(testid, **kwargs)
page.find("[data-testid='#{testid}']", **kwargs)
def find_by_testid(testid, context: page, **kwargs)
context.find("[data-testid='#{testid}']", **kwargs)
end
def within_testid(testid, **kwargs, &block)
page.within("[data-testid='#{testid}']", **kwargs, &block)
def within_testid(testid, context: page, **kwargs, &block)
context.within("[data-testid='#{testid}']", **kwargs, &block)
end
end
end