diff --git a/app/assets/javascripts/todos/components/todo_item_title.vue b/app/assets/javascripts/todos/components/todo_item_title.vue
index a2b1d2a4601..4732da05e56 100644
--- a/app/assets/javascripts/todos/components/todo_item_title.vue
+++ b/app/assets/javascripts/todos/components/todo_item_title.vue
@@ -2,6 +2,7 @@
import { GlIcon } from '@gitlab/ui';
import StatusBadge from '~/issuable/components/status_badge.vue';
import { STATUS_OPEN, TYPE_ALERT, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
+import { s__ } from '~/locale';
import {
TODO_ACTION_TYPE_MEMBER_ACCESS_REQUESTED,
TODO_TARGET_TYPE_ALERT,
@@ -11,6 +12,8 @@ import {
TODO_TARGET_TYPE_MERGE_REQUEST,
TODO_TARGET_TYPE_PIPELINE,
TODO_TARGET_TYPE_SSH_KEY,
+ TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED,
+ TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED,
} from '../constants';
export default {
@@ -40,6 +43,12 @@ export default {
isMemberAccessRequestAction() {
return this.todo.action === TODO_ACTION_TYPE_MEMBER_ACCESS_REQUESTED;
},
+ isDuoActionType() {
+ return (
+ this.todo.action === TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED ||
+ this.todo.action === TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED
+ );
+ },
issuableType() {
if (this.isMergeRequest) {
return TYPE_MERGE_REQUEST;
@@ -79,6 +88,8 @@ export default {
* Full title line of the todo title + full reference, joined by a middot
*/
todoTitle() {
+ if (this.isDuoActionType) return s__('Todos|Getting started with GitLab Duo');
+
return [this.targetName, this.targetFullReference].filter(Boolean).join(' · ');
},
/**
@@ -130,6 +141,8 @@ export default {
return '';
},
icon() {
+ if (this.isDuoActionType) return 'book';
+
switch (this.todo.targetType) {
case TODO_TARGET_TYPE_ISSUE:
return 'issues';
diff --git a/app/assets/javascripts/todos/constants.js b/app/assets/javascripts/todos/constants.js
index 084b697cc70..cc2e7410bac 100644
--- a/app/assets/javascripts/todos/constants.js
+++ b/app/assets/javascripts/todos/constants.js
@@ -27,6 +27,8 @@ export const TODO_ACTION_TYPE_OKR_CHECKIN_REQUESTED = 'okr_checkin_requested';
export const TODO_ACTION_TYPE_ADDED_APPROVER = 'added_approver';
export const TODO_ACTION_TYPE_SSH_KEY_EXPIRED = 'ssh_key_expired';
export const TODO_ACTION_TYPE_SSH_KEY_EXPIRING_SOON = 'ssh_key_expiring_soon';
+export const TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED = 'duo_pro_access_granted';
+export const TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED = 'duo_enterprise_access_granted';
export const TODO_EMPTY_TITLE_POOL = [
s__("Todos|Good job! Looks like you don't have anything left on your To-Do List"),
diff --git a/app/assets/stylesheets/framework/top_bar.scss b/app/assets/stylesheets/framework/top_bar.scss
index 1ec8cb44f95..96c14e3c595 100644
--- a/app/assets/stylesheets/framework/top_bar.scss
+++ b/app/assets/stylesheets/framework/top_bar.scss
@@ -24,4 +24,3 @@
.top-app-header {
top: $calc-application-header-height;
}
-
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index da0a15b4748..156961c504d 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -257,8 +257,6 @@
bottom: $calc-application-footer-height;
z-index: $zindex-dropdown-menu;
height: var(--rca-bar-height);
- padding-left: $super-sidebar-width;
- padding-right: $right-sidebar-collapsed-width;
background: var(--gl-background-color-default);
border-top: 1px solid var(--gl-border-color-default);
@apply gl-transition-padding;
@@ -272,7 +270,12 @@
.rca-bar-content {
max-width: $limited-layout-width;
padding: 0 $container-margin;
- margin: 0 auto;
+ margin-left: $super-sidebar-width + 0.5rem;
+ margin-right: auto;
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ margin: 0 auto;
+ }
}
.loader-animation {
diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb
index c5df34e9d38..2fc17605826 100644
--- a/app/graphql/types/todo_action_enum.rb
+++ b/app/graphql/types/todo_action_enum.rb
@@ -17,5 +17,7 @@ module Types
value 'added_approver', value: 13, description: 'User was added as an approver.'
value 'ssh_key_expired', value: 14, description: 'SSH key of the user has expired.'
value 'ssh_key_expiring_soon', value: 15, description: 'SSH key of the user will expire soon.'
+ value 'duo_pro_access_granted', value: 16, description: 'Access to Duo Pro has been granted to the user.'
+ value 'duo_enterprise_access_granted', value: 17, description: 'Access to Duo Enterprise has been granted to the user.'
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 51a114a2679..a69caff04ab 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -413,7 +413,11 @@ class Commit
message_body = ["(cherry picked from commit #{sha})"]
if merged_merge_request?(user)
- commits_in_merge_request = merged_merge_request(user).commits
+ commits_in_merge_request = if Feature.enabled?(:optimized_commit_storage, project)
+ merged_merge_request(user).commits(load_from_gitaly: true)
+ else
+ merged_merge_request(user).commits
+ end
if commits_in_merge_request.present?
message_body << ""
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index e9679298c88..55eccb3c6a2 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -345,11 +345,19 @@ class MergeRequestDiff < ApplicationRecord
end
def first_commit
- commits.last
+ if Feature.enabled?(:optimized_commit_storage, project)
+ commits(load_from_gitaly: true).last
+ else
+ commits.last
+ end
end
def last_commit
- commits.first
+ if Feature.enabled?(:optimized_commit_storage, project)
+ commits(load_from_gitaly: true).first
+ else
+ commits.first
+ end
end
def base_commit
@@ -847,7 +855,7 @@ class MergeRequestDiff < ApplicationRecord
end
def save_commits
- MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse)
+ MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse, skip_commit_data: Feature.enabled?(:optimized_commit_storage, project))
self.class.uncached { merge_request_diff_commits.reset }
end
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 15b00beeac4..49f66b73834 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -39,7 +39,7 @@ class MergeRequestDiffCommit < ApplicationRecord
# Deprecated; use `bulk_insert!` from `BulkInsertSafe` mixin instead.
# cf. https://gitlab.com/gitlab-org/gitlab/issues/207989 for progress
- def self.create_bulk(merge_request_diff_id, commits)
+ def self.create_bulk(merge_request_diff_id, commits, skip_commit_data: false)
commit_hashes, user_tuples = prepare_commits_for_bulk_insert(commits)
users = MergeRequest::DiffCommitUser.bulk_find_or_create(user_tuples)
@@ -59,7 +59,7 @@ class MergeRequestDiffCommit < ApplicationRecord
commit_hash = commit_hash
.except(:author_name, :author_email, :committer_name, :committer_email, :extended_trailers)
- commit_hash.merge(
+ commit_hash = commit_hash.merge(
commit_author_id: author.id,
committer_id: committer.id,
merge_request_diff_id: merge_request_diff_id,
@@ -69,6 +69,12 @@ class MergeRequestDiffCommit < ApplicationRecord
committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]),
trailers: Gitlab::Json.dump(commit_hash.fetch(:trailers, {}))
)
+
+ if skip_commit_data
+ commit_hash.merge(message: '')
+ else
+ commit_hash
+ end
end
ApplicationRecord.legacy_bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 1dc92e13cd3..50e7de3fdff 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -26,6 +26,8 @@ class Todo < ApplicationRecord
ADDED_APPROVER = 13 # This is an EE-only feature,
SSH_KEY_EXPIRED = 14
SSH_KEY_EXPIRING_SOON = 15
+ DUO_PRO_ACCESS_GRANTED = 16 # This is an EE-only feature,
+ DUO_ENTERPRISE_ACCESS_GRANTED = 17 # This is an EE-only feature,
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -42,11 +44,20 @@ class Todo < ApplicationRecord
OKR_CHECKIN_REQUESTED => :okr_checkin_requested,
ADDED_APPROVER => :added_approver,
SSH_KEY_EXPIRED => :ssh_key_expired,
- SSH_KEY_EXPIRING_SOON => :ssh_key_expiring_soon
+ SSH_KEY_EXPIRING_SOON => :ssh_key_expiring_soon,
+ DUO_PRO_ACCESS_GRANTED => :duo_pro_access_granted,
+ DUO_ENTERPRISE_ACCESS_GRANTED => :duo_enterprise_access_granted
}.freeze
ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze
+ PARENTLESS_ACTION_TYPES = [
+ DUO_PRO_ACCESS_GRANTED,
+ DUO_ENTERPRISE_ACCESS_GRANTED,
+ SSH_KEY_EXPIRED,
+ SSH_KEY_EXPIRING_SOON
+ ].freeze
+
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
@@ -68,8 +79,8 @@ class Todo < ApplicationRecord
validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- validates :project, presence: true, unless: -> { group_id || for_ssh_key? }
- validates :group, presence: true, unless: -> { project_id || for_ssh_key? }
+ validates :project, presence: true, unless: -> { group_id || parentless_type? }
+ validates :group, presence: true, unless: -> { project_id || parentless_type? }
scope :pending, -> { with_state(:pending) }
scope :snoozed, -> { where(arel_table[:snoozed_until].gt(Time.current)) }
@@ -353,6 +364,14 @@ class Todo < ApplicationRecord
target_type == Key.name
end
+ def for_duo_access_granted?
+ action == DUO_PRO_ACCESS_GRANTED || action == DUO_ENTERPRISE_ACCESS_GRANTED
+ end
+
+ def parentless_type?
+ PARENTLESS_ACTION_TYPES.include?(action)
+ end
+
# override to return commits, which are not active record
def target
if for_commit?
@@ -379,6 +398,8 @@ class Todo < ApplicationRecord
def target_url
return if target.nil?
+ return build_duo_getting_started_url if for_duo_access_granted?
+
case target
when WorkItem
build_work_item_target_url
@@ -413,6 +434,10 @@ class Todo < ApplicationRecord
private
+ def build_duo_getting_started_url
+ ::Gitlab::Routing.url_helpers.help_page_path('user/get_started/getting_started_gitlab_duo.md')
+ end
+
def build_work_item_target_url
::Gitlab::UrlBuilder.build(
target,
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 4d1aee4924a..2784a414df0 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -387,20 +387,20 @@ class TodoService
end
def create_assignment_todo(target, author, old_assignees = [])
- if target.assignees.any?
- project = target.project
- assignees = target.assignees - old_assignees
- attributes = attributes_for_todo(project, target, author, Todo::ASSIGNED)
+ return unless target.assignees.any?
- create_todos(assignees, attributes, target_namespace(target), project)
- end
+ project = target.project
+ assignees = target.assignees - old_assignees
+ attributes = attributes_for_todo(project, target, author, ::Todo::ASSIGNED)
+
+ create_todos(assignees, attributes, target_namespace(target), project)
end
def create_reviewer_todo(target, author, old_reviewers = [])
- if target.reviewers.any?
- reviewers = target.reviewers - old_reviewers
- create_request_review_todo(target, author, reviewers)
- end
+ return unless target.reviewers.any?
+
+ reviewers = target.reviewers - old_reviewers
+ create_request_review_todo(target, author, reviewers)
end
def create_mention_todos(parent, target, author, note = nil, skip_users = [])
diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml
deleted file mode 100644
index fdb29455d87..00000000000
--- a/app/views/projects/network/_head.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.row-content-block.second-block.content-component-block.gl-px-0.gl-py-3
- .gl-w-max.gl-max-w-full
- #js-graph-ref-switcher{ data: { project_id: @project.id, ref: @ref, network_path: project_network_path(@project, @ref, ref_type: @ref_type) } }
-
- .oneline
- = _("You can move around the graph by using the arrow keys.")
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index af74bbefb2e..142399330fa 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,18 +1,28 @@
-- breadcrumb_title _("Graph")
-- page_title _("Graph"), @ref
+- title = _("Repository graph")
+- breadcrumb_title title
+- page_title title, @ref
- network_path = project_network_path(@project, @ref, ref_type: @ref_type)
-= render "head"
-.gl-mt-5
- .project-network.gl-border-1.gl-border-solid.gl-border-gray-300
- .controls.gl-bg-strong.gl-p-2.gl-text-base.gl-text-subtle.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-300
- = form_tag network_path, method: :get, class: 'form-inline network-form' do |f|
- = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control gl-form-input input-mx-250 search-sha gl-mr-2'
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, icon: 'search', button_options: {'aria-label': _("Search"), 'title': _("Search")})
- .form-group{ class: 'gl-ml-5 !gl-mb-3' }
+
+.project-network-header.gl-flex.gl-flex-col.gl-overflow-hidden
+ = render ::Layouts::PageHeadingComponent.new(title) do |c|
+ - c.with_description do
+ = _("You can move around the graph by using the arrow keys.")
+
+ .project-network
+ .gl-flex.gl-flex-wrap.gl-items-start.gl-gap-3.gl-p-5.gl-bg-subtle.gl-border-t.gl-border-b
+ .gl-min-w-26
+ #js-graph-ref-switcher{ data: { project_id: @project.id, ref: @ref, network_path: project_network_path(@project, @ref, ref_type: @ref_type) } }
+
+ = form_tag network_path, method: :get, class: 'gl-grow gl-flex gl-flex-wrap gl-gap-3 gl-items-center network-form' do |f|
+ .gl-flex
+ = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control gl-form-input search-sha !gl-rounded-r-none'
+ = render Pajamas::ButtonComponent.new(type: :submit, icon: 'search', button_options: { class: '!gl-rounded-l-none -gl-ml-[1px]', 'aria-label': _("Search"), 'title': _("Search") })
+
+ .gl-mt-3
= render Pajamas::CheckboxTagComponent.new(name: :filter_ref, checked: @options[:filter_ref]) do |c|
- c.with_label do
= _("Begin with the selected commit")
- if @commit
- .network-graph.gl-bg-white.gl-overflow-scroll.gl-overflow-x-hidden{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
- = gl_loading_icon(size: 'md', css_class: 'gl-mt-3')
+ .network-graph.gl-overflow-scroll.gl-overflow-x-hidden{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
+ = gl_loading_icon(size: 'md', css_class: 'gl-mt-5')
diff --git a/config/feature_flags/gitlab_com_derisk/duo_seat_assignment_todo.yml b/config/feature_flags/gitlab_com_derisk/duo_seat_assignment_todo.yml
new file mode 100644
index 00000000000..8303869c199
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/duo_seat_assignment_todo.yml
@@ -0,0 +1,9 @@
+---
+name: duo_seat_assignment_todo
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/507338
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177019
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/514937
+milestone: '17.10'
+group: group::acquisition
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/config/feature_flags/gitlab_com_derisk/optimized_commit_storage.yml b/config/feature_flags/gitlab_com_derisk/optimized_commit_storage.yml
new file mode 100644
index 00000000000..e49bd7f7c15
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/optimized_commit_storage.yml
@@ -0,0 +1,9 @@
+---
+name: optimized_commit_storage
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/517497
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181958
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/520259
+milestone: '17.10'
+group: group::source code
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/db/migrate/20250108095918_create_ai_duo_chat_events.rb b/db/migrate/20250108095918_create_ai_duo_chat_events.rb
index a96f34b070f..637990d21da 100644
--- a/db/migrate/20250108095918_create_ai_duo_chat_events.rb
+++ b/db/migrate/20250108095918_create_ai_duo_chat_events.rb
@@ -6,7 +6,7 @@ class CreateAiDuoChatEvents < Gitlab::Database::Migration[2.2]
def up
# rubocop:disable Migration/Datetime -- "timestamp" is a column name
- create_table :ai_duo_chat_events, # rubocop:disable Migration/EnsureFactoryForTable -- code_suggestion_event
+ create_table :ai_duo_chat_events,
options: 'PARTITION BY RANGE (timestamp)',
primary_key: [:id, :timestamp] do |t|
t.bigserial :id, null: false
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index f08f2c943ef..2f83508928b 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -43326,6 +43326,8 @@ Values for sorting timelogs.
|
`assigned` | User was assigned. |
|
`build_failed` | Build triggered by the user failed. |
|
`directly_addressed` | User was directly addressed. |
+|
`duo_enterprise_access_granted` | Access to Duo Enterprise has been granted to the user. |
+|
`duo_pro_access_granted` | Access to Duo Pro has been granted to the user. |
|
`marked` | User added a to-do item. |
|
`member_access_requested` | Group or project access requested from the user. |
|
`mentioned` | User was mentioned. |
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index e1526d0480f..b780827a813 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -103,15 +103,6 @@ updating the active and current stable releases only, with no backports. Factors
the very low likelihood of exploitation, the low impact of the vulnerability, the complexity of security fixes and
the eventual risk to stability. We always address high and critical security issues with a patch release.
-In cases where a strategic user has a requirement to test a feature before it is
-officially released, we can offer to create a Release Candidate (RC) version that
-includes the specific feature. This should be needed only in extreme cases and can be requested for
-consideration by raising an issue in the [release/tasks](https://gitlab.com/gitlab-org/release/tasks/-/issues/new?issuable_template=Backporting-request) issue tracker.
-It is important to note that the Release Candidate contains other features and changes as
-it is not possible to easily isolate a specific feature (similar reasons as noted above). The
-Release Candidate is no different than any code that is deployed to GitLab.com or is publicly
-accessible.
-
### Backporting to older releases
Backporting to more than one stable release is usually reserved for [security fixes](#patch-releases).
diff --git a/doc/user/clusters/agent/managed_kubernetes_resources.md b/doc/user/clusters/agent/managed_kubernetes_resources.md
index f5f3bb33919..a7ee47c6fff 100644
--- a/doc/user/clusters/agent/managed_kubernetes_resources.md
+++ b/doc/user/clusters/agent/managed_kubernetes_resources.md
@@ -18,12 +18,6 @@ title: GitLab-managed Kubernetes resources
{{< /history >}}
-{{< alert type="flag" >}}
-
-The availability of this feature is controlled by a feature flag. For more information, see the history.
-
-{{< /alert >}}
-
Use GitLab-managed Kubernetes resources to provision Kubernetes resources with environment templates. An environment template can:
- Create namespaces and service accounts automatically for new environments
diff --git a/doc/user/group/saml_sso/troubleshooting.md b/doc/user/group/saml_sso/troubleshooting.md
index f65d3e93667..e2a96b6277a 100644
--- a/doc/user/group/saml_sso/troubleshooting.md
+++ b/doc/user/group/saml_sso/troubleshooting.md
@@ -317,7 +317,7 @@ Here are possible causes and solutions:
| Cause | Solution |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| When a user account with the email address already exists in GitLab, but the user does not have the SAML identity tied to their account. | The user needs to [link their account](_index.md#user-access-and-management). |
+| If a GitLab user account exists with the same email address, but the account is not associated with a SAML identity. | On GitLab.com, the user needs to [link their account](_index.md#user-access-and-management). On GitLab Self-Managed, administrators can configure the instance to [automatically link the SAML identity with the GitLab user account](../../../integration/saml.md#link-saml-identity-for-an-existing-user) when they first sign in. |
User accounts are created in one of the following ways:
diff --git a/lib/gitlab/http_router/rule_context.rb b/lib/gitlab/http_router/rule_context.rb
index 1e062203538..3b05a17b99a 100644
--- a/lib/gitlab/http_router/rule_context.rb
+++ b/lib/gitlab/http_router/rule_context.rb
@@ -25,7 +25,7 @@ module Gitlab
ALLOWED_ROUTER_RULE_ACTIONS = %w[classify proxy].freeze
# We do not expect a type for `proxy` rules
ROUTER_RULE_ACTIONS_WITHOUT_TYPE = %w[proxy].freeze
- ALLOWED_ROUTER_RULE_TYPES = %w[FIRST_CELL SESSION_PREFIX].freeze
+ ALLOWED_ROUTER_RULE_TYPES = %w[FIRST_CELL SESSION_PREFIX CELL_ID].freeze
private
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6c6e66f3727..46405b939ff 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -60260,6 +60260,9 @@ msgstr ""
msgid "Todos|For one hour"
msgstr ""
+msgid "Todos|Getting started with GitLab Duo"
+msgstr ""
+
msgid "Todos|Give yourself a pat on the back!"
msgstr ""
@@ -60471,6 +60474,12 @@ msgstr ""
msgid "Todos|You"
msgstr ""
+msgid "Todos|You now have access to AI-powered features. Boost your productivity with Code Suggestions and GitLab Duo Chat"
+msgstr ""
+
+msgid "Todos|You now have access to AI-powered features. Boost your productivity with Code Suggestions, GitLab Duo Chat, Vulnerability Explanation, and more"
+msgstr ""
+
msgid "Todos|Your SSH key has expired"
msgstr ""
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index e84bbf382ad..b0cebb80a58 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :groups_and_proje
it 'filters select tag' do
switch_ref_to('v1.0.0')
- expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false
+ expect(page).to have_css 'title', text: 'Repository graph · v1.0.0', visible: false
page.within '.network-graph' do
expect(page).to have_content 'Change some files'
end
diff --git a/spec/frontend/todos/components/todo_item_body_spec.js b/spec/frontend/todos/components/todo_item_body_spec.js
index db054423152..072970d5490 100644
--- a/spec/frontend/todos/components/todo_item_body_spec.js
+++ b/spec/frontend/todos/components/todo_item_body_spec.js
@@ -17,6 +17,8 @@ import {
TODO_ACTION_TYPE_UNMERGEABLE,
TODO_ACTION_TYPE_SSH_KEY_EXPIRED,
TODO_ACTION_TYPE_SSH_KEY_EXPIRING_SOON,
+ TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED,
+ TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED,
} from '~/todos/constants';
import { SAML_HIDDEN_TODO } from '../mock_data';
@@ -61,22 +63,24 @@ describe('TodoItemBody', () => {
describe('correct text for actionName', () => {
it.each`
- actionName | text | showsAuthor
- ${TODO_ACTION_TYPE_ADDED_APPROVER} | ${'set you as an approver.'} | ${true}
- ${TODO_ACTION_TYPE_APPROVAL_REQUIRED} | ${'set you as an approver.'} | ${true}
- ${TODO_ACTION_TYPE_ASSIGNED} | ${'assigned you.'} | ${true}
- ${TODO_ACTION_TYPE_BUILD_FAILED} | ${'The pipeline failed.'} | ${false}
- ${TODO_ACTION_TYPE_DIRECTLY_ADDRESSED} | ${'mentioned you.'} | ${true}
- ${TODO_ACTION_TYPE_MARKED} | ${'added a to-do item'} | ${true}
- ${TODO_ACTION_TYPE_MEMBER_ACCESS_REQUESTED} | ${'has requested access to group Foo'} | ${true}
- ${TODO_ACTION_TYPE_MENTIONED} | ${'mentioned you.'} | ${true}
- ${TODO_ACTION_TYPE_MERGE_TRAIN_REMOVED} | ${'Removed from Merge Train.'} | ${false}
- ${TODO_ACTION_TYPE_OKR_CHECKIN_REQUESTED} | ${'requested an OKR update for Foo'} | ${true}
- ${TODO_ACTION_TYPE_REVIEW_REQUESTED} | ${'requested a review.'} | ${true}
- ${TODO_ACTION_TYPE_REVIEW_SUBMITTED} | ${'reviewed your merge request.'} | ${true}
- ${TODO_ACTION_TYPE_UNMERGEABLE} | ${'Could not merge.'} | ${false}
- ${TODO_ACTION_TYPE_SSH_KEY_EXPIRED} | ${'Your SSH key has expired.'} | ${false}
- ${TODO_ACTION_TYPE_SSH_KEY_EXPIRING_SOON} | ${'Your SSH key is expiring soon.'} | ${false}
+ actionName | text | showsAuthor
+ ${TODO_ACTION_TYPE_ADDED_APPROVER} | ${'set you as an approver.'} | ${true}
+ ${TODO_ACTION_TYPE_APPROVAL_REQUIRED} | ${'set you as an approver.'} | ${true}
+ ${TODO_ACTION_TYPE_ASSIGNED} | ${'assigned you.'} | ${true}
+ ${TODO_ACTION_TYPE_BUILD_FAILED} | ${'The pipeline failed.'} | ${false}
+ ${TODO_ACTION_TYPE_DIRECTLY_ADDRESSED} | ${'mentioned you.'} | ${true}
+ ${TODO_ACTION_TYPE_MARKED} | ${'added a to-do item'} | ${true}
+ ${TODO_ACTION_TYPE_MEMBER_ACCESS_REQUESTED} | ${'has requested access to group Foo'} | ${true}
+ ${TODO_ACTION_TYPE_MENTIONED} | ${'mentioned you.'} | ${true}
+ ${TODO_ACTION_TYPE_MERGE_TRAIN_REMOVED} | ${'Removed from Merge Train.'} | ${false}
+ ${TODO_ACTION_TYPE_OKR_CHECKIN_REQUESTED} | ${'requested an OKR update for Foo'} | ${true}
+ ${TODO_ACTION_TYPE_REVIEW_REQUESTED} | ${'requested a review.'} | ${true}
+ ${TODO_ACTION_TYPE_REVIEW_SUBMITTED} | ${'reviewed your merge request.'} | ${true}
+ ${TODO_ACTION_TYPE_UNMERGEABLE} | ${'Could not merge.'} | ${false}
+ ${TODO_ACTION_TYPE_SSH_KEY_EXPIRED} | ${'Your SSH key has expired.'} | ${false}
+ ${TODO_ACTION_TYPE_SSH_KEY_EXPIRING_SOON} | ${'Your SSH key is expiring soon.'} | ${false}
+ ${TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED} | ${'You now have access to AI-powered features. Boost your productivity with Code Suggestions and GitLab Duo Chat'} | ${false}
+ ${TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED} | ${'You now have access to AI-powered features. Boost your productivity with Code Suggestions, GitLab Duo Chat, Vulnerability Explanation, and more.'} | ${false}
`('renders "$text" for the "$actionName" action', ({ actionName, text, showsAuthor }) => {
createComponent({ action: actionName, memberAccessType: 'group' });
expect(wrapper.text()).toContain(text);
@@ -98,6 +102,14 @@ describe('TodoItemBody', () => {
});
});
+ it.each([
+ TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED,
+ TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED,
+ ])('when todo action is `%s`, avatar is not shown', (action) => {
+ createComponent({ action });
+ expect(wrapper.findComponent(GlAvatarLink).exists()).toBe(false);
+ });
+
describe('when todo has a note', () => {
it('renders note text', () => {
createComponent({ note: { bodyFirstLineHtml: '
This is a note
' } });
diff --git a/spec/frontend/todos/components/todo_item_title_spec.js b/spec/frontend/todos/components/todo_item_title_spec.js
index 9a1b3f203da..e8cc0b2ec6c 100644
--- a/spec/frontend/todos/components/todo_item_title_spec.js
+++ b/spec/frontend/todos/components/todo_item_title_spec.js
@@ -10,6 +10,8 @@ import {
TODO_TARGET_TYPE_MERGE_REQUEST,
TODO_TARGET_TYPE_PIPELINE,
TODO_TARGET_TYPE_SSH_KEY,
+ TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED,
+ TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED,
} from '~/todos/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { DESIGN_TODO, MR_BUILD_FAILED_TODO } from '../mock_data';
@@ -56,6 +58,11 @@ describe('TodoItemTitle', () => {
'Important issue › Screenshot_2024-11-22_at_16.11.25.png · Flightjs / Flight #35',
DESIGN_TODO,
],
+ [
+ 'to-do for duo pro access granted',
+ 'Getting started with GitLab Duo',
+ { ...mockToDo, action: TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED },
+ ],
])(`renders %s as %s`, (_a, b, c) => {
createComponent(c);
expect(wrapper.findByTestId('todo-title').text()).toBe(b);
@@ -85,4 +92,21 @@ describe('TodoItemTitle', () => {
}
});
});
+
+ describe('correct icon for action', () => {
+ it.each`
+ action | icon | showsIcon
+ ${TODO_ACTION_TYPE_DUO_PRO_ACCESS_GRANTED} | ${'book'} | ${true}
+ ${TODO_ACTION_TYPE_DUO_ENTERPRISE_ACCESS_GRANTED} | ${'book'} | ${true}
+ `('renders "$icon" for the "$action" action', ({ action, icon, showsIcon }) => {
+ createComponent({ ...mockToDo, action });
+
+ const glIcon = wrapper.findComponent(GlIcon);
+ expect(glIcon.exists()).toBe(showsIcon);
+
+ if (showsIcon) {
+ expect(glIcon.props('name')).toBe(icon);
+ }
+ });
+ });
});
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index c21e500b5a5..06f062e572e 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -557,6 +557,14 @@ EOS
let(:commit) { project.commit('video') }
it { expect(commit.cherry_pick_message(user)).to include("\n\n(cherry picked from commit 88790590ed1337ab189bccaa355f068481c90bec)") }
+
+ context 'when "optimized_commit_storage" feature flag is disabled' do
+ before do
+ stub_feature_flags(optimized_commit_storage: false)
+ end
+
+ it { expect(commit.cherry_pick_message(user)).to include("\n\n(cherry picked from commit 88790590ed1337ab189bccaa355f068481c90bec)") }
+ end
end
context 'of a merge commit' do
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 3f1933727ae..b19eae43356 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -26,14 +26,17 @@ RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow d
it 'returns the same results as Commit#to_hash, except for parent_ids' do
commit_from_repo = project.repository.commit(subject.sha)
- commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: [])
+ commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: [], message: '')
expect(subject.to_hash).to eq(commit_from_repo_hash)
end
end
describe '.create_bulk' do
+ subject { described_class.create_bulk(merge_request_diff_id, commits, skip_commit_data: skip_commit_data) }
+
let(:merge_request_diff_id) { merge_request.merge_request_diff.id }
+ let(:skip_commit_data) { false }
let(:commits) do
[
project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e'),
@@ -68,8 +71,6 @@ RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow d
]
end
- subject { described_class.create_bulk(merge_request_diff_id, commits) }
-
it 'inserts the commits into the database en masse' do
expect(ApplicationRecord).to receive(:legacy_bulk_insert)
.with(described_class.table_name, rows)
@@ -92,6 +93,19 @@ RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow d
expect(commit_row.committer).to eq(commit_user_row)
end
+ context 'when "skip_commit_data: true"' do
+ let(:skip_commit_data) { true }
+
+ it 'inserts the commits into the database en masse' do
+ rows_with_empty_messages = rows.map { |h| h.merge(message: '') }
+
+ expect(ApplicationRecord).to receive(:legacy_bulk_insert)
+ .with(described_class.table_name, rows_with_empty_messages)
+
+ subject
+ end
+ end
+
context 'with dates larger than the DB limit' do
let(:commits) do
# This commit's date is "Sun Aug 17 07:12:55 292278994 +0000"
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index db6bf1d8703..a592e24ccec 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -57,6 +57,20 @@ RSpec.describe MergeRequestDiff, feature_category: :code_review_workflow do
it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
it { expect(subject.patch_id_sha).to eq('f14ae956369247901117b8b7d237c9dc605898c5') }
+ it 'creates commits with empty messages' do
+ expect(subject.commits).to all(have_attributes(message: ''))
+ end
+
+ context 'when feature flag "optimized_commit_storage" is disabled' do
+ before do
+ stub_feature_flags(optimized_commit_storage: false)
+ end
+
+ it 'creates commits with messages' do
+ expect(subject.commits).to all(have_attributes(message: be_present))
+ end
+ end
+
it 'calls GraphqlTriggers.merge_request_diff_generated' do
merge_request = create(:merge_request, :skip_diff_creation)
@@ -1244,12 +1258,32 @@ RSpec.describe MergeRequestDiff, feature_category: :code_review_workflow do
it 'returns first commit' do
expect(diff_with_commits.first_commit.sha).to eq(diff_with_commits.merge_request_diff_commits.last.sha)
end
+
+ context 'when "optimized_commit_storage" feature flag is disabled' do
+ before do
+ stub_feature_flags(optimized_commit_storage: false)
+ end
+
+ it 'returns first commit' do
+ expect(diff_with_commits.first_commit.sha).to eq(diff_with_commits.merge_request_diff_commits.last.sha)
+ end
+ end
end
describe '#last_commit' do
it 'returns last commit' do
expect(diff_with_commits.last_commit.sha).to eq(diff_with_commits.merge_request_diff_commits.first.sha)
end
+
+ context 'when "optimized_commit_storage" feature flag is disabled' do
+ before do
+ stub_feature_flags(optimized_commit_storage: false)
+ end
+
+ it 'returns last commit' do
+ expect(diff_with_commits.last_commit.sha).to eq(diff_with_commits.merge_request_diff_commits.first.sha)
+ end
+ end
end
describe '#includes_any_commits?' do
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index edd8f1011f5..434811e0c7d 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Todo, feature_category: :notifications do
let(:issue) { create(:issue) }
+ let_it_be(:user) { create(:user) }
describe 'relationships' do
it { is_expected.to belong_to(:author).class_name("User") }
@@ -25,6 +26,41 @@ RSpec.describe Todo, feature_category: :notifications do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:author) }
+ context "for project and/or group" do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.new(project: project, group: group) }
+
+ where(:project?, :group?) do
+ true | true
+ true | false
+ false | true
+ end
+
+ with_them do
+ it "are valid" do
+ subject.project = project? ? project : nil
+ subject.group = group? ? group : nil
+ subject.validate
+
+ expect(subject.errors[:project]).to be_empty
+ expect(subject.errors[:group]).to be_empty
+ end
+ end
+
+ it "are both are missing" do
+ subject.project = nil
+ subject.group = nil
+ subject.validate
+
+ expect(subject.errors.messages[:project].first).to eq("can't be blank")
+ expect(subject.errors.messages[:group].first).to eq("can't be blank")
+ end
+ end
+
context 'for commits' do
subject { described_class.new(target_type: 'Commit') }
@@ -38,6 +74,24 @@ RSpec.describe Todo, feature_category: :notifications do
it { is_expected.to validate_presence_of(:target_id) }
it { is_expected.not_to validate_presence_of(:commit_id) }
end
+
+ context 'for parentless types' do
+ where(:action_type) do
+ [
+ [Todo::DUO_PRO_ACCESS_GRANTED],
+ [Todo::SSH_KEY_EXPIRED]
+ ]
+ end
+
+ with_them do
+ context "for #{params[:action_type]} should not require a target" do
+ subject { described_class.new(target: user, action: action_type) }
+
+ it { is_expected.not_to validate_presence_of(:project) }
+ it { is_expected.not_to validate_presence_of(:group) }
+ end
+ end
+ end
end
describe '#body' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index e6414b57367..5f9986ada5a 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1808,7 +1808,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
it 'returns a 200 when merge request is valid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
- commit = merge_request.commits.first
+ commit = merge_request.merge_request_diff.last_commit
expect_successful_response_with_paginated_array
expect(json_response.size).to eq(merge_request.commits.size)
@@ -1817,14 +1817,15 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
expect(json_response.first['parent_ids']).to be_present
end
- context 'when commits_from_gitaly feature flag is disabled' do
+ context 'when commits_from_gitaly and optimized_commit_storage feature flags are disabled' do
before do
stub_feature_flags(commits_from_gitaly: false)
+ stub_feature_flags(optimized_commit_storage: false)
end
it 'returns a 200 without parent_ids' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
- commit = merge_request.commits.first
+ commit = merge_request.merge_request_diff.last_commit
expect_successful_response_with_paginated_array
expect(json_response.size).to eq(merge_request.commits.size)
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
index 0d3e5e5016e..4eb9e963805 100644
--- a/spec/requests/application_controller_spec.rb
+++ b/spec/requests/application_controller_spec.rb
@@ -131,6 +131,36 @@ RSpec.describe ApplicationController, type: :request, feature_category: :shared
end
end
+ context 'for classify action by SESSION_PREFIX' do
+ let(:headers) do
+ {
+ 'X-Gitlab-Http-Router-Rule-Action' => 'classify',
+ 'X-Gitlab-Http-Router-Rule-Type' => 'SESSION_PREFIX'
+ }
+ end
+
+ it 'increments the counter with labels' do
+ expect { perform_request }.to change {
+ http_router_rule_counter.get(rule_action: 'classify', rule_type: 'SESSION_PREFIX')
+ }.by(1)
+ end
+ end
+
+ context 'for classify action by CELL_ID' do
+ let(:headers) do
+ {
+ 'X-Gitlab-Http-Router-Rule-Action' => 'classify',
+ 'X-Gitlab-Http-Router-Rule-Type' => 'CELL_ID'
+ }
+ end
+
+ it 'increments the counter with labels' do
+ expect { perform_request }.to change {
+ http_router_rule_counter.get(rule_action: 'classify', rule_type: 'CELL_ID')
+ }.by(1)
+ end
+ end
+
context 'for proxy action' do
let(:headers) do
{
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 307d738f120..04c09a1d748 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -890,10 +890,10 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
target_project: @project
)
- commits = draft_merge_request.commits
+ commits = draft_merge_request.commits(load_from_gitaly: true)
oldrev = commits.last.id
newrev = commits.first.id
- draft_commit = draft_merge_request.commits.find(&:draft?)
+ draft_commit = commits.find(&:draft?)
refresh_service.execute(oldrev, newrev, 'refs/heads/wip')