diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8e58120dc6f..301695a2ff3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -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:
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 77986539403..f2bdb1bb6af 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -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;
diff --git a/app/assets/javascripts/lib/utils/table_utility.js b/app/assets/javascripts/lib/utils/table_utility.js
index 5d3aba9f4ed..f5a814c8fdc 100644
--- a/app/assets/javascripts/lib/utils/table_utility.js
+++ b/app/assets/javascripts/lib/utils/table_utility.js
@@ -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.
diff --git a/app/assets/javascripts/organizations/show/components/association_count_card.vue b/app/assets/javascripts/organizations/show/components/association_count_card.vue
index 0567f43132f..5ea8701cf9b 100644
--- a/app/assets/javascripts/organizations/show/components/association_count_card.vue
+++ b/app/assets/javascripts/organizations/show/components/association_count_card.vue
@@ -1,6 +1,5 @@
@@ -48,7 +42,7 @@ export default {
{{ formattedCount }}{{ count }}
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index e3b0db670b5..ae77638abdd 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -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',
diff --git a/app/assets/javascripts/search/topbar/components/search_type_indicator.vue b/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
index 362139bf64d..68049f35347 100644
--- a/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
+++ b/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
@@ -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 {
-
+
{{ content }}
-
+
{{ content }}
diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss
index 70553bfcecb..30230178672 100644
--- a/app/assets/stylesheets/framework/read_more.scss
+++ b/app/assets/stylesheets/framework/read_more.scss
@@ -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.
diff --git a/app/graphql/mutations/saved_replies/base.rb b/app/graphql/mutations/saved_replies/base.rb
index d8d46dbad7d..6fd9e03171c 100644
--- a/app/graphql/mutations/saved_replies/base.rb
+++ b/app/graphql/mutations/saved_replies/base.rb
@@ -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.'
diff --git a/app/graphql/mutations/saved_replies/destroy.rb b/app/graphql/mutations/saved_replies/destroy.rb
index c84c0eb3e22..b67bd11e0cb 100644
--- a/app/graphql/mutations/saved_replies/destroy.rb
+++ b/app/graphql/mutations/saved_replies/destroy.rb
@@ -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)
diff --git a/app/graphql/mutations/saved_replies/update.rb b/app/graphql/mutations/saved_replies/update.rb
index cbb06d85658..6acbdd8d770 100644
--- a/app/graphql/mutations/saved_replies/update.rb
+++ b/app/graphql/mutations/saved_replies/update.rb
@@ -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)
diff --git a/app/graphql/resolvers/custom_emoji_resolver.rb b/app/graphql/resolvers/custom_emoji_resolver.rb
index 1e39fafe486..f06320ef011 100644
--- a/app/graphql/resolvers/custom_emoji_resolver.rb
+++ b/app/graphql/resolvers/custom_emoji_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/saved_reply_resolver.rb b/app/graphql/resolvers/saved_reply_resolver.rb
index 1a5f2c9be78..f9e5ce16e11 100644
--- a/app/graphql/resolvers/saved_reply_resolver.rb
+++ b/app/graphql/resolvers/saved_reply_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/users/groups_resolver.rb b/app/graphql/resolvers/users/groups_resolver.rb
index 09c6b51cc3d..c01362e3b9f 100644
--- a/app/graphql/resolvers/users/groups_resolver.rb
+++ b/app/graphql/resolvers/users/groups_resolver.rb
@@ -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
diff --git a/app/graphql/types/saved_reply_type.rb b/app/graphql/types/saved_reply_type.rb
index 74b3796ef8a..d59b705a7d6 100644
--- a/app/graphql/types/saved_reply_type.rb
+++ b/app/graphql/types/saved_reply_type.rb
@@ -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.'
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 7687da35baa..a938c994b5a 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -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.'
diff --git a/app/graphql/types/users/saved_reply_type.rb b/app/graphql/types/users/saved_reply_type.rb
new file mode 100644
index 00000000000..bc313044906
--- /dev/null
+++ b/app/graphql/types/users/saved_reply_type.rb
@@ -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
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 913242ef9ac..8c8fce23696 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -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?
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index 4d825adb961..8beafcaa309 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -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
diff --git a/app/models/note.rb b/app/models/note.rb
index 955cc57af02..a401496a1eb 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -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
diff --git a/app/services/concerns/work_items/widgetable_service.rb b/app/services/concerns/work_items/widgetable_service.rb
index 1c7bbc41aeb..44b5f0f08f6 100644
--- a/app/services/concerns/work_items/widgetable_service.rb
+++ b/app/services/concerns/work_items/widgetable_service.rb
@@ -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
diff --git a/app/services/groups/autocomplete_service.rb b/app/services/groups/autocomplete_service.rb
index 5b9d60495e9..ad1cfc66d6d 100644
--- a/app/services/groups/autocomplete_service.rb
+++ b/app/services/groups/autocomplete_service.rb
@@ -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
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e508499a92b..b6bc79a613e 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -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
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 1b852710677..1fb210a1f47 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -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
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 74b0c96236b..3c2f5dcbbc5 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -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
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 11437ad90fc..8114044b019 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -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
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index b5f6bff756b..e5bbec57e09 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -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
diff --git a/app/views/projects/_sidebar.html.haml b/app/views/projects/_sidebar.html.haml
index 38793747196..781ae59a70c 100644
--- a/app/views/projects/_sidebar.html.haml
+++ b/app/views/projects/_sidebar.html.haml
@@ -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),
diff --git a/app/views/shared/integrations/teamcity/_help.html.haml b/app/views/shared/integrations/teamcity/_help.html.haml
new file mode 100644
index 00000000000..f18c20664d9
--- /dev/null
+++ b/app/views/shared/integrations/teamcity/_help.html.haml
@@ -0,0 +1,5 @@
+.info-well
+ .well-segment
+ = markdown integration.help
+ %p
+ %em= integration.attribution_notice
diff --git a/app/views/shared/integrations/youtrack/_help.html.haml b/app/views/shared/integrations/youtrack/_help.html.haml
new file mode 100644
index 00000000000..f18c20664d9
--- /dev/null
+++ b/app/views/shared/integrations/youtrack/_help.html.haml
@@ -0,0 +1,5 @@
+.info-well
+ .well-segment
+ = markdown integration.help
+ %p
+ %em= integration.attribution_notice
diff --git a/config/application.rb b/config/application.rb
index e3559db51bb..88310ba38e5 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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'
diff --git a/config/feature_flags/development/use_sync_service_token_worker.yml b/config/feature_flags/development/use_sync_service_token_worker.yml
deleted file mode 100644
index 6efe5cfa1ef..00000000000
--- a/config/feature_flags/development/use_sync_service_token_worker.yml
+++ /dev/null
@@ -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
diff --git a/db/migrate/20240212170304_add_ancestors_column_to_sbom_occurrences.rb b/db/migrate/20240212170304_add_ancestors_column_to_sbom_occurrences.rb
new file mode 100644
index 00000000000..459ce27da38
--- /dev/null
+++ b/db/migrate/20240212170304_add_ancestors_column_to_sbom_occurrences.rb
@@ -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
diff --git a/db/post_migrate/20240219135417_replace_old_fk_ci_build_trace_metadata_to_ci_job_artifacts.rb b/db/post_migrate/20240219135417_replace_old_fk_ci_build_trace_metadata_to_ci_job_artifacts.rb
new file mode 100644
index 00000000000..9cc815f3a12
--- /dev/null
+++ b/db/post_migrate/20240219135417_replace_old_fk_ci_build_trace_metadata_to_ci_job_artifacts.rb
@@ -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
diff --git a/db/post_migrate/20240219142421_replace_old_fk_ci_job_artifact_states_to_ci_job_artifacts.rb b/db/post_migrate/20240219142421_replace_old_fk_ci_job_artifact_states_to_ci_job_artifacts.rb
new file mode 100644
index 00000000000..22193d33480
--- /dev/null
+++ b/db/post_migrate/20240219142421_replace_old_fk_ci_job_artifact_states_to_ci_job_artifacts.rb
@@ -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
diff --git a/db/schema_migrations/20240212170304 b/db/schema_migrations/20240212170304
new file mode 100644
index 00000000000..77fb2e698c6
--- /dev/null
+++ b/db/schema_migrations/20240212170304
@@ -0,0 +1 @@
+83cb4871102a1096c688ef1f414c654581416479c1d082ba3540ae774d02ecfa
\ No newline at end of file
diff --git a/db/schema_migrations/20240219135417 b/db/schema_migrations/20240219135417
new file mode 100644
index 00000000000..310281965cf
--- /dev/null
+++ b/db/schema_migrations/20240219135417
@@ -0,0 +1 @@
+ee6b6d16269e1d4c1addac4c7d403f196f7b339f0bb593677f874f652e14b4ed
\ No newline at end of file
diff --git a/db/schema_migrations/20240219142421 b/db/schema_migrations/20240219142421
new file mode 100644
index 00000000000..91ae1315669
--- /dev/null
+++ b/db/schema_migrations/20240219142421
@@ -0,0 +1 @@
+0c728d47b305f6ac23d902de40560fcb689ed293e249aa2e2d17de72ef9c57dd
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8377e786150..1f6d216dfd7 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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;
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 622660ad499..5726fdad712 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7043,7 +7043,7 @@ Input type: `SavedReplyDestroyInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| `id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the saved reply. |
+| `id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the user saved reply. |
#### Fields
@@ -7063,7 +7063,7 @@ Input type: `SavedReplyUpdateInput`
| ---- | ---- | ----------- |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `content` | [`String!`](#string) | Content of the saved reply. |
-| `id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the saved reply. |
+| `id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the user saved reply. |
| `name` | [`String!`](#string) | Name of the saved reply. |
#### Fields
@@ -11389,6 +11389,30 @@ The edge type for [`GroupMember`](#groupmember).
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`GroupMember`](#groupmember) | The item at the end of the edge. |
+#### `GroupSavedReplyConnection`
+
+The connection type for [`GroupSavedReply`](#groupsavedreply).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `count` | [`Int!`](#int) | Total count of collection. |
+| `edges` | [`[GroupSavedReplyEdge]`](#groupsavedreplyedge) | A list of edges. |
+| `nodes` | [`[GroupSavedReply]`](#groupsavedreply) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `GroupSavedReplyEdge`
+
+The edge type for [`GroupSavedReply`](#groupsavedreply).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `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.
| `requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request access to namespace. |
| `requireTwoFactorAuthentication` | [`Boolean`](#boolean) | Indicates if all users in this group are required to set up two-factor authentication. |
| `rootStorageStatistics` | [`RootStorageStatistics`](#rootstoragestatistics) | Aggregated storage statistics of the namespace. Only available for root namespaces. |
+| `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. |
| `securityPolicyProject` | [`Project`](#project) | Security policy project assigned to the namespace. |
| `shareWithGroupLock` | [`Boolean`](#boolean) | Indicates if sharing a project with another group within this group is prevented. |
| `sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
@@ -20807,6 +20832,22 @@ four standard [pagination arguments](#connection-pagination-arguments):
| `upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Filter by upgrade status. |
| `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 |
+| ---- | ---- | ----------- |
+| `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.
| `releasesCount` | [`Int`](#int) | Total number of releases in all descendant projects of the group. |
| `releasesPercentage` | [`Int`](#int) | Percentage of the group's descendant projects that have at least one release. |
+### `GroupSavedReply`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `content` | [`String!`](#string) | Content of the saved reply. |
+| `id` | [`GroupsSavedReplyID!`](#groupssavedreplyid) | Global ID of the group saved reply. |
+| `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 |
| ---- | ---- | ----------- |
| `content` | [`String!`](#string) | Content of the saved reply. |
-| `id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the saved reply. |
+| `id` | [`UsersSavedReplyID!`](#userssavedreplyid) | Global ID of the user saved reply. |
| `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.
diff --git a/doc/development/gitlab_beta_program/features.md b/doc/development/gitlab_beta_program/features.md
new file mode 100644
index 00000000000..41330327fb5
--- /dev/null
+++ b/doc/development/gitlab_beta_program/features.md
@@ -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)
+
+
+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
+ ```
+
+ 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)
+
+
+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)
+
+
+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)
+
+
+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)
+
+
+`/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.
diff --git a/doc/development/gitlab_beta_program/index.md b/doc/development/gitlab_beta_program/index.md
new file mode 100644
index 00000000000..c02213a6585
--- /dev/null
+++ b/doc/development/gitlab_beta_program/index.md
@@ -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 |
+
+
diff --git a/keeps/delete_old_feature_flags.rb b/keeps/delete_old_feature_flags.rb
index 952245dc48f..bf2d074a659 100644
--- a/keeps/delete_old_feature_flags.rb
+++ b/keeps/delete_old_feature_flags.rb
@@ -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
diff --git a/lib/gitlab/ci/parsers/sbom/component.rb b/lib/gitlab/ci/parsers/sbom/component.rb
index 1a4aa5071ae..8bc038452a7 100644
--- a/lib/gitlab/ci/parsers/sbom/component.rb
+++ b/lib/gitlab/ci/parsers/sbom/component.rb
@@ -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,
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
index 9c48dd69a41..97648dd315d 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
@@ -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
diff --git a/lib/gitlab/ci/reports/sbom/component.rb b/lib/gitlab/ci/reports/sbom/component.rb
index 764078970c5..24ac9baf9e9 100644
--- a/lib/gitlab/ci/reports/sbom/component.rb
+++ b/lib/gitlab/ci/reports/sbom/component.rb
@@ -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)
diff --git a/lib/gitlab/ci/reports/sbom/dependency_adjacency_list.rb b/lib/gitlab/ci/reports/sbom/dependency_adjacency_list.rb
new file mode 100644
index 00000000000..fdec798059a
--- /dev/null
+++ b/lib/gitlab/ci/reports/sbom/dependency_adjacency_list.rb
@@ -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
diff --git a/lib/gitlab/ci/reports/sbom/report.rb b/lib/gitlab/ci/reports/sbom/report.rb
index 9a71c67388d..e9364e551e7 100644
--- a/lib/gitlab/ci/reports/sbom/report.rb
+++ b/lib/gitlab/ci/reports/sbom/report.rb
@@ -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
diff --git a/lib/search/navigation.rb b/lib/search/navigation.rb
index 482458ad6c6..4af69d1ffc7 100644
--- a/lib/search/navigation.rb
+++ b/lib/search/navigation.rb
@@ -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
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index fbeb0476533..ad70bc06e34 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d5ab5389916..63c0e7aeabf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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."
diff --git a/scripts/frontend/lib/compile_css.mjs b/scripts/frontend/lib/compile_css.mjs
index 90b90534298..8b59c17bab3 100644
--- a/scripts/frontend/lib/compile_css.mjs
+++ b/scripts/frontend/lib/compile_css.mjs
@@ -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 }) {
diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline
index e8efcaee740..1f32a33e6c1 100755
--- a/scripts/generate-e2e-pipeline
+++ b/scripts/generate-e2e-pipeline
@@ -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
)
diff --git a/spec/factories/ci/reports/sbom/components.rb b/spec/factories/ci/reports/sbom/components.rb
index 2f17460f9eb..12302843ca8 100644
--- a/spec/factories/ci/reports/sbom/components.rb
+++ b/spec/factories/ci/reports/sbom/components.rb
@@ -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,
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index 934234aa171..83cc8af3b47 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -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
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index 6b955ced06e..7541d7658b3 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -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
diff --git a/spec/features/nav/pinned_nav_items_spec.rb b/spec/features/nav/pinned_nav_items_spec.rb
index 07958bf3d66..74dfe572149 100644
--- a/spec/features/nav/pinned_nav_items_spec.rb
+++ b/spec/features/nav/pinned_nav_items_spec.rb
@@ -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
diff --git a/spec/frontend/lib/utils/table_utility_spec.js b/spec/frontend/lib/utils/table_utility_spec.js
index df9006f4909..d6544fa8327 100644
--- a/spec/frontend/lib/utils/table_utility_spec.js
+++ b/spec/frontend/lib/utils/table_utility_spec.js
@@ -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;
diff --git a/spec/frontend/organizations/show/components/association_count_card_spec.js b/spec/frontend/organizations/show/components/association_count_card_spec.js
index 752a02110b6..b7bf4ab7436 100644
--- a/spec/frontend/organizations/show/components/association_count_card_spec.js
+++ b/spec/frontend/organizations/show/components/association_count_card_spec.js
@@ -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);
});
diff --git a/spec/frontend/organizations/show/components/association_counts_spec.js b/spec/frontend/organizations/show/components/association_counts_spec.js
index e9ebf5e6889..30e55bd0e72 100644
--- a/spec/frontend/organizations/show/components/association_counts_spec.js
+++ b/spec/frontend/organizations/show/components/association_counts_spec.js
@@ -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',
diff --git a/spec/frontend/search/topbar/components/search_type_indicator_spec.js b/spec/frontend/search/topbar/components/search_type_indicator_spec.js
index d69ca6dfb16..139576aafcb 100644
--- a/spec/frontend/search/topbar/components/search_type_indicator_spec.js
+++ b/spec/frontend/search/topbar/components/search_type_indicator_spec.js
@@ -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',
diff --git a/spec/lib/gitlab/ci/parsers/sbom/component_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/component_spec.rb
index 17cc3fb21f5..fc37a4de681 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/component_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/component_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
index 0c9a6be7100..f65cd27c6f1 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
index 242c9e4071b..5aaa443807f 100644
--- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
@@ -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,
diff --git a/spec/lib/gitlab/ci/reports/sbom/dependency_adjacency_list_spec.rb b/spec/lib/gitlab/ci/reports/sbom/dependency_adjacency_list_spec.rb
new file mode 100644
index 00000000000..91277decba3
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/sbom/dependency_adjacency_list_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
index 5d281f6ed76..7f8c375cb2b 100644
--- a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
@@ -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
diff --git a/spec/lib/search/navigation_spec.rb b/spec/lib/search/navigation_spec.rb
index 88413ae8ac7..faf0cef9d10 100644
--- a/spec/lib/search/navigation_spec.rb
+++ b/spec/lib/search/navigation_spec.rb
@@ -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
diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb
index 2294e687296..daea849f6aa 100644
--- a/spec/models/integrations/teamcity_spec.rb
+++ b/spec/models/integrations/teamcity_spec.rb
@@ -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 ||= %()
diff --git a/spec/models/integrations/youtrack_spec.rb b/spec/models/integrations/youtrack_spec.rb
index 69dda244413..6599052e1ef 100644
--- a/spec/models/integrations/youtrack_spec.rb
+++ b/spec/models/integrations/youtrack_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 9f5d0035b18..867c6fdfbb8 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -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
diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb
index 4c6ccac3e9a..d0f2a26d61c 100644
--- a/spec/services/packages/pypi/create_package_service_spec.rb
+++ b/spec/services/packages/pypi/create_package_service_spec.rb
@@ -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
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 8664f13ab16..e75deacd147 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -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)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 52a81818184..bb054b274ae 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -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
diff --git a/spec/support/helpers/features/dom_helpers.rb b/spec/support/helpers/features/dom_helpers.rb
index 619f16f5e6d..96d02e8f03d 100644
--- a/spec/support/helpers/features/dom_helpers.rb
+++ b/spec/support/helpers/features/dom_helpers.rb
@@ -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