diff --git a/Gemfile b/Gemfile
index 363aec7b029..68010756679 100644
--- a/Gemfile
+++ b/Gemfile
@@ -402,7 +402,7 @@ end
group :development, :test do
gem 'deprecation_toolkit', '~> 1.5.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'bullet', '~> 7.1.1' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'bullet', '~> 7.1.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'parser', '~> 3.2', '>= 3.2.2.4' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'pry-byebug' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'pry-rails', '~> 0.3.9' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -434,7 +434,7 @@ group :development, :test do
gem 'knapsack', '~> 1.21.1', feature_category: :tooling
gem 'crystalball', '~> 0.7.0', require: false, feature_category: :tooling
- gem 'test_file_finder', '~> 0.1.3', feature_category: :tooling
+ gem 'test_file_finder', '~> 0.2.1', feature_category: :tooling
gem 'simple_po_parser', '~> 1.1.6', require: false # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 44e1da6987e..cc5c849c8ec 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -64,7 +64,7 @@
{"name":"bootsnap","version":"1.17.0","platform":"ruby","checksum":"6b0ea4dd68f0d424968dcd13953c3f04b13a19a8761c540d3af13507fcfa1347"},
{"name":"browser","version":"5.3.1","platform":"ruby","checksum":"62745301701ff2c6c5d32d077bb12532b20be261929dcb52c6781ed0d5658b3c"},
{"name":"builder","version":"3.2.4","platform":"ruby","checksum":"99caf08af60c8d7f3a6b004029c4c3c0bdaebced6c949165fe98f1db27fbbc10"},
-{"name":"bullet","version":"7.1.1","platform":"ruby","checksum":"ad7789d9ad2bfe772f96620ba8f927e756c74525f2c03e7843d3518ce50e5b9c"},
+{"name":"bullet","version":"7.1.2","platform":"ruby","checksum":"429725c174cb74ca0ae99b9720bf22cab80be59ee9401805f7ecc9ac62cbb3bb"},
{"name":"bundler-audit","version":"0.9.1","platform":"ruby","checksum":"bdc716fc21cd8652a6507b137e5bc51f5e0e4f6f106a114ab004c89d0200bd3d"},
{"name":"byebug","version":"11.1.3","platform":"ruby","checksum":"2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b"},
{"name":"capybara","version":"3.39.2","platform":"ruby","checksum":"d6f0ca5f30897e64789428d4b047a0df105815a302069913578ac35d5ca99884"},
@@ -637,7 +637,7 @@
{"name":"terminal-table","version":"3.0.2","platform":"ruby","checksum":"f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91"},
{"name":"terser","version":"1.0.2","platform":"ruby","checksum":"80c2e0bc7e2db4e12e8529658f9e0820e13d685ae67d745bf981f269743bb28e"},
{"name":"test-prof","version":"1.2.3","platform":"ruby","checksum":"c52a40194cb30f399ed3eb6beb4c45b5daad8b8eb418e8ef69089e4dc7e01fd6"},
-{"name":"test_file_finder","version":"0.1.4","platform":"ruby","checksum":"bc36d8339eac4fb9dc36514a7c5f4d389ac2fb6d010716fc715c5c8fbb98eacd"},
+{"name":"test_file_finder","version":"0.2.1","platform":"ruby","checksum":"a5e9b369d80c76aefbb609acf5e11d89a048f35e565de3cc261c20112f0fcdb3"},
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
{"name":"thor","version":"1.2.2","platform":"ruby","checksum":"2f93c652828cba9fcf4f65f5dc8c306f1a7317e05aad5835a13740122c17f24c"},
{"name":"thread_safe","version":"0.3.6","platform":"java","checksum":"bb28394cd0924c068981adee71f36a81c85c92e7d74d3f62372bd51489a0e0c2"},
diff --git a/Gemfile.lock b/Gemfile.lock
index f33f819954a..e8ff3d24c35 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -329,7 +329,7 @@ GEM
msgpack (~> 1.2)
browser (5.3.1)
builder (3.2.4)
- bullet (7.1.1)
+ bullet (7.1.2)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.1)
@@ -1596,8 +1596,8 @@ GEM
terser (1.0.2)
execjs (>= 0.3.0, < 3)
test-prof (1.2.3)
- test_file_finder (0.1.4)
- faraday (~> 1.0)
+ test_file_finder (0.2.1)
+ faraday (>= 1.0, < 3.0, != 2.0.0)
text (1.3.1)
thor (1.2.2)
thread_safe (0.3.6)
@@ -1771,7 +1771,7 @@ DEPENDENCIES
better_errors (~> 2.10.1)
bootsnap (~> 1.17.0)
browser (~> 5.3.1)
- bullet (~> 7.1.1)
+ bullet (~> 7.1.2)
bundler-audit (~> 0.9.1)
bundler-checksum (~> 0.1.0)!
capybara (~> 3.39, >= 3.39.2)
@@ -2036,7 +2036,7 @@ DEPENDENCIES
telesignenterprise (~> 2.2)
terser (= 1.0.2)
test-prof (~> 1.2.3)
- test_file_finder (~> 0.1.3)
+ test_file_finder (~> 0.2.1)
thrift (>= 0.16.0)
timfel-krb5-auth (~> 0.8)
toml-rb (~> 2.2.0)
diff --git a/app/components/projects/ml/models_index_component.rb b/app/components/projects/ml/models_index_component.rb
index 57900165ad1..5754c2a1fa9 100644
--- a/app/components/projects/ml/models_index_component.rb
+++ b/app/components/projects/ml/models_index_component.rb
@@ -3,10 +3,11 @@
module Projects
module Ml
class ModelsIndexComponent < ViewComponent::Base
- attr_reader :paginator
+ attr_reader :paginator, :model_count
- def initialize(paginator:)
+ def initialize(paginator:, model_count:)
@paginator = paginator
+ @model_count = model_count
end
private
@@ -14,7 +15,8 @@ module Projects
def view_model
vm = {
models: models_view_model,
- page_info: page_info_view_model
+ page_info: page_info_view_model,
+ model_count: model_count
}
Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
@@ -26,7 +28,8 @@ module Projects
name: m.name,
version: m.latest_version_name,
version_count: m.version_count,
- path: m.latest_package_path
+ version_package_path: m.latest_package_path,
+ version_path: m.latest_version_path
}
end
end
diff --git a/app/controllers/projects/ml/models_controller.rb b/app/controllers/projects/ml/models_controller.rb
index 795cf67ce62..68a8b7a1686 100644
--- a/app/controllers/projects/ml/models_controller.rb
+++ b/app/controllers/projects/ml/models_controller.rb
@@ -15,9 +15,11 @@ module Projects
.transform_keys(&:underscore)
.permit(:name, :order_by, :sort)
- @paginator = ::Projects::Ml::ModelFinder.new(@project, find_params)
- .execute
- .keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
+ finder = ::Projects::Ml::ModelFinder.new(@project, find_params)
+
+ @paginator = finder.execute.keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
+
+ @model_count = finder.count
end
def show; end
diff --git a/app/finders/projects/ml/model_finder.rb b/app/finders/projects/ml/model_finder.rb
index 9feff3c1ad4..57e0620c7a7 100644
--- a/app/finders/projects/ml/model_finder.rb
+++ b/app/finders/projects/ml/model_finder.rb
@@ -3,6 +3,8 @@
module Projects
module Ml
class ModelFinder
+ include Gitlab::Utils::StrongMemoize
+
VALID_ORDER_BY = %w[name created_at id].freeze
VALID_SORT = %w[asc desc].freeze
@@ -12,16 +14,26 @@ module Projects
end
def execute
+ relation
+ end
+
+ def count
+ relation.length
+ end
+
+ private
+
+ def relation
@models = ::Ml::Model
- .by_project(project)
- .including_latest_version
- .with_version_count
+ .by_project(project)
+ .including_latest_version
+ .including_project
+ .with_version_count
@models = by_name
ordered
end
-
- private
+ strong_memoize_attr :relation
def by_name
return models unless params[:name].present?
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index bb587ec87f5..b6f7e9a0639 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -22,6 +22,7 @@ module Ml
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :including_project, -> { includes(:project) }
scope :with_version_count, -> {
left_outer_joins(:versions)
.select("ml_models.*, count(ml_model_versions.id) as version_count")
diff --git a/app/presenters/ml/model_presenter.rb b/app/presenters/ml/model_presenter.rb
index 324701be2c6..24d30af1d4e 100644
--- a/app/presenters/ml/model_presenter.rb
+++ b/app/presenters/ml/model_presenter.rb
@@ -5,7 +5,7 @@ module Ml
presents ::Ml::Model, as: :model
def latest_version_name
- model.latest_version&.version
+ latest_version&.version
end
def version_count
@@ -15,13 +15,21 @@ module Ml
end
def latest_package_path
- return unless model.latest_version&.package_id.present?
+ latest_version&.package_path
+ end
- Gitlab::Routing.url_helpers.project_package_path(model.project, model.latest_version.package_id)
+ def latest_version_path
+ latest_version&.path
end
def path
- Gitlab::Routing.url_helpers.project_ml_model_path(model.project, model.id)
+ project_ml_model_path(model.project, model.id)
+ end
+
+ private
+
+ def latest_version
+ model.latest_version&.present
end
end
end
diff --git a/app/presenters/ml/model_version_presenter.rb b/app/presenters/ml/model_version_presenter.rb
index 90e2e3003cb..210b213ca2a 100644
--- a/app/presenters/ml/model_version_presenter.rb
+++ b/app/presenters/ml/model_version_presenter.rb
@@ -9,11 +9,17 @@ module Ml
end
def path
- Gitlab::Routing.url_helpers.project_ml_model_version_path(
+ project_ml_model_version_path(
model_version.model.project,
model_version.model,
model_version
)
end
+
+ def package_path
+ return unless model_version.package_id.present?
+
+ project_package_path(model_version.project, model_version.package_id)
+ end
end
end
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index 1890addf692..3f972e747b9 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -9,7 +9,12 @@ module MergeRequests
def initialize(project:, current_user:, changes:, push_options:, params: {})
super(project: project, current_user: current_user, params: params)
- @target_project = @project.default_merge_request_target
+ @target_project = if push_options[:target_project]
+ Project.find_by_full_path(push_options[:target_project])
+ else
+ @project.default_merge_request_target
+ end
+
@changes = Gitlab::ChangesList.new(changes)
@push_options = push_options
@errors = []
@@ -63,6 +68,10 @@ module MergeRequests
return
end
+ unless project == target_project || project.in_fork_network_of?(target_project)
+ errors << "Projects #{project.full_path} and #{target_project.full_path} are not in the same network"
+ end
+
unless target_project.merge_requests_enabled?
errors << "Merge requests are not enabled for project #{target_project.full_path}"
end
diff --git a/app/views/projects/ml/models/index.html.haml b/app/views/projects/ml/models/index.html.haml
index 08f0db257ae..ffe7ee3397e 100644
--- a/app/views/projects/ml/models/index.html.haml
+++ b/app/views/projects/ml/models/index.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title s_('ModelRegistry|Model registry')
- page_title s_('ModelRegistry|Model registry')
-= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator))
+= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator, model_count: @model_count))
diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml
new file mode 100644
index 00000000000..cd85f7e4ab2
--- /dev/null
+++ b/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml
@@ -0,0 +1,7 @@
+---
+migration_job_name: DeleteInvalidProtectedBranchMergeAccessLevels
+description: Remove rows from protected_branch_merge_access_levels for groups that do not have project_group_links to the project for the associated protected branch
+feature_category: source_code_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486
+milestone: 16.6
+queued_migration_version: 20231016173129
diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml
new file mode 100644
index 00000000000..dd92e35f26f
--- /dev/null
+++ b/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml
@@ -0,0 +1,7 @@
+---
+migration_job_name: DeleteInvalidProtectedBranchPushAccessLevels
+description: Remove rows from protected_branch_push_access_levels for groups that do not have project_group_links to the project for the associated protected branch
+feature_category: source_code_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486
+milestone: 16.6
+queued_migration_version: 20231016194927
diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml
new file mode 100644
index 00000000000..0c406c7650b
--- /dev/null
+++ b/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml
@@ -0,0 +1,7 @@
+---
+migration_job_name: DeleteInvalidProtectedTagCreateAccessLevels
+description: Remove rows from protected_tag_create_access_levels for groups that do not have project_group_links to the project for the associated protected tag
+feature_category: source_code_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486
+milestone: 16.6
+queued_migration_version: 20231016194943
diff --git a/db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb b/db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb
new file mode 100644
index 00000000000..0d8fbdfea00
--- /dev/null
+++ b/db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddTemporaryIndexToMergeAccessLevels < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.6'
+
+ INDEX_NAME = 'tmp_idx_protected_branch_merge_access_levels_on_id_with_group'
+
+ def up
+ # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843
+ add_concurrent_index(
+ :protected_branch_merge_access_levels,
+ %i[id],
+ where: 'group_id IS NOT NULL',
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :protected_branch_merge_access_levels,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb b/db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb
new file mode 100644
index 00000000000..3f4009d783c
--- /dev/null
+++ b/db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class QueueDeleteInvalidProtectedBranchMergeAccessLevels < Gitlab::Database::Migration[2.1]
+ MIGRATION = "DeleteInvalidProtectedBranchMergeAccessLevels"
+ DELAY_INTERVAL = 2.minutes
+ MAX_BATCH_SIZE = 10_000
+ BATCH_SIZE = 5_000
+ SUB_BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :protected_branch_merge_access_levels,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231016173129',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :protected_branch_merge_access_levels, :id, [])
+ end
+end
diff --git a/db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb b/db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb
new file mode 100644
index 00000000000..91599051fd4
--- /dev/null
+++ b/db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddTemporaryIndexToPushAccessLevels < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.6'
+ INDEX_NAME = 'tmp_idx_protected_branch_push_access_levels_on_id_with_group'
+
+ def up
+ # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843
+ add_concurrent_index(
+ :protected_branch_push_access_levels,
+ %i[id],
+ where: 'group_id IS NOT NULL',
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :protected_branch_push_access_levels,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb b/db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb
new file mode 100644
index 00000000000..6accaa3296b
--- /dev/null
+++ b/db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class QueueDeleteInvalidProtectedBranchPushAccessLevels < Gitlab::Database::Migration[2.1]
+ MIGRATION = "DeleteInvalidProtectedBranchPushAccessLevels"
+ DELAY_INTERVAL = 2.minutes
+ MAX_BATCH_SIZE = 10_000
+ BATCH_SIZE = 5_000
+ SUB_BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :protected_branch_push_access_levels,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231016194927',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :protected_branch_push_access_levels, :id, [])
+ end
+end
diff --git a/db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb b/db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb
new file mode 100644
index 00000000000..d28b664c517
--- /dev/null
+++ b/db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddTemporaryIndexToCreateAccessLevels < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.6'
+
+ INDEX_NAME = 'tmp_idx_protected_tag_create_access_levels_on_id_with_group'
+
+ def up
+ # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843
+ add_concurrent_index(
+ :protected_tag_create_access_levels,
+ %i[id],
+ where: 'group_id IS NOT NULL',
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :protected_tag_create_access_levels,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb b/db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb
new file mode 100644
index 00000000000..5880124d0a6
--- /dev/null
+++ b/db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class QueueDeleteInvalidProtectedTagCreateAccessLevels < Gitlab::Database::Migration[2.1]
+ MIGRATION = "DeleteInvalidProtectedTagCreateAccessLevels"
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 10_000
+ SUB_BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :protected_tag_create_access_levels,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231016194943',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :protected_tag_create_access_levels, :id, [])
+ end
+end
diff --git a/db/schema_migrations/20231016173128 b/db/schema_migrations/20231016173128
new file mode 100644
index 00000000000..6aa7c3f955e
--- /dev/null
+++ b/db/schema_migrations/20231016173128
@@ -0,0 +1 @@
+b720259efa4eb9fe75a3352a64b9e14ae7b048240daf34c40a66cc5ef409dcc0
\ No newline at end of file
diff --git a/db/schema_migrations/20231016173129 b/db/schema_migrations/20231016173129
new file mode 100644
index 00000000000..acbf77968e9
--- /dev/null
+++ b/db/schema_migrations/20231016173129
@@ -0,0 +1 @@
+f886678df9907d2bf60f98c0184c91604069cd613b541a0476e30789f327df15
\ No newline at end of file
diff --git a/db/schema_migrations/20231016194926 b/db/schema_migrations/20231016194926
new file mode 100644
index 00000000000..4aa858f00f0
--- /dev/null
+++ b/db/schema_migrations/20231016194926
@@ -0,0 +1 @@
+0f2e4b7fc2658b5063dbe8dea6c881fb59a9d99ed53332ae1bdb5578343c3e89
\ No newline at end of file
diff --git a/db/schema_migrations/20231016194927 b/db/schema_migrations/20231016194927
new file mode 100644
index 00000000000..6d8d7d11191
--- /dev/null
+++ b/db/schema_migrations/20231016194927
@@ -0,0 +1 @@
+44474805c7858d07d093650e43f3313746976e4c523b408d029e32829a5b7301
\ No newline at end of file
diff --git a/db/schema_migrations/20231016194942 b/db/schema_migrations/20231016194942
new file mode 100644
index 00000000000..05862999a37
--- /dev/null
+++ b/db/schema_migrations/20231016194942
@@ -0,0 +1 @@
+43de9dd5e63a80c51aa21e42b7f41d03b6d36143afa45cf45ead6ee0cc8152cc
\ No newline at end of file
diff --git a/db/schema_migrations/20231016194943 b/db/schema_migrations/20231016194943
new file mode 100644
index 00000000000..df18251008c
--- /dev/null
+++ b/db/schema_migrations/20231016194943
@@ -0,0 +1 @@
+b17f7eaff454fab3e46e438d81fdebab14776322af261e0f2a12ceb69a5623a8
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 0da8e0b7ce1..dea21fc06ca 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -35071,6 +35071,12 @@ CREATE INDEX tmp_idx_orphaned_approval_merge_request_rules ON approval_merge_req
CREATE INDEX tmp_idx_orphaned_approval_project_rules ON approval_project_rules USING btree (id) WHERE ((report_type = ANY (ARRAY[2, 4])) AND (security_orchestration_policy_configuration_id IS NULL));
+CREATE INDEX tmp_idx_protected_branch_merge_access_levels_on_id_with_group ON protected_branch_merge_access_levels USING btree (id) WHERE (group_id IS NOT NULL);
+
+CREATE INDEX tmp_idx_protected_branch_push_access_levels_on_id_with_group ON protected_branch_push_access_levels USING btree (id) WHERE (group_id IS NOT NULL);
+
+CREATE INDEX tmp_idx_protected_tag_create_access_levels_on_id_with_group ON protected_tag_create_access_levels USING btree (id) WHERE (group_id IS NOT NULL);
+
CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL));
CREATE INDEX tmp_index_cis_vulnerability_reads_on_id ON vulnerability_reads USING btree (id) WHERE (report_type = 7);
diff --git a/doc/architecture/blueprints/gitlab_ml_experiments/index.md b/doc/architecture/blueprints/gitlab_ml_experiments/index.md
index e0675bb5be6..b9830778902 100644
--- a/doc/architecture/blueprints/gitlab_ml_experiments/index.md
+++ b/doc/architecture/blueprints/gitlab_ml_experiments/index.md
@@ -120,51 +120,46 @@ However, Service-Integration will establish certain necessary and optional requi
###### Ease of Use, Ownership Requirements
-1. `R100`: Required: the platform should be easy to use: imagine Heroku with [GitLab Production Readiness-approved](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/) defaults.
-1. `R110`: Required: with the exception of an Infrastructure-led onboarding process, services are owned, deployed and managed by stage-group teams. In other words,services follow a "You Build It, You Run It" model of ownership.
-1. `R120`: Required: programming-language agnostic: no requirements for services. Services should be packaged as container images.
-1. `R130`: Recommended: Each service should be evaluated against the GitLab.com [Service Maturity Model](https://about.gitlab.com/handbook/engineering/infrastructure/service-maturity-model/).
-1. `R140`: Recommended: services using the platform have expedited production-readiness processes.
- 1. Production-readiness requirements graded by service maturity: low-traffic, low-maturity experimental services will have lower requirement thresholds than more mature services.
- 1. By default, the platform should provide services with defaults that would pass production-readiness review for the lowest service maturity-level.
- 1. At introduction, lowest maturity services can be deployed without production readiness, provided the meet certain automatically validated requirements. This removes Infrastructure gate-keeping from being a blocker to experimental service delivery.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R100` | Required | The platform should be easy to use: imagine Heroku with [GitLab Production Readiness-approved](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/) defaults. | [Runway to [BETA] : Increased Adoption and Self Service](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1115) | **{dotted-circle}** No |
+| `R110` | Required | With the exception of an Infrastructure-led onboarding process, services are owned, deployed and managed by stage-group teams. In other words,services follow a “You Build It, You Run It” model of ownership.| [[Paused] Discussion: Tiered Support Model for Runway](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/97) | **{dotted-circle}** No |
+| `R120` | Required | Programming-language agnostic: no requirements for services. Services should be packaged as container images.| [Runway to [BETA] : Increased Adoption and Self Service](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1115) | **{dotted-circle}** No |
+| `R130` | Recommended | Each service should be evaluated against the GitLab.com [Service Maturity Model](https://about.gitlab.com/handbook/engineering/infrastructure/service-maturity-model/).| [Discussion: Introduce an 'Infrastructure Well-Architected Service Framework'](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2537) | **{dotted-circle}** No |
+| `R140` | Recommended | Services using the platform have expedited production-readiness processes. {::nomarkdown}
- Production-readiness requirements graded by service maturity: low-traffic, low-maturity experimental services will have lower requirement thresholds than more mature services.
- By default, the platform should provide services with defaults that would pass production-readiness review for the lowest service maturity-level.
- At introduction, lowest maturity services can be deployed without production readiness, provided the meet certain automatically validated requirements. This removes Infrastructure gate-keeping from being a blocker to experimental service delivery.
{:/} | | |
###### Observability Requirements
-1. `R200`: Required: the platform must provide SLIs for services out-of-the-box.
- 1. While it is recommended that services expose internal metrics, it is not mandatory. The platform will provide monitoring from the load-balancer. This is to speed up deployment by removing barriers to experimentation.
- 1. For services that provide internal metrics scrape endpoints, the platform must be configurable to collect these.
- 1. The platform must provide generic load-balancer level SLIs for all services. Service owners must be able to select from constructing SLIs from internal application metrics, the platform-provided external SLIs, or a combination of both.
-1. `R210`: Required: Observability dashboards, rules, alerts (with per-term routing) must be generated from a manifest.
-1. `R220`:Required: standardized logging infrastructure.
- 1. Mandate that all logging emitted from services must be Structured JSON. Text logs are permitted but not recommended.
- 1. See [Common Service Libraries](#common-service-libraries) for more details of building common SDKs for observability.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R200` | Required | The platform must provide SLIs for services out-of-the-box.{::nomarkdown}- While it is recommended that services expose internal metrics, it is not mandatory. The platform will provide monitoring from the load-balancer. This is to speed up deployment by removing barriers to experimentation.
- For services that provide internal metrics scrape endpoints, the platform must be configurable to collect these.
- The platform must provide generic load-balancer level SLIs for all services. Service owners must be able to select from constructing SLIs from internal application metrics, the platform-provided external SLIs, or a combination of both.
{:/} | [Observability: Default Metrics](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/72), [Observability: Custom Metrics](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/67) | **{check-circle}** Yes |
+| `R210` | Required | Observability dashboards, rules, alerts (with per-term routing) must be generated from a manifest. | [Observability: Metrics Catalog](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/74) | **{check-circle}** Yes |
+| `R220` | Required | Standardized logging infrastructure.{::nomarkdown}- Mandate that all logging emitted from services must be Structured JSON. Text logs are permitted but not recommended.
- See Common Service Libraries for more details of building common SDKs for observability.
{:/} | [Observability: Logs in Elasticsearch for model-gateway](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/75), [Observability: Runway logs available to users](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/84) | |
###### Deployment Requirements
-1. `R300`: Required: No secrets stored in CI/CD.
- 1. Authentication with Cloud Provider Resources should be exclusively via OIDC, managed as part of the platform.
- 1. Secrets should be stored in the Infrastructure-provided Hashicorp Vault for the environment and passed to applications through files or environment variables.
- 1. Generation and management of service account tokens should be done declaratively, without manual interaction.
-1. `R310`: Required: multiple environment should be supported, eg Staging and Production.
-1. `R320`: Required: the platform should be cost-effective. Kubernetes clusters should support multiple services and teams.
-1. `R330`: Recommended: gradual rollouts, rollbacks, blue-green deployments.
-1. `R340`: Required: services should be isolated from one another.
-1. `R350`: Recommended: services should have the ability to specify node characteristic requirements (eg, GPU).
-1. `R360`: Required: Developers should not need knowledge of Helm, Kubernetes, Prometheus in order to deploy. All required values are configured and validated in project-hosted manifest before generating Kubernetes manifests, Prometheus rules, etc.
-1. `R370`: Initially services should be synchronous only - using REST or GRPC requests.
- 1. This does not however preclude long-running HTTP(s) requests, for example long-polling or Websocket requests.
-1. `R390`: Each service hosted in its own GitLab repository with deployment manifest stored in the repository.
- 1. Continuous deployments that are initiated from the CI pipeline of the corresponding GitLab repository.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R300` | Required | No secrets stored in CI/CD. {::nomarkdown} - Authentication with Cloud Provider Resources should be exclusively via OIDC, managed as part of the platform.
- Secrets should be stored in the Infrastructure-provided Hashicorp Vault for the environment and passed to applications through files or environment variables.
- Generation and management of service account tokens should be done declaratively, without manual interaction.
{:/} | [Secrets Management](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/52) | **{dotted-circle}** No |
+| `R310` | Required | Multiple environment should be supported, eg Staging and Production. | | **{check-circle}** Yes |
+| `R320` | Required | The platform should be cost-effective. Kubernetes clusters should support multiple services and teams. | | |
+| `R330` | Recommended | Gradual rollouts, rollbacks, blue-green deployments. | | |
+| `R340` | Required | Services should be isolated from one another. | | |
+| `R350` | Recommended | Services should have the ability to specify node characteristic requirements (eg, GPU). | | |
+| `R360` | Required | Developers should not need knowledge of Helm, Kubernetes, Prometheus in order to deploy. All required values are configured and validated in project-hosted manifest before generating Kubernetes manifests, Prometheus rules, etc. | | |
+| `R370` | | Initially services should be synchronous only - using REST or GRPC requests.{::nomarkdown}- This does not however preclude long-running HTTP(s) requests, for example long-polling or Websocket requests.
{:/} | | |
+| `R390` | | Each service hosted in its own GitLab repository with deployment manifest stored in the repository. {::nomarkdown}- Continuous deployments that are initiated from the CI pipeline of the corresponding GitLab repository.
{:/} | | |
##### Security Requirements
-1. `R400`: stateful services deployed on the platform that utilize their own stateful storage (for example, custom deployed Postgres instance), must not store application security tokens, cloud-provider service keys or other long-lived security tokens in their stateful stores.
-1. `R410`: long-lived shared secrets are discouraged, and should be referenced in the service manifest as such, to allow for accounting and monitoring.
-1. `R420`: services using long-lived shared secrets should ensure that secret rotation can take place without downtime.
- 1. During a rotation, old and new generations of secrets should pass authentication, allowing gradual roll-out of new secrets.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R400` | | Stateful services deployed on the platform that utilize their own stateful storage (for example, custom deployed Postgres instance), must not store application security tokens, cloud-provider service keys or other long-lived security tokens in their stateful stores. | | |
+| `R410` | | Long-lived shared secrets are discouraged, and should be referenced in the service manifest as such, to allow for accounting and monitoring. | | |
+| `R420` | | Services using long-lived shared secrets should ensure that secret rotation can take place without downtime. {::nomarkdown}- During a rotation, old and new generations of secrets should pass authentication, allowing gradual roll-out of new secrets.
{:/} | | |
##### Common Service Libraries
-1. `R500`: Experimental services would be strongly encouraged to adopt and use [LabKit](https://gitlab.com/gitlab-org/labkit) (for Go services), or [LabKit-Ruby](https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby) for observability, context, correlation, FIPs verification, etc.
- 1. At present, there is no LabKit-Python library, but some experiments will run in Python, so building a library to providing observability, context, correlation services in Python will be required.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R500` | Required | Experimental services would be strongly encouraged to adopt and use [LabKit](https://gitlab.com/gitlab-org/labkit) (for Go services), or [LabKit-Ruby](https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby) for observability, context, correlation, FIPs verification, etc. {::nomarkdown}- At present, there is no LabKit-Python library, but some experiments will run in Python, so building a library to providing observability, context, correlation services in Python will be required.
{:/} | | |
diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md
index 221270b69f6..3a1be55f671 100644
--- a/doc/user/clusters/agent/install/index.md
+++ b/doc/user/clusters/agent/install/index.md
@@ -220,7 +220,7 @@ The following example projects can help you get started with the agent.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340882) in GitLab 14.8, GitLab warns you on the agent's list page to update the agent version installed on your cluster.
-For the best experience, the version of the agent installed in your cluster should match the GitLab major and minor version. The previous minor version is also supported. For example, if your GitLab version is v14.9.4 (major version 14, minor version 9), then versions v14.9.0 and v14.9.1 of the agent are ideal, but any v14.8.x version of the agent is also supported. See [the release page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/releases) of the GitLab agent.
+For the best experience, the version of the agent installed in your cluster should match the GitLab major and minor version. The previous and next minor versions are also supported. For example, if your GitLab version is v14.9.4 (major version 14, minor version 9), then versions v14.9.0 and v14.9.1 of the agent are ideal, but any v14.8.x or v14.10.x version of the agent is also supported. See [the release page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/releases) of the GitLab agent.
### Update the agent version
diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md
index e8451e3049d..6c89e09bd47 100644
--- a/doc/user/project/push_options.md
+++ b/doc/user/project/push_options.md
@@ -45,7 +45,8 @@ Git push options can perform actions for merge requests while pushing changes:
| Push option | Description |
|----------------------------------------------|-------------|
| `merge_request.create` | Create a new merge request for the pushed branch. |
-| `merge_request.target=` | Set the target of the merge request to a particular branch or upstream project, such as: `git push -o merge_request.target=project_path/branch`. |
+| `merge_request.target=` | Set the target of the merge request to a particular branch, such as: `git push -o merge_request.target=branch_name`. |
+| `merge_request.target_project=` | Set the target of the merge request to a particular upstream project, such as: `git push -o merge_request.target_project=path/to/project`. Introduced in [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132475). |
| `merge_request.merge_when_pipeline_succeeds` | Set the merge request to [merge when its pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md). |
| `merge_request.remove_source_branch` | Set the merge request to remove the source branch when it's merged. |
| `merge_request.title=""` | Set the title of the merge request. For example: `git push -o merge_request.title="The title I want"`. |
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb
new file mode 100644
index 00000000000..99bc638532a
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_branch_merge_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedBranchMergeAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_branch_merge_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_branch_merge_access_levels.group_id
+ AND pgl.project_id = protected_branches.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb
new file mode 100644
index 00000000000..a6934cf5adc
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_branch_push_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedBranchPushAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_branch_push_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_branch_push_access_levels.group_id
+ AND pgl.project_id = protected_branches.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb
new file mode 100644
index 00000000000..8c59e42a9f6
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_tag_create_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedTagCreateAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_tag_create_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_tags ON protected_tags.id = protected_tag_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_tag_create_access_levels.group_id
+ AND pgl.project_id = protected_tags.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
index 4471d21b9ac..e817f2130f4 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -14,6 +14,7 @@ module Gitlab
:milestone,
:remove_source_branch,
:target,
+ :target_project,
:title,
:unassign,
:unlabel
diff --git a/spec/components/projects/ml/models_index_component_spec.rb b/spec/components/projects/ml/models_index_component_spec.rb
index c42c94d5d01..b662e8c0a08 100644
--- a/spec/components/projects/ml/models_index_component_spec.rb
+++ b/spec/components/projects/ml/models_index_component_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
end
subject(:component) do
- described_class.new(paginator: paginator)
+ described_class.new(model_count: 5, paginator: paginator)
end
describe 'rendered' do
@@ -43,13 +43,15 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
{
'name' => model1.name,
'version' => model1.latest_version.version,
- 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionPackagePath' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionPath' => "/#{project.full_path}/-/ml/models/#{model1.id}/versions/#{model1.latest_version.id}",
'versionCount' => 1
},
{
'name' => model2.name,
'version' => nil,
- 'path' => nil,
+ 'versionPackagePath' => nil,
+ 'versionPath' => nil,
'versionCount' => 0
}
],
@@ -58,7 +60,8 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
'hasPreviousPage' => false,
'startCursor' => 'abcde',
'endCursor' => 'defgh'
- }
+ },
+ 'modelCount' => 5
})
end
end
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index fd367b8e763..ea6f3ae1966 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :groups
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
- expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index 9747d499ae9..94c42c0f098 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :groups_an
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
- expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
end
diff --git a/spec/finders/projects/ml/model_finder_spec.rb b/spec/finders/projects/ml/model_finder_spec.rb
index 4104507445e..a2c2836a63d 100644
--- a/spec/finders/projects/ml/model_finder_spec.rb
+++ b/spec/finders/projects/ml/model_finder_spec.rb
@@ -19,9 +19,11 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do
is_expected.to eq([model3, model2, model1])
end
- it 'including the latest version', :aggregate_failures do
+ it 'including the latest version and project', :aggregate_failures do
expect(models[0].association_cached?(:latest_version)).to be(true)
+ expect(models[0].association_cached?(:project)).to be(true)
expect(models[1].association_cached?(:latest_version)).to be(true)
+ expect(models[1].association_cached?(:project)).to be(true)
end
it 'does not return models belonging to a different project' do
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb
new file mode 100644
index 00000000000..1e5b9d30436
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchMergeAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_branches_table) { table(:protected_branches) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_branch_merge_access_levels_table) { table(:protected_branch_merge_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_branch_merge_access_levels_table.minimum(:id),
+ end_id: protected_branch_merge_access_levels_table.maximum(:id),
+ batch_table: :protected_branch_merge_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are merge access levels' do
+ let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:merge_access_level_for_user) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:merge_access_level_with_linked_group) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:merge_access_level_with_unlinked_group) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes merge access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_branch_merge_access_levels_table.count }.from(3).to(2)
+ expect(protected_branch_merge_access_levels_table.all).to contain_exactly(
+ merge_access_level_with_linked_group,
+ merge_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb
new file mode 100644
index 00000000000..62201831dd1
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchPushAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_branches_table) { table(:protected_branches) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_branch_push_access_levels_table) { table(:protected_branch_push_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_branch_push_access_levels_table.minimum(:id),
+ end_id: protected_branch_push_access_levels_table.maximum(:id),
+ batch_table: :protected_branch_push_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are push access levels' do
+ let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:push_access_level_for_user) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:push_access_level_with_linked_group) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:push_access_level_with_unlinked_group) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes push access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_branch_push_access_levels_table.count }.from(3).to(2)
+ expect(protected_branch_push_access_levels_table.all).to contain_exactly(
+ push_access_level_with_linked_group,
+ push_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb
new file mode 100644
index 00000000000..fd6cee9e4db
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedTagCreateAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_tags_table) { table(:protected_tags) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_tag_create_access_levels_table) { table(:protected_tag_create_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_tag_create_access_levels_table.minimum(:id),
+ end_id: protected_tag_create_access_levels_table.maximum(:id),
+ batch_table: :protected_tag_create_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are push access levels' do
+ let(:protected_tag) { protected_tags_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:push_access_level_for_user) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:push_access_level_with_linked_group) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:push_access_level_with_unlinked_group) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes push access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_tag_create_access_levels_table.count }.from(3).to(2)
+ expect(protected_tag_create_access_levels_table.all).to contain_exactly(
+ push_access_level_with_linked_group,
+ push_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb b/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb
new file mode 100644
index 00000000000..292fdca026f
--- /dev/null
+++ b/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedBranchMergeAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_branch_merge_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb b/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb
new file mode 100644
index 00000000000..db42bb05f15
--- /dev/null
+++ b/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedBranchPushAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_branch_push_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb b/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb
new file mode 100644
index 00000000000..4acc46a65c5
--- /dev/null
+++ b/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedTagCreateAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_tag_create_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb
index ed04c12040b..ae7c3f163f3 100644
--- a/spec/models/ml/model_spec.rb
+++ b/spec/models/ml/model_spec.rb
@@ -78,6 +78,14 @@ RSpec.describe Ml::Model, feature_category: :mlops do
end
end
+ describe '.including_project' do
+ subject { described_class.including_project }
+
+ it 'loads latest version' do
+ expect(subject.first.association_cached?(:project)).to be(true)
+ end
+ end
+
describe 'with_version_count' do
let(:model) { existing_model }
diff --git a/spec/presenters/ml/model_presenter_spec.rb b/spec/presenters/ml/model_presenter_spec.rb
index 991606c3b08..31bf4e7ad6c 100644
--- a/spec/presenters/ml/model_presenter_spec.rb
+++ b/spec/presenters/ml/model_presenter_spec.rb
@@ -58,6 +58,22 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
end
end
+ describe '#latest_version_path' do
+ subject { model.present.latest_version_path }
+
+ context 'when model version does not have package' do
+ let(:model) { model1 }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when latest model version has package' do
+ let(:model) { model2 }
+
+ it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model.latest_version.id}") }
+ end
+ end
+
describe '#path' do
subject { model1.present.path }
diff --git a/spec/presenters/ml/model_version_presenter_spec.rb b/spec/presenters/ml/model_version_presenter_spec.rb
index 4e88791f910..7624aaffc7a 100644
--- a/spec/presenters/ml/model_version_presenter_spec.rb
+++ b/spec/presenters/ml/model_version_presenter_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:model) { build_stubbed(:ml_models, name: 'a_model', project: project) }
- let_it_be(:model_version) { build_stubbed(:ml_model_versions, model: model, version: '1.1.1') }
+ let_it_be(:model_version) { build_stubbed(:ml_model_versions, :with_package, model: model, version: '1.1.1') }
let_it_be(:presenter) { model_version.present }
describe '.display_name' do
@@ -15,8 +15,14 @@ RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do
end
describe '#path' do
- subject { model_version.present.path }
+ subject { presenter.path }
it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model_version.id}") }
end
+
+ describe '#package_path' do
+ subject { presenter.package_path }
+
+ it { is_expected.to eq("/#{project.full_path}/-/packages/#{model_version.package_id}") }
+ end
end
diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb
index 028fde06486..cda3f777a72 100644
--- a/spec/requests/projects/ml/models_controller_spec.rb
+++ b/spec/requests/projects/ml/models_controller_spec.rb
@@ -43,17 +43,26 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
index_request
end
- it 'fetches the correct models' do
+ it 'fetches the correct variables', :aggregate_failures do
+ stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2)
+
index_request
- expect(assigns(:paginator).records).to match_array([model3, model2, model1])
+ page_models = [model3, model2]
+ all_models = [model3, model2, model1]
+
+ expect(assigns(:paginator).records).to match_array(page_models)
+ expect(assigns(:model_count)).to be all_models.count
end
it 'does not perform N+1 sql queries' do
+ list_models
+
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models }
create_list(:ml_model_versions, 2, model: model1)
create_list(:ml_model_versions, 2, model: model2)
+ create_list(:ml_models, 4, project: project)
expect { list_models }.not_to exceed_all_query_limit(control_count)
end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index 49ec8b09939..038977e4fd0 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -54,6 +54,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ shared_examples_for 'a service that can set the target project of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request with the correct target project' do
+ project_path = push_options[:target_project] || project.default_merge_request_target.full_path
+
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ expect(last_mr.target_project.full_path).to eq(project_path)
+ end
+ end
+
shared_examples_for 'a service that can set the title of a merge request' do
subject(:last_mr) { MergeRequest.last }
@@ -347,6 +358,31 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
it_behaves_like 'with the project default branch'
end
+ describe '`target_project` push option' do
+ let(:changes) { new_branch_changes }
+ let(:double_forked_project) { fork_project(forked_project, user1, repository: true) }
+ let(:service) { described_class.new(project: double_forked_project, current_user: user1, changes: changes, push_options: push_options) }
+ let(:push_options) { { create: true, target_project: target_project.full_path } }
+
+ context 'to self' do
+ let(:target_project) { double_forked_project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+
+ context 'to intermediate project' do
+ let(:target_project) { forked_project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+
+ context 'to base project' do
+ let(:target_project) { project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+ end
+
describe '`title` push option' do
let(:push_options) { { title: title } }
@@ -861,6 +897,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ describe 'when the target project does not exist' do
+ let(:push_options) { { create: true, target: 'my-branch', target_project: 'does-not-exist' } }
+ let(:changes) { default_branch_changes }
+
+ it 'records an error', :sidekiq_inline do
+ service.execute
+
+ expect(service.errors).to eq(["User access was denied"])
+ end
+ end
+
describe 'when user does not have access to target project' do
let(:push_options) { { create: true, target: 'my-branch' } }
let(:changes) { default_branch_changes }
@@ -890,6 +937,18 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ describe 'when projects are unrelated' do
+ let(:unrelated_project) { create(:project, :public, :repository, group: child_group) }
+ let(:push_options) { { create: true, target_project: unrelated_project.full_path } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(["Projects #{project.full_path} and #{unrelated_project.full_path} are not in the same network"])
+ end
+ end
+
describe 'when MR has ActiveRecord errors' do
let(:push_options) { { create: true } }
let(:changes) { new_branch_changes }
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 8e8e7e8ad05..6d283113e85 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -158,7 +158,7 @@ def click_sort_option(option, ascending)
wait_for_requests
# Reset the sort direction
- if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending
+ if page.has_selector?('button[aria-label="Sort direction: Ascending"]', wait: 0) && !ascending
click_button 'Sort direction'
wait_for_requests