Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/abuse_report.rb5
-rw-r--r--app/models/ai/service_access_token.rb3
-rw-r--r--app/models/analytics/cycle_analytics/aggregation.rb19
-rw-r--r--app/models/analytics/cycle_analytics/stage.rb17
-rw-r--r--app/models/application_setting.rb256
-rw-r--r--app/models/application_setting_implementation.rb5
-rw-r--r--app/models/bulk_imports/entity.rb6
-rw-r--r--app/models/bulk_imports/failure.rb8
-rw-r--r--app/models/ci/build.rb25
-rw-r--r--app/models/ci/catalog/resources/version.rb9
-rw-r--r--app/models/ci/instance_variable.rb1
-rw-r--r--app/models/ci/namespace_mirror.rb1
-rw-r--r--app/models/ci/pipeline.rb11
-rw-r--r--app/models/ci/pipeline_artifact.rb3
-rw-r--r--app/models/ci/pipeline_chat_data.rb7
-rw-r--r--app/models/ci/pipeline_config.rb4
-rw-r--r--app/models/ci/pipeline_metadata.rb7
-rw-r--r--app/models/ci/pipeline_variable.rb3
-rw-r--r--app/models/ci/processable.rb4
-rw-r--r--app/models/ci/project_mirror.rb2
-rw-r--r--app/models/ci/runner.rb52
-rw-r--r--app/models/ci/runner_manager.rb37
-rw-r--r--app/models/ci/stage.rb3
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/compare.rb4
-rw-r--r--app/models/concerns/analytics/cycle_analytics/parentable.rb11
-rw-r--r--app/models/concerns/atomic_internal_id.rb4
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/ci/has_runner_status.rb50
-rw-r--r--app/models/concerns/ci/partitionable/testing.rb4
-rw-r--r--app/models/concerns/commit_signature.rb12
-rw-r--r--app/models/concerns/database_event_tracking.rb52
-rw-r--r--app/models/concerns/enums/commit_signature.rb24
-rw-r--r--app/models/concerns/integrations/enable_ssl_verification.rb3
-rw-r--r--app/models/concerns/integrations/has_issue_tracker_fields.rb13
-rw-r--r--app/models/concerns/integrations/slack_mattermost_fields.rb18
-rw-r--r--app/models/concerns/partitioned_table.rb3
-rw-r--r--app/models/concerns/restricted_signup.rb8
-rw-r--r--app/models/concerns/routable.rb36
-rw-r--r--app/models/container_registry/protection/rule.rb17
-rw-r--r--app/models/container_repository.rb16
-rw-r--r--app/models/deployment.rb3
-rw-r--r--app/models/group.rb34
-rw-r--r--app/models/integration.rb8
-rw-r--r--app/models/integrations/apple_app_store.rb2
-rw-r--r--app/models/integrations/bamboo.rb12
-rw-r--r--app/models/integrations/campfire.rb11
-rw-r--r--app/models/integrations/clickup.rb4
-rw-r--r--app/models/integrations/confluence.rb3
-rw-r--r--app/models/integrations/diffblue_cover.rb126
-rw-r--r--app/models/integrations/discord.rb5
-rw-r--r--app/models/integrations/external_wiki.rb1
-rw-r--r--app/models/integrations/google_play.rb18
-rw-r--r--app/models/integrations/harbor.rb4
-rw-r--r--app/models/integrations/mattermost.rb2
-rw-r--r--app/models/integrations/mattermost_slash_commands.rb2
-rw-r--r--app/models/integrations/slack.rb2
-rw-r--r--app/models/integrations/squash_tm.rb2
-rw-r--r--app/models/integrations/youtrack.rb4
-rw-r--r--app/models/issue_email_participant.rb3
-rw-r--r--app/models/jira_connect_subscription.rb2
-rw-r--r--app/models/label.rb5
-rw-r--r--app/models/member.rb67
-rw-r--r--app/models/members/group_member.rb53
-rw-r--r--app/models/members/project_member.rb44
-rw-r--r--app/models/merge_request.rb23
-rw-r--r--app/models/merge_request/metrics.rb30
-rw-r--r--app/models/merge_request_diff.rb26
-rw-r--r--app/models/ml/experiment.rb3
-rw-r--r--app/models/ml/model_metadata.rb2
-rw-r--r--app/models/ml/model_version.rb13
-rw-r--r--app/models/ml/model_version_metadata.rb14
-rw-r--r--app/models/namespace.rb27
-rw-r--r--app/models/namespace/package_setting.rb10
-rw-r--r--app/models/namespace_setting.rb4
-rw-r--r--app/models/namespaces/descendants.rb30
-rw-r--r--app/models/namespaces/traversal/cached.rb34
-rw-r--r--app/models/onboarding/completion.rb16
-rw-r--r--app/models/onboarding/progress.rb3
-rw-r--r--app/models/organizations/organization.rb10
-rw-r--r--app/models/organizations/organization_detail.rb2
-rw-r--r--app/models/organizations/organization_user.rb12
-rw-r--r--app/models/pages/project_settings.rb25
-rw-r--r--app/models/pages_deployment.rb8
-rw-r--r--app/models/project.rb29
-rw-r--r--app/models/project_authorizations/changes.rb48
-rw-r--r--app/models/project_statistics.rb5
-rw-r--r--app/models/project_team.rb7
-rw-r--r--app/models/projects/project_topic.rb2
-rw-r--r--app/models/projects/repository_storage_move.rb6
-rw-r--r--app/models/projects/topic.rb9
-rw-r--r--app/models/release.rb6
-rw-r--r--app/models/resource_label_event.rb6
-rw-r--r--app/models/resource_milestone_event.rb2
-rw-r--r--app/models/route.rb44
-rw-r--r--app/models/service_desk/custom_email_credential.rb5
-rw-r--r--app/models/snippets/repository_storage_move.rb6
-rw-r--r--app/models/ssh_host_key.rb5
-rw-r--r--app/models/time_tracking/timelog_category.rb2
-rw-r--r--app/models/timelog.rb1
-rw-r--r--app/models/tree.rb11
-rw-r--r--app/models/user.rb56
-rw-r--r--app/models/users/callout.rb4
-rw-r--r--app/models/users/credit_card_validation.rb2
-rw-r--r--app/models/users/in_product_marketing_email.rb75
-rw-r--r--app/models/users/phone_number_validation.rb35
-rw-r--r--app/models/work_item.rb2
-rw-r--r--app/models/work_items/hierarchy_restriction.rb9
-rw-r--r--app/models/work_items/widget_definition.rb4
-rw-r--r--app/models/work_items/widgets/notes.rb10
111 files changed, 1149 insertions, 720 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 19dc0e40564..e19a75a68e8 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -205,11 +205,12 @@ class AbuseReport < ApplicationRecord
return if links_to_spam.blank?
links_to_spam.each do |link|
- Gitlab::UrlBlocker.validate!(
+ Gitlab::HTTP_V2::UrlBlocker.validate!(
link,
schemes: %w[http https],
allow_localhost: true,
- dns_rebind_protection: true
+ dns_rebind_protection: true,
+ deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?
)
next unless link.length > MAX_CHAR_LIMIT_URL
diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb
index 46dfbe9078c..d2d64079c74 100644
--- a/app/models/ai/service_access_token.rb
+++ b/app/models/ai/service_access_token.rb
@@ -2,11 +2,8 @@
module Ai
class ServiceAccessToken < ApplicationRecord
- include IgnorableColumns
self.table_name = 'service_access_tokens'
- ignore_column :category, remove_with: '16.8', remove_after: '2024-01-22'
-
scope :expired, -> { where('expires_at < :now', now: Time.current) }
scope :active, -> { where('expires_at > :now', now: Time.current) }
diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb
index 0f8e184933e..5ac5437a442 100644
--- a/app/models/analytics/cycle_analytics/aggregation.rb
+++ b/app/models/analytics/cycle_analytics/aggregation.rb
@@ -59,19 +59,26 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
estimation < 1 ? nil : estimation.from_now
end
- def self.safe_create_for_namespace(group_or_project_namespace)
+ def self.safe_create_for_namespace(target_namespace)
# Namespaces::ProjectNamespace has no root_ancestor
# Related: https://gitlab.com/gitlab-org/gitlab/-/issues/386124
- group = group_or_project_namespace.is_a?(Group) ? group_or_project_namespace : group_or_project_namespace.parent
- top_level_group = group.root_ancestor
- aggregation = find_by(group_id: top_level_group.id)
+ namespace = if target_namespace.is_a?(Group) || target_namespace.is_a?(Namespaces::UserNamespace)
+ target_namespace
+ else
+ target_namespace.parent
+ end
+ # personal namespace projects and associated ProjectNamespace respond to `namespace`
+ # and this is close enough to "root ancestor"
+ top_level_namespace =
+ target_namespace.respond_to?(:root_ancestor) ? namespace.root_ancestor : namespace.namespace
+ aggregation = find_by(group_id: top_level_namespace.id)
return aggregation if aggregation&.enabled?
# At this point we're sure that the group is licensed, we can always enable the aggregation.
# This re-enables the aggregation in case the group downgraded and later upgraded the license.
- upsert({ group_id: top_level_group.id, enabled: true })
+ upsert({ group_id: top_level_namespace.id, enabled: true })
- find(top_level_group.id)
+ find(top_level_namespace.id)
end
private
diff --git a/app/models/analytics/cycle_analytics/stage.rb b/app/models/analytics/cycle_analytics/stage.rb
index 6f152e7749e..4686dc3aedd 100644
--- a/app/models/analytics/cycle_analytics/stage.rb
+++ b/app/models/analytics/cycle_analytics/stage.rb
@@ -7,7 +7,6 @@ module Analytics
self.table_name = :analytics_cycle_analytics_group_stages
- include DatabaseEventTracking
include Analytics::CycleAnalytics::Stageable
include Analytics::CycleAnalytics::Parentable
@@ -38,22 +37,6 @@ module Analytics
.select("DISTINCT ON(stage_event_hash_id) #{quoted_table_name}.*")
end
- SNOWPLOW_ATTRIBUTES = %i[
- id
- created_at
- updated_at
- relative_position
- start_event_identifier
- end_event_identifier
- group_id
- start_event_label_id
- end_event_label_id
- hidden
- custom
- name
- group_value_stream_id
- ].freeze
-
private
def max_stages_count
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index cb533a5e99d..35d4722b711 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -99,7 +99,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :default_branch_protection_defaults, json_schema: { filename: 'default_branch_protection_defaults' }
validates :default_branch_protection_defaults, bytesize: { maximum: -> { DEFAULT_BRANCH_PROTECTIONS_DEFAULT_MAX_SIZE } }
- validates :failed_login_attempts_unlock_period_in_minutes,
+ validates :external_pipeline_validation_service_timeout,
+ :failed_login_attempts_unlock_period_in_minutes,
+ :max_login_attempts,
allow_nil: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -118,10 +120,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
allow_nil: false,
qualified_domain_array: true
- validates :session_expire_delay,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :minimum_password_length,
presence: true,
numericality: {
@@ -222,38 +220,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
hostname: true,
length: { maximum: 255 }
- validates :max_attachment_size,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :max_artifacts_size,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :max_export_size,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :max_import_size,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :max_import_remote_file_size,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :bulk_import_max_download_file_size,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :max_decompressed_archive_size,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :max_login_attempts,
- allow_nil: true,
- numericality: { only_integer: true, greater_than: 0 }
-
validates :max_pages_size,
presence: true,
numericality: {
@@ -261,31 +227,11 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
less_than: ::Gitlab::Pages::MAX_SIZE / 1.megabyte
}
- validates :max_pages_custom_domains_per_project,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :jobs_per_stage_page_size,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :max_terraform_state_size_bytes,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :container_registry_token_expire_delay,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :decompress_archive_file_timeout,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validate :check_repository_storages_weighted
validates :auto_devops_domain,
@@ -300,14 +246,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' },
if: :domain_denylist_enabled?
- validates :housekeeping_optimize_repository_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :terminal_max_session_time,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :polling_interval_multiplier,
presence: true,
numericality: { greater_than_or_equal_to: 0 }
@@ -413,59 +351,26 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
- validates :push_event_hooks_limit,
- numericality: { greater_than_or_equal_to: 0 }
-
validates :push_event_activities_limit,
+ :push_event_hooks_limit,
numericality: { greater_than_or_equal_to: 0 }
- validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes }
validates :wiki_asciidoc_allow_uri_includes, inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :max_yaml_size_bytes, numericality: { only_integer: true, greater_than: 0 }, presence: true
- validates :max_yaml_depth, numericality: { only_integer: true, greater_than: 0 }, presence: true
-
- validates :ci_max_total_yaml_size_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, presence: true
-
- validates :ci_max_includes, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, presence: true
validates :email_restrictions, untrusted_regexp: true
validates :hashed_storage_enabled, inclusion: { in: [true], message: N_("Hashed storage can't be disabled anymore for new projects") }
- validates :container_registry_delete_tags_service_timeout,
- :container_registry_cleanup_tags_service_max_list_size,
- :container_registry_data_repair_detail_worker_max_concurrency,
- :container_registry_expiration_policies_worker_capacity,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :container_registry_expiration_policies_caching,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :container_registry_import_max_tags_count,
- :container_registry_import_max_retries,
- :container_registry_import_start_max_retries,
- :container_registry_import_max_step_duration,
- :container_registry_pre_import_timeout,
- :container_registry_import_timeout,
- allow_nil: false,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :container_registry_pre_import_tags_rate,
allow_nil: false,
numericality: { greater_than_or_equal_to: 0 }
validates :container_registry_import_target_plan, presence: true
validates :container_registry_import_created_before, presence: true
- validates :dependency_proxy_ttl_group_policy_worker_capacity,
- allow_nil: false,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :packages_cleanup_package_file_worker_capacity,
- :package_registry_cleanup_policies_worker_capacity,
- allow_nil: false,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
@@ -584,15 +489,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
length: { maximum: 255 },
allow_blank: true
- validates :issues_create_limit,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :raw_blob_request_limit,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :pipeline_limit_per_project_user_sha,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
@@ -619,41 +515,90 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :slack_app_verification_token
end
- with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do
- validates :throttle_unauthenticated_api_requests_per_period
- validates :throttle_unauthenticated_api_period_in_seconds
- validates :throttle_unauthenticated_requests_per_period
- validates :throttle_unauthenticated_period_in_seconds
- validates :throttle_unauthenticated_packages_api_requests_per_period
- validates :throttle_unauthenticated_packages_api_period_in_seconds
- validates :throttle_unauthenticated_files_api_requests_per_period
- validates :throttle_unauthenticated_files_api_period_in_seconds
- validates :throttle_unauthenticated_deprecated_api_requests_per_period
- validates :throttle_unauthenticated_deprecated_api_period_in_seconds
- validates :throttle_authenticated_api_requests_per_period
- validates :throttle_authenticated_api_period_in_seconds
- validates :throttle_authenticated_git_lfs_requests_per_period
- validates :throttle_authenticated_git_lfs_period_in_seconds
- validates :throttle_authenticated_web_requests_per_period
- validates :throttle_authenticated_web_period_in_seconds
- validates :throttle_authenticated_packages_api_requests_per_period
- validates :throttle_authenticated_packages_api_period_in_seconds
- validates :throttle_authenticated_files_api_requests_per_period
- validates :throttle_authenticated_files_api_period_in_seconds
- validates :throttle_authenticated_deprecated_api_requests_per_period
- validates :throttle_authenticated_deprecated_api_period_in_seconds
- validates :throttle_protected_paths_requests_per_period
- validates :throttle_protected_paths_period_in_seconds
- validates :project_jobs_api_rate_limit
+ with_options(numericality: { only_integer: true, greater_than: 0 }) do
+ validates :bulk_import_concurrent_pipeline_batch_limit,
+ :container_registry_token_expire_delay,
+ :housekeeping_optimize_repository_period,
+ :inactive_projects_delete_after_months,
+ :max_artifacts_size,
+ :max_attachment_size,
+ :max_yaml_depth,
+ :max_yaml_size_bytes,
+ :namespace_aggregation_schedule_lease_duration_in_seconds,
+ :project_jobs_api_rate_limit,
+ :snippet_size_limit,
+ :throttle_authenticated_api_period_in_seconds,
+ :throttle_authenticated_api_requests_per_period,
+ :throttle_authenticated_deprecated_api_period_in_seconds,
+ :throttle_authenticated_deprecated_api_requests_per_period,
+ :throttle_authenticated_files_api_period_in_seconds,
+ :throttle_authenticated_files_api_requests_per_period,
+ :throttle_authenticated_git_lfs_period_in_seconds,
+ :throttle_authenticated_git_lfs_requests_per_period,
+ :throttle_authenticated_packages_api_period_in_seconds,
+ :throttle_authenticated_packages_api_requests_per_period,
+ :throttle_authenticated_web_period_in_seconds,
+ :throttle_authenticated_web_requests_per_period,
+ :throttle_protected_paths_period_in_seconds,
+ :throttle_protected_paths_requests_per_period,
+ :throttle_unauthenticated_api_period_in_seconds,
+ :throttle_unauthenticated_api_requests_per_period,
+ :throttle_unauthenticated_deprecated_api_period_in_seconds,
+ :throttle_unauthenticated_deprecated_api_requests_per_period,
+ :throttle_unauthenticated_files_api_period_in_seconds,
+ :throttle_unauthenticated_files_api_requests_per_period,
+ :throttle_unauthenticated_packages_api_period_in_seconds,
+ :throttle_unauthenticated_packages_api_requests_per_period,
+ :throttle_unauthenticated_period_in_seconds,
+ :throttle_unauthenticated_requests_per_period
end
with_options(numericality: { only_integer: true, greater_than_or_equal_to: 0 }) do
- validates :notes_create_limit
- validates :search_rate_limit
- validates :search_rate_limit_unauthenticated
- validates :projects_api_rate_limit_unauthenticated
- validates :gitlab_shell_operation_limit
- end
+ validates :bulk_import_max_download_file_size,
+ :ci_max_includes,
+ :ci_max_total_yaml_size_bytes,
+ :container_registry_cleanup_tags_service_max_list_size,
+ :container_registry_data_repair_detail_worker_max_concurrency,
+ :container_registry_delete_tags_service_timeout,
+ :container_registry_expiration_policies_worker_capacity,
+ :container_registry_import_max_retries,
+ :container_registry_import_max_step_duration,
+ :container_registry_import_max_tags_count,
+ :container_registry_import_start_max_retries,
+ :container_registry_import_timeout,
+ :container_registry_pre_import_timeout,
+ :decompress_archive_file_timeout,
+ :dependency_proxy_ttl_group_policy_worker_capacity,
+ :gitlab_shell_operation_limit,
+ :inactive_projects_min_size_mb,
+ :issues_create_limit,
+ :jobs_per_stage_page_size,
+ :max_decompressed_archive_size,
+ :max_export_size,
+ :max_import_remote_file_size,
+ :max_import_size,
+ :max_pages_custom_domains_per_project,
+ :max_terraform_state_size_bytes,
+ :members_delete_limit,
+ :notes_create_limit,
+ :package_registry_cleanup_policies_worker_capacity,
+ :packages_cleanup_package_file_worker_capacity,
+ :pipeline_limit_per_project_user_sha,
+ :projects_api_rate_limit_unauthenticated,
+ :raw_blob_request_limit,
+ :search_rate_limit,
+ :search_rate_limit_unauthenticated,
+ :session_expire_delay,
+ :sidekiq_job_limiter_compression_threshold_bytes,
+ :sidekiq_job_limiter_limit_bytes,
+ :terminal_max_session_time,
+ :users_get_by_id_limit
+ end
+
+ jsonb_accessor :rate_limits,
+ members_delete_limit: [:integer, { default: 60 }]
+
+ validates :rate_limits, json_schema: { filename: "application_setting_rate_limits" }
validates :search_rate_limit_allowlist,
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
@@ -669,10 +614,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :external_pipeline_validation_service_url,
addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true
- validates :external_pipeline_validation_service_timeout,
- allow_nil: true,
- numericality: { only_integer: true, greater_than: 0 }
-
validates :whats_new_variant,
inclusion: { in: ApplicationSetting.whats_new_variants.keys }
@@ -686,10 +627,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :sidekiq_job_limiter_mode,
inclusion: { in: self.sidekiq_job_limiter_modes }
- validates :sidekiq_job_limiter_compression_threshold_bytes,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
- validates :sidekiq_job_limiter_limit_bytes,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :sentry_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
@@ -711,8 +648,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
length: { maximum: 255 },
if: :error_tracking_enabled?
- validates :users_get_by_id_limit,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :users_get_by_id_limit_allowlist,
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
@@ -724,20 +659,11 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
if: :update_runner_versions_enabled?
- validates :inactive_projects_min_size_mb,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :inactive_projects_delete_after_months,
- numericality: { only_integer: true, greater_than: 0 }
-
validates :inactive_projects_send_warning_email_after_months,
numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months }
validates :prometheus_alert_db_indicators_settings, json_schema: { filename: 'application_setting_prometheus_alert_db_indicators_settings' }, allow_nil: true
- validates :namespace_aggregation_schedule_lease_duration_in_seconds,
- numericality: { only_integer: true, greater_than: 0 }
-
validates :sentry_clientside_traces_sample_rate,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1, message: N_('must be a value between 0 and 1') }
@@ -815,10 +741,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :bulk_import_concurrent_pipeline_batch_limit,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
validates :allow_runner_registration_token,
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
@@ -835,6 +757,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :math_rendering_limits_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :require_admin_two_factor_authentication,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
@@ -982,7 +907,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
end
def parsed_kroki_url
- @parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w[http https], enforce_sanitization: true)[0]
+ @parsed_kroki_url ||= Gitlab::HTTP_V2::UrlBlocker.validate!(
+ kroki_url, schemes: %w[http https],
+ enforce_sanitization: true,
+ deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?)[0]
rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
self.errors.add(
:kroki_url,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 851b65055d0..d1899b18a4f 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -79,6 +79,7 @@ module ApplicationSettingImplementation
ecdsa_sk_key_restriction: default_min_key_size(:ecdsa_sk),
ed25519_key_restriction: default_min_key_size(:ed25519),
ed25519_sk_key_restriction: default_min_key_size(:ed25519_sk),
+ require_admin_two_factor_authentication: false,
eks_access_key_id: nil,
eks_account_id: nil,
eks_integration_enabled: false,
@@ -136,6 +137,7 @@ module ApplicationSettingImplementation
mirror_available: true,
notes_create_limit: 300,
notes_create_limit_allowlist: [],
+ members_delete_limit: 60,
notify_on_unknown_sign_in: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,
@@ -275,7 +277,8 @@ module ApplicationSettingImplementation
allow_account_deletion: true,
gitlab_shell_operation_limit: 600,
project_jobs_api_rate_limit: 600,
- security_txt_content: nil
+ security_txt_content: nil,
+ allow_project_creation_for_guest_and_below: true
}.tap do |hsh|
hsh.merge!(non_production_defaults) unless Rails.env.production?
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 894e28dd88a..a6969ce6f76 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -150,9 +150,9 @@ class BulkImports::Entity < ApplicationRecord
File.join(base_resource_path, 'export_relations')
end
- def export_relations_url_path(batched: false)
- if batched && bulk_import.supports_batched_export?
- Gitlab::Utils.add_url_parameters(export_relations_url_path_base, batched: batched)
+ def export_relations_url_path
+ if bulk_import.supports_batched_export?
+ Gitlab::Utils.add_url_parameters(export_relations_url_path_base, batched: true)
else
export_relations_url_path_base
end
diff --git a/app/models/bulk_imports/failure.rb b/app/models/bulk_imports/failure.rb
index 8a6077b523c..e23e49c6396 100644
--- a/app/models/bulk_imports/failure.rb
+++ b/app/models/bulk_imports/failure.rb
@@ -19,6 +19,14 @@ class BulkImports::Failure < ApplicationRecord
super(::Projects::ImportErrorFilter.filter_message(message).truncate(255))
end
+ def source_title=(title)
+ super(title&.truncate(255, omission: ''))
+ end
+
+ def source_url=(url)
+ super(url&.truncate(255, omission: ''))
+ end
+
private
def pipeline_relation
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e56f3d2536c..d4c70a294ff 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -27,6 +27,7 @@ module Ci
foreign_key: :commit_id,
partition_foreign_key: :partition_id,
inverse_of: :builds
+ belongs_to :project_mirror, primary_key: :project_id, foreign_key: :project_id, inverse_of: :builds
RUNNER_FEATURES = {
upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? },
@@ -42,6 +43,8 @@ module Ci
DEPLOYMENT_NAMES = %w[deploy release rollout].freeze
+ TOKEN_PREFIX = 'glcbt-'
+
has_one :pending_state, class_name: 'Ci::BuildPendingState', foreign_key: :build_id, inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id, inverse_of: :build
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id, inverse_of: :build
@@ -98,6 +101,7 @@ module Ci
delegate :harbor_integration, to: :project
delegate :apple_app_store_integration, to: :project
delegate :google_play_integration, to: :project
+ delegate :diffblue_cover_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
delegate :enable_debug_trace!, to: :metadata
@@ -188,6 +192,10 @@ module Ci
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123131
scope :with_runner_type, -> (runner_type) { joins(:runner).where(runner: { runner_type: runner_type }) }
+ scope :belonging_to_runner_manager, -> (runner_machine_id) {
+ joins(:runner_manager_build).where(p_ci_runner_machine_builds: { runner_machine_id: runner_machine_id })
+ }
+
scope :with_secure_reports_from_config_options, -> (job_types) do
joins(:metadata).where("#{Ci::BuildMetadata.quoted_table_name}.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
end
@@ -204,7 +212,7 @@ module Ci
add_authentication_token_field :token,
encrypted: :required,
- format_with_prefix: :partition_id_prefix_in_16_bit_encode
+ format_with_prefix: :prefix_and_partition_for_token
after_save :stick_build_if_status_changed
@@ -516,6 +524,7 @@ module Ci
.concat(harbor_variables)
.concat(apple_app_store_variables)
.concat(google_play_variables)
+ .concat(diffblue_cover_variables)
end
end
@@ -568,6 +577,12 @@ module Ci
Gitlab::Ci::Variables::Collection.new(google_play_integration.ci_variables(protected_ref: pipeline.protected_ref?))
end
+ def diffblue_cover_variables
+ return [] unless diffblue_cover_integration.try(:activated?)
+
+ Gitlab::Ci::Variables::Collection.new(diffblue_cover_integration.ci_variables)
+ end
+
def features
{
trace_sections: true,
@@ -1232,6 +1247,14 @@ module Ci
def partition_id_prefix_in_16_bit_encode
"#{partition_id.to_s(16)}_"
end
+
+ def prefix_and_partition_for_token
+ if Feature.enabled?(:prefix_ci_build_tokens, project, type: :beta)
+ TOKEN_PREFIX + partition_id_prefix_in_16_bit_encode
+ else
+ partition_id_prefix_in_16_bit_encode
+ end
+ end
end
end
diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb
index 4273c4515bc..0ea2735b030 100644
--- a/app/models/ci/catalog/resources/version.rb
+++ b/app/models/ci/catalog/resources/version.rb
@@ -19,6 +19,7 @@ module Ci
scope :for_catalog_resources, ->(catalog_resources) { where(catalog_resource_id: catalog_resources) }
scope :preloaded, -> { includes(:catalog_resource, project: [:route, { namespace: :route }], release: :author) }
+ scope :by_name, ->(name) { joins(:release).merge(Release.where(tag: name)) }
scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
@@ -122,6 +123,14 @@ module Ci
project.commit_by(oid: sha)
end
+ def path
+ Gitlab::Routing.url_helpers.project_tag_path(project, name)
+ end
+
+ def readme
+ project.repository.tree(sha).readme
+ end
+
private
def update_catalog_resource
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 179befb8469..6a2fb1132c0 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -13,6 +13,7 @@ module Ci
alias_attribute :secret_value, :value
+ validates :description, length: { maximum: 255 }, allow_blank: true
validates :key, uniqueness: {
message: -> (object, data) { _("(%{value}) has already been taken") }
}
diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb
index ff7e681217a..5f55713b436 100644
--- a/app/models/ci/namespace_mirror.rb
+++ b/app/models/ci/namespace_mirror.rb
@@ -7,6 +7,7 @@ module Ci
include FromUnion
belongs_to :namespace
+ has_many :project_mirrors, primary_key: :namespace_id, foreign_key: :namespace_id, inverse_of: :namespace_mirror
scope :by_group_and_descendants, -> (id) do
where('traversal_ids @> ARRAY[?]::int[]', id)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 9d5b2e5a0b1..1bf4d585e1c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -20,7 +20,6 @@ module Ci
include IgnorableColumns
ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22'
- ignore_column :auto_canceled_by_id_convert_to_bigint, remove_with: '16.6', remove_after: '2023-10-22'
MAX_OPEN_MERGE_REQUESTS_REFS = 4
@@ -439,7 +438,7 @@ module Ci
where_exists(Ci::Build.latest.scoped_pipeline.with_artifacts(reports_scope))
end
- scope :with_only_interruptible_builds, -> do
+ scope :conservative_interruptible, -> do
where_not_exists(
Ci::Build.scoped_pipeline.with_status(STARTED_STATUSES).not_interruptible
)
@@ -621,7 +620,7 @@ module Ci
end
def valid_commit_sha
- if self.sha == Gitlab::Git::BLANK_SHA
+ if self.sha == Gitlab::Git::SHA1_BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
end
end
@@ -675,7 +674,7 @@ module Ci
end
def before_sha
- super || Gitlab::Git::BLANK_SHA
+ super || Gitlab::Git::SHA1_BLANK_SHA
end
def short_sha
@@ -1394,6 +1393,10 @@ module Ci
merge_request.merge_request_diff_for(merge_request_diff_sha)
end
+ def auto_cancel_on_new_commit
+ pipeline_metadata&.auto_cancel_on_new_commit || 'conservative'
+ end
+
private
def add_message(severity, content)
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 6d22a875aab..e0e6906f211 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -4,6 +4,7 @@
module Ci
class PipelineArtifact < Ci::ApplicationRecord
+ include Ci::Partitionable
include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
@@ -31,6 +32,8 @@ module Ci
validates :size, presence: true, numericality: { less_than_or_equal_to: FILE_SIZE_LIMIT }
validates :file_type, presence: true
+ partitionable scope: :pipeline
+
mount_file_store_uploader Ci::PipelineArtifactUploader
update_project_statistics project_statistics_name: :pipeline_artifacts_size
diff --git a/app/models/ci/pipeline_chat_data.rb b/app/models/ci/pipeline_chat_data.rb
index ba20c993e36..1a2bc37d17d 100644
--- a/app/models/ci/pipeline_chat_data.rb
+++ b/app/models/ci/pipeline_chat_data.rb
@@ -2,14 +2,21 @@
module Ci
class PipelineChatData < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::NamespacedModelName
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
self.table_name = 'ci_pipeline_chat_data'
belongs_to :chat_name
+ belongs_to :pipeline
validates :pipeline_id, presence: true
validates :chat_name_id, presence: true
validates :response_url, presence: true
+
+ partitionable scope: :pipeline
end
end
diff --git a/app/models/ci/pipeline_config.rb b/app/models/ci/pipeline_config.rb
index e2dcad653d7..11decd3fc66 100644
--- a/app/models/ci/pipeline_config.rb
+++ b/app/models/ci/pipeline_config.rb
@@ -2,11 +2,15 @@
module Ci
class PipelineConfig < Ci::ApplicationRecord
+ include Ci::Partitionable
+
self.table_name = 'ci_pipelines_config'
self.primary_key = :pipeline_id
belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_config
validates :pipeline, presence: true
validates :content, presence: true
+
+ partitionable scope: :pipeline
end
end
diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb
index 37fa3e32ad8..21d102374f0 100644
--- a/app/models/ci/pipeline_metadata.rb
+++ b/app/models/ci/pipeline_metadata.rb
@@ -2,12 +2,15 @@
module Ci
class PipelineMetadata < Ci::ApplicationRecord
+ include Ci::Partitionable
+ include Importable
+
self.primary_key = :pipeline_id
enum auto_cancel_on_new_commit: {
conservative: 0,
interruptible: 1,
- disabled: 2
+ none: 2
}, _prefix: true
enum auto_cancel_on_job_failure: {
@@ -21,5 +24,7 @@ module Ci
validates :pipeline, presence: true
validates :project, presence: true
validates :name, length: { minimum: 1, maximum: 255 }, allow_nil: true
+
+ partitionable scope: :pipeline
end
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index b1831e365b1..4fddb3e053e 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -5,9 +5,6 @@ module Ci
include Ci::Partitionable
include Ci::HasVariable
include Ci::RawVariable
- include IgnorableColumns
-
- ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.5', remove_after: '2023-10-22'
belongs_to :pipeline
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 414d36da7c3..989d6337ab7 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -33,6 +33,10 @@ module Ci
where('NOT EXISTS (?)', needs)
end
+ scope :interruptible, -> do
+ joins(:metadata).merge(Ci::BuildMetadata.with_interruptible)
+ end
+
scope :not_interruptible, -> do
joins(:metadata).where.not(
Ci::BuildMetadata.table_name => { id: Ci::BuildMetadata.scoped_build.with_interruptible.select(:id) }
diff --git a/app/models/ci/project_mirror.rb b/app/models/ci/project_mirror.rb
index 23cd5d92730..c6828f827b5 100644
--- a/app/models/ci/project_mirror.rb
+++ b/app/models/ci/project_mirror.rb
@@ -7,6 +7,8 @@ module Ci
include FromUnion
belongs_to :project
+ belongs_to :namespace_mirror, primary_key: :namespace_id, foreign_key: :namespace_id, inverse_of: :project_mirrors
+ has_many :builds, primary_key: :project_id, foreign_key: :project_id, inverse_of: :project_mirror
scope :by_namespace_id, -> (namespace_id) { where(namespace_id: namespace_id) }
scope :by_project_id, -> (project_id) { where(project_id: project_id) }
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 9c30beeeb59..5fb982ee21e 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -14,6 +14,7 @@ module Ci
include Presentable
include EachBatch
include Ci::HasRunnerExecutor
+ include Ci::HasRunnerStatus
extend ::Gitlab::Utils::Override
@@ -85,22 +86,22 @@ module Ci
before_save :ensure_token
- scope :active, -> (value = true) { where(active: value) }
+ scope :active, ->(value = true) { where(active: value) }
scope :paused, -> { active(false) }
- scope :online, -> { where(arel_table[:contacted_at].gt(online_contact_time_deadline)) }
scope :recent, -> do
timestamp = stale_deadline
where(arel_table[:created_at].gteq(timestamp).or(arel_table[:contacted_at].gteq(timestamp)))
end
scope :stale, -> do
- timestamp = stale_deadline
+ stale_timestamp = stale_deadline
+
+ created_before_stale_deadline = arel_table[:created_at].lteq(stale_timestamp)
+ contacted_before_stale_deadline = arel_table[:contacted_at].lteq(stale_timestamp)
+ never_contacted = arel_table[:contacted_at].eq(nil)
- where(arel_table[:created_at].lteq(timestamp))
- .where(arel_table[:contacted_at].eq(nil).or(arel_table[:contacted_at].lteq(timestamp)))
+ where(created_before_stale_deadline).where(never_contacted.or(contacted_before_stale_deadline))
end
- scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
- scope :never_contacted, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where(arel_table[:contacted_at].gt(recent_queue_deadline)) }
@@ -220,6 +221,11 @@ module Ci
validate :exactly_one_group, if: :group_type?
scope :with_version_prefix, ->(value) { joins(:runner_managers).merge(RunnerManager.with_version_prefix(value)) }
+ scope :with_runner_type, ->(runner_type) do
+ return all if AVAILABLE_TYPES.exclude?(runner_type.to_s)
+
+ where(runner_type: runner_type)
+ end
acts_as_taggable
@@ -348,23 +354,6 @@ module Ci
description
end
- def online?
- contacted_at && contacted_at > self.class.online_contact_time_deadline
- end
-
- def stale?
- return false unless created_at
-
- [created_at, contacted_at].compact.max <= self.class.stale_deadline
- end
-
- def status
- return :stale if stale?
- return :never_contacted unless contacted_at
-
- online? ? :online : :offline
- end
-
# DEPRECATED
# TODO Remove in v5 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
def deprecated_rest_status
@@ -475,6 +464,21 @@ module Ci
end
end
+ def clear_heartbeat
+ cleared_attributes = {
+ version: nil,
+ revision: nil,
+ platform: nil,
+ architecture: nil,
+ ip_address: nil,
+ executor_type: nil,
+ config: {},
+ contacted_at: nil
+ }
+ merge_cache_attributes(cleared_attributes)
+ update_columns(cleared_attributes)
+ end
+
def pick_build!(build)
tick_runner_queue if matches_build?(build)
end
diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb
index e6576859827..44fe1bdd67d 100644
--- a/app/models/ci/runner_manager.rb
+++ b/app/models/ci/runner_manager.rb
@@ -5,10 +5,13 @@ module Ci
include FromUnion
include RedisCacheable
include Ci::HasRunnerExecutor
+ include Ci::HasRunnerStatus
# For legacy reasons, the table name is ci_runner_machines in the database
self.table_name = 'ci_runner_machines'
+ AVAILABLE_STATUSES = %w[online offline never_contacted stale].freeze
+
# The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner Machine DB entry can be updated
UPDATE_CONTACT_COLUMN_EVERY = (40.minutes)..(55.minutes)
@@ -36,19 +39,26 @@ module Ci
STALE_TIMEOUT = 7.days
scope :stale, -> do
- created_some_time_ago = arel_table[:created_at].lteq(STALE_TIMEOUT.ago)
- contacted_some_time_ago = arel_table[:contacted_at].lteq(STALE_TIMEOUT.ago)
+ stale_timestamp = stale_deadline
+
+ created_before_stale_deadline = arel_table[:created_at].lteq(stale_timestamp)
+ contacted_before_stale_deadline = arel_table[:contacted_at].lteq(stale_timestamp)
from_union(
- where(contacted_at: nil),
- where(contacted_some_time_ago),
- remove_duplicates: false).where(created_some_time_ago)
+ never_contacted,
+ where(contacted_before_stale_deadline),
+ remove_duplicates: false
+ ).where(created_before_stale_deadline)
end
scope :for_runner, ->(runner_id) do
where(runner_id: runner_id)
end
+ scope :with_system_xid, ->(system_xid) do
+ where(system_xid: system_xid)
+ end
+
scope :with_running_builds, -> do
where('EXISTS(?)',
Ci::Build.select(1)
@@ -114,25 +124,8 @@ module Ci
end
end
- def status
- return :stale if stale?
- return :never_contacted unless contacted_at
-
- online? ? :online : :offline
- end
-
private
- def online?
- contacted_at && contacted_at > self.class.online_contact_time_deadline
- end
-
- def stale?
- return false unless created_at
-
- [created_at, contacted_at].compact.max <= self.class.stale_deadline
- end
-
def persist_cached_data?
# Use a random threshold to prevent beating DB updates.
contacted_at_max_age = Random.rand(UPDATE_CONTACT_COLUMN_EVERY)
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index becb8f204bf..ba1a0a46247 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -7,9 +7,6 @@ module Ci
include Ci::HasStatus
include Gitlab::OptimisticLocking
include Presentable
- include IgnorableColumns
-
- ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.6', remove_after: '2023-10-22'
partitionable scope: :pipeline
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 886e6e9fbd7..9c8d7604031 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -359,7 +359,7 @@ class Commit
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
+ base_sha: self.parent_id || Gitlab::Git::SHA1_BLANK_SHA,
head_sha: self.sha
)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f1aeb7e528f..3a9b1465682 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -86,7 +86,7 @@ class CommitStatus < Ci::ApplicationRecord
scope :for_project_paths, -> (paths) do
# Pluck is used to split this query. Splitting the query is required for database decomposition for `ci_*` tables.
# https://docs.gitlab.com/ee/development/database/transaction_guidelines.html#database-decomposition-and-sharding
- project_ids = Project.where_full_path_in(Array(paths), use_includes: false).pluck(:id)
+ project_ids = Project.where_full_path_in(Array(paths), preload_routes: false).pluck(:id)
for_project(project_ids)
end
diff --git a/app/models/compare.rb b/app/models/compare.rb
index 58279cb58aa..d80f3f72ca7 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
-require 'set'
+require 'set' # rubocop:disable Lint/RedundantRequireStatement -- Ruby 3.1 and earlier needs this. Drop this line after Ruby 3.2+ is only supported.
class Compare
include Gitlab::Utils::StrongMemoize
include ActsAsPaginatedDiff
- delegate :same, :head, :base, to: :@compare
+ delegate :same, :head, :base, :generated_files, to: :@compare
attr_reader :project
diff --git a/app/models/concerns/analytics/cycle_analytics/parentable.rb b/app/models/concerns/analytics/cycle_analytics/parentable.rb
index 785f6eea6bf..90a38e3c58c 100644
--- a/app/models/concerns/analytics/cycle_analytics/parentable.rb
+++ b/app/models/concerns/analytics/cycle_analytics/parentable.rb
@@ -6,16 +6,7 @@ module Analytics
extend ActiveSupport::Concern
included do
- belongs_to :namespace, class_name: 'Namespace', foreign_key: :group_id, optional: false # rubocop: disable Rails/InverseOf
-
- validate :ensure_namespace_type
-
- def ensure_namespace_type
- return if namespace.nil?
- return if namespace.is_a?(::Namespaces::ProjectNamespace) || namespace.is_a?(::Group)
-
- errors.add(:namespace, s_('CycleAnalytics|the assigned object is not supported'))
- end
+ belongs_to :namespace, class_name: 'Namespace', foreign_key: :group_id, optional: false # rubocop: disable Rails/InverseOf -- this relation is not present on Namespace
end
end
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index ec4ee7985fe..f51b0967968 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -219,8 +219,8 @@ module AtomicInternalId
::AtomicInternalId.scope_usage(self.class)
end
- def self.scope_usage(including_class)
- including_class.table_name.to_sym
+ def self.scope_usage(klass)
+ klass.respond_to?(:internal_id_scope_usage) ? klass.internal_id_scope_usage : klass.table_name.to_sym
end
def self.project_init(klass, column_name = :iid)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 6a855198697..7c7fd882228 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -40,8 +40,6 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE
-
if Feature.enabled?(:personal_snippet_reference_filters, context[:author])
context[:user] = self.parent_user
end
diff --git a/app/models/concerns/ci/has_runner_status.rb b/app/models/concerns/ci/has_runner_status.rb
new file mode 100644
index 00000000000..f6fb9940b44
--- /dev/null
+++ b/app/models/concerns/ci/has_runner_status.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Ci
+ module HasRunnerStatus
+ extend ActiveSupport::Concern
+
+ included do
+ scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
+ scope :never_contacted, -> { where(contacted_at: nil) }
+ scope :online, -> { where(arel_table[:contacted_at].gt(online_contact_time_deadline)) }
+
+ scope :with_status, ->(status) do
+ return all if available_statuses.exclude?(status.to_s)
+
+ public_send(status) # rubocop:disable GitlabSecurity/PublicSend -- safe to call
+ end
+ end
+
+ class_methods do
+ def available_statuses
+ self::AVAILABLE_STATUSES
+ end
+
+ def online_contact_time_deadline
+ raise NotImplementedError
+ end
+
+ def stale_deadline
+ raise NotImplementedError
+ end
+ end
+
+ def status
+ return :stale if stale?
+ return :never_contacted unless contacted_at
+
+ online? ? :online : :offline
+ end
+
+ def online?
+ contacted_at && contacted_at > self.class.online_contact_time_deadline
+ end
+
+ def stale?
+ return false unless created_at
+
+ [created_at, contacted_at].compact.max <= self.class.stale_deadline
+ end
+ end
+end
diff --git a/app/models/concerns/ci/partitionable/testing.rb b/app/models/concerns/ci/partitionable/testing.rb
index b961d72db94..9f0d55329ad 100644
--- a/app/models/concerns/ci/partitionable/testing.rb
+++ b/app/models/concerns/ci/partitionable/testing.rb
@@ -21,6 +21,10 @@ module Ci
Ci::PendingBuild
Ci::RunningBuild
Ci::RunnerManagerBuild
+ Ci::PipelineArtifact
+ Ci::PipelineChatData
+ Ci::PipelineConfig
+ Ci::PipelineMetadata
Ci::PipelineVariable
Ci::Sources::Pipeline
Ci::Stage
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 201994cb321..12e4a5a0ee0 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -9,17 +9,7 @@ module CommitSignature
sha_attribute :commit_sha
- enum verification_status: {
- unverified: 0,
- verified: 1,
- same_user_different_email: 2,
- other_user: 3,
- unverified_key: 4,
- unknown_key: 5,
- multiple_signatures: 6,
- revoked_key: 7,
- verified_system: 8
- }
+ enum verification_status: Enums::CommitSignature.verification_statuses
belongs_to :project, class_name: 'Project', foreign_key: 'project_id', optional: false
diff --git a/app/models/concerns/database_event_tracking.rb b/app/models/concerns/database_event_tracking.rb
deleted file mode 100644
index 7e2f445189e..00000000000
--- a/app/models/concerns/database_event_tracking.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-module DatabaseEventTracking
- extend ActiveSupport::Concern
-
- included do
- after_create_commit :publish_database_create_event
- after_destroy_commit :publish_database_destroy_event
- after_update_commit :publish_database_update_event
- end
-
- def publish_database_create_event
- publish_database_event('create')
- end
-
- def publish_database_destroy_event
- publish_database_event('destroy')
- end
-
- def publish_database_update_event
- publish_database_event('update')
- end
-
- def publish_database_event(name)
- # Gitlab::Tracking#event is triggering Snowplow event
- # Snowplow events are sent with usage of
- # https://snowplow.github.io/snowplow-ruby-tracker/SnowplowTracker/AsyncEmitter.html
- # that reports data asynchronously and does not impact performance nor carries a risk of
- # rollback in case of error
-
- Gitlab::Tracking.database_event(
- self.class.to_s,
- "database_event_#{name}",
- label: self.class.table_name,
- project: try(:project),
- namespace: (try(:group) || try(:namespace)) || try(:project)&.namespace,
- property: name,
- **filtered_record_attributes
- )
- rescue StandardError => err
- # this rescue should be a dead code due to utilization of AsyncEmitter, however
- # since this concern is expected to be included in every model, it is better to
- # prevent against any unexpected outcome
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err)
- end
-
- def filtered_record_attributes
- attributes
- .with_indifferent_access
- .slice(*self.class::SNOWPLOW_ATTRIBUTES)
- end
-end
diff --git a/app/models/concerns/enums/commit_signature.rb b/app/models/concerns/enums/commit_signature.rb
new file mode 100644
index 00000000000..92625af58ef
--- /dev/null
+++ b/app/models/concerns/enums/commit_signature.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Enums
+ class CommitSignature
+ VERIFICATION_STATUSES = {
+ unverified: 0,
+ verified: 1,
+ same_user_different_email: 2,
+ other_user: 3,
+ unverified_key: 4,
+ unknown_key: 5,
+ multiple_signatures: 6,
+ revoked_key: 7,
+ verified_system: 8
+ # EE adds more values in ee/app/models/concerns/ee/enums/commit_signature.rb
+ }.freeze
+
+ def self.verification_statuses
+ VERIFICATION_STATUSES
+ end
+ end
+end
+
+Enums::CommitSignature.prepend_mod
diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb
index cb20955488a..1dffe183475 100644
--- a/app/models/concerns/integrations/enable_ssl_verification.rb
+++ b/app/models/concerns/integrations/enable_ssl_verification.rb
@@ -9,7 +9,8 @@ module Integrations
type: :checkbox,
title: -> { s_('Integrations|SSL verification') },
checkbox_label: -> { s_('Integrations|Enable SSL verification') },
- help: -> { s_('Integrations|Clear if using a self-signed certificate.') }
+ help: -> { s_('Integrations|Clear if using a self-signed certificate.') },
+ description: -> { s_('Enable SSL verification. Defaults to `true` (enabled).') }
end
def initialize_properties
diff --git a/app/models/concerns/integrations/has_issue_tracker_fields.rb b/app/models/concerns/integrations/has_issue_tracker_fields.rb
index 223191fb963..3ce1dd36a5e 100644
--- a/app/models/concerns/integrations/has_issue_tracker_fields.rb
+++ b/app/models/concerns/integrations/has_issue_tracker_fields.rb
@@ -10,16 +10,16 @@ module Integrations
field :project_url,
required: true,
title: -> { _('Project URL') },
- help: -> do
- s_('IssueTracker|The URL to the project in the external issue tracker.')
- end
+ description: -> { s_('URL of the project.') },
+ help: -> { s_('IssueTracker|URL of the project in the external issue tracker.') }
field :issues_url,
required: true,
title: -> { s_('IssueTracker|Issue URL') },
+ description: -> { s_('URL of the issue.') },
help: -> do
ERB::Util.html_escape(
- s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.')
+ s_('IssueTracker|URL to view an issue in the external issue tracker. Must contain %{colon_id}.')
) % {
colon_id: '<code>:id</code>'.html_safe
}
@@ -28,9 +28,8 @@ module Integrations
field :new_issue_url,
required: true,
title: -> { s_('IssueTracker|New issue URL') },
- help: -> do
- s_('IssueTracker|The URL to create an issue in the external issue tracker.')
- end
+ description: -> { s_('URL of the new issue.') },
+ help: -> { s_('IssueTracker|URL to create an issue in the external issue tracker.') }
end
end
end
diff --git a/app/models/concerns/integrations/slack_mattermost_fields.rb b/app/models/concerns/integrations/slack_mattermost_fields.rb
index a8e63c4e405..08f86813cc1 100644
--- a/app/models/concerns/integrations/slack_mattermost_fields.rb
+++ b/app/models/concerns/integrations/slack_mattermost_fields.rb
@@ -7,26 +7,40 @@ module Integrations
included do
field :webhook,
help: -> { webhook_help },
+ description: -> do
+ Kernel.format(_("%{title} webhook (for example, `%{example}`)."), title: title, example: webhook_help)
+ end,
required: true,
if: -> { requires_webhook? }
field :username,
placeholder: 'GitLab-integration',
+ description: -> { Kernel.format(_("%{title} username."), title: title) },
if: -> { requires_webhook? }
+ field :channel,
+ description: -> { _('Default channel to use if no other channel is configured.') },
+ api_only: true
+
field :notify_only_broken_pipelines,
type: :checkbox,
section: Integration::SECTION_TYPE_CONFIGURATION,
+ description: -> { _('Send notifications for broken pipelines.') },
help: 'Do not send notifications for successful pipelines.'
field :branches_to_be_notified,
type: :select,
section: Integration::SECTION_TYPE_CONFIGURATION,
title: -> { s_('Integration|Branches for which notifications are to be sent') },
+ description: -> {
+ _('Branches to send notifications for. Valid options are `all`, `default`, `protected`, ' \
+ 'and `default_and_protected`. The default value is `default`.')
+ },
choices: -> { branch_choices }
field :labels_to_be_notified,
section: Integration::SECTION_TYPE_CONFIGURATION,
+ description: -> { _('Labels to send notifications for. Leave blank to receive notifications for all events.') },
placeholder: '~backend,~frontend',
help: 'Send notifications for issue, merge request, and comment events with the listed labels only. ' \
'Leave blank to receive notifications for all events.'
@@ -34,6 +48,10 @@ module Integrations
field :labels_to_be_notified_behavior,
type: :select,
section: Integration::SECTION_TYPE_CONFIGURATION,
+ description: -> {
+ _('Labels to be notified for. Valid options are `match_any` and `match_all`. ' \
+ 'The default value is `match_any`.')
+ },
choices: [
['Match any of the labels', Integrations::BaseChatNotification::MATCH_ANY_LABEL],
['Match all of the labels', Integrations::BaseChatNotification::MATCH_ALL_LABELS]
diff --git a/app/models/concerns/partitioned_table.rb b/app/models/concerns/partitioned_table.rb
index c322a736e79..8feb162207d 100644
--- a/app/models/concerns/partitioned_table.rb
+++ b/app/models/concerns/partitioned_table.rb
@@ -9,7 +9,8 @@ module PartitionedTable
PARTITIONING_STRATEGIES = {
monthly: Gitlab::Database::Partitioning::MonthlyStrategy,
sliding_list: Gitlab::Database::Partitioning::SlidingListStrategy,
- ci_sliding_list: Gitlab::Database::Partitioning::CiSlidingListStrategy
+ ci_sliding_list: Gitlab::Database::Partitioning::CiSlidingListStrategy,
+ int_range: Gitlab::Database::Partitioning::IntRangeStrategy
}.freeze
def partitioned_by(partitioning_key, strategy:, **kwargs)
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
index 87b62214529..8fcf0532151 100644
--- a/app/models/concerns/restricted_signup.rb
+++ b/app/models/concerns/restricted_signup.rb
@@ -31,10 +31,10 @@ module RestrictedSignup
def error_message
{
admin: {
- allowlist: html_escape_once(_("Go to the 'Admin area &gt; Sign-up restrictions', and check 'Allowed domains for sign-ups'.")).html_safe,
- denylist: html_escape_once(_("Go to the 'Admin area &gt; Sign-up restrictions', and check the 'Domain denylist'.")).html_safe,
- restricted: html_escape_once(_("Go to the 'Admin area &gt; Sign-up restrictions', and check 'Email restrictions for sign-ups'.")).html_safe,
- group_setting: html_escape_once(_("Go to the group’s 'Settings &gt; General' page, and check 'Restrict membership by email domain'.")).html_safe
+ allowlist: ERB::Util.html_escape_once(_("Go to the 'Admin area &gt; Sign-up restrictions', and check 'Allowed domains for sign-ups'.")).html_safe,
+ denylist: ERB::Util.html_escape_once(_("Go to the 'Admin area &gt; Sign-up restrictions', and check the 'Domain denylist'.")).html_safe,
+ restricted: ERB::Util.html_escape_once(_("Go to the 'Admin area &gt; Sign-up restrictions', and check 'Email restrictions for sign-ups'.")).html_safe,
+ group_setting: ERB::Util.html_escape_once(_("Go to the group’s 'Settings &gt; General' page, and check 'Restrict membership by email domain'.")).html_safe
},
nonadmin: {
allowlist: error_nonadmin,
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 242194be440..43874d0211c 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -87,37 +87,27 @@ module Routable
# Klass.where_full_path_in(%w{gitlab-org/gitlab-foss gitlab-org/gitlab})
#
# Returns an ActiveRecord::Relation.
- def where_full_path_in(paths, use_includes: true)
+ def where_full_path_in(paths, preload_routes: true)
return none if paths.empty?
- wheres = paths.map do |path|
+ path_condition = paths.map do |path|
"(LOWER(routes.path) = LOWER(#{connection.quote(path)}))"
- end
+ end.join(' OR ')
- if Feature.enabled?(:optimize_where_full_path_in, Feature.current_request)
- route_scope = all
- source_type_condition = { source_type: route_scope.klass.base_class }
+ route_scope = all
+ source_type_condition = { source_type: route_scope.klass.base_class }
- routes_matching_condition = Route.where(source_type_condition).where(wheres.join(' OR '))
+ routes_matching_condition = Route
+ .where(source_type_condition)
+ .where(path_condition)
- result = route_scope.where(id: routes_matching_condition.pluck(:source_id))
+ source_ids = routes_matching_condition.pluck(:source_id)
+ result = route_scope.where(id: source_ids)
- if use_includes
- result.preload(:route)
- else
- result
- end
+ if preload_routes
+ result.preload(:route)
else
- route =
- if use_includes
- includes(:route).references(:routes)
- else
- joins(:route)
- end
-
- route
- .where(wheres.join(' OR '))
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+ result
end
end
end
diff --git a/app/models/container_registry/protection/rule.rb b/app/models/container_registry/protection/rule.rb
index a7324b3b3b8..34d00bdef2f 100644
--- a/app/models/container_registry/protection/rule.rb
+++ b/app/models/container_registry/protection/rule.rb
@@ -19,6 +19,23 @@ module ContainerRegistry
validates :repository_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 }
validates :delete_protected_up_to_access_level, presence: true
validates :push_protected_up_to_access_level, presence: true
+
+ scope :for_repository_path, ->(repository_path) do
+ return none if repository_path.blank?
+
+ where(
+ ":repository_path ILIKE #{::Gitlab::SQL::Glob.to_like('repository_path_pattern')}",
+ repository_path: repository_path
+ )
+ end
+
+ def self.for_push_exists?(access_level:, repository_path:)
+ return false if access_level.blank? || repository_path.blank?
+
+ where(push_protected_up_to_access_level: access_level..)
+ .for_repository_path(repository_path)
+ .exists?
+ end
end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 6bcfd23e69c..3b1c10c0259 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -482,7 +482,7 @@ class ContainerRepository < ApplicationRecord
raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES
end
- def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100)
+ def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100, referrers: nil)
raise ArgumentError, 'not a migrated repository' unless migrated?
page = gitlab_api_client.tags(
@@ -491,7 +491,8 @@ class ContainerRepository < ApplicationRecord
before: before,
last: last,
sort: sort,
- name: name
+ name: name,
+ referrers: referrers
)
{
@@ -618,12 +619,11 @@ class ContainerRepository < ApplicationRecord
self.new(project: path.repository_project, name: path.repository_name)
end
- def self.find_or_create_from_path(path)
- repository = safe_find_or_create_by(
- project: path.repository_project,
+ def self.find_or_create_from_path!(path)
+ ContainerRepository.upsert({
+ project_id: path.repository_project.id,
name: path.repository_name
- )
- return repository if repository.persisted?
+ }, unique_by: %i[project_id name])
find_by_path!(path)
end
@@ -657,6 +657,8 @@ class ContainerRepository < ApplicationRecord
tag.total_size = raw_tag['size_bytes']
tag.manifest_digest = raw_tag['digest']
tag.revision = raw_tag['config_digest'].to_s.split(':')[1] || ''
+ tag.referrers = raw_tag['referrers']
+ tag.published_at = raw_tag['published_at']
tag
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 36f4a0ef426..1fff089451d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -9,6 +9,7 @@ class Deployment < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include FastDestroyAll
include IgnorableColumns
+ include EachBatch
StatusUpdateError = Class.new(StandardError)
StatusSyncError = Class.new(StandardError)
@@ -230,7 +231,7 @@ class Deployment < ApplicationRecord
##
# FastDestroyAll concerns
def begin_fast_destroy
- preload(:project).find_each.map do |deployment|
+ preload(:project, :environment).find_each.map do |deployment|
[deployment.project, deployment.ref_path]
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index ac843f392fd..bbf34ce21c0 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -37,8 +37,8 @@ class Group < Namespace
has_many :all_group_members, -> { non_request }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :all_owner_members, -> { non_request.all_owners }, as: :source, class_name: 'GroupMember'
- has_many :group_members, -> { non_request.where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
- has_many :namespace_members, -> { non_request.where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }).unscope(where: %i[source_id source_type]) },
+ has_many :group_members, -> { non_request.non_minimal_access }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+ has_many :namespace_members, -> { non_request.non_minimal_access.unscope(where: %i[source_id source_type]) },
foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
alias_method :members, :group_members
@@ -338,6 +338,18 @@ class Group < Namespace
by_ids_or_paths(ids, paths).pluck(:id)
end
+ def descendant_groups_counts
+ left_joins(:children).group(:id).count(:children_namespaces)
+ end
+
+ def projects_counts
+ left_joins(:non_archived_projects).group(:id).count(:projects)
+ end
+
+ def group_members_counts
+ left_joins(:group_members).group(:id).count(:members)
+ end
+
private
def public_to_user_arel(user)
@@ -434,7 +446,9 @@ class Group < Namespace
end
def owned_by?(user)
- owners.include?(user)
+ return false unless user
+
+ all_owner_members.non_invite.exists?(user: user)
end
def add_members(users, access_level, current_user: nil, expires_at: nil)
@@ -593,6 +607,14 @@ class Group < Namespace
end
end
+ # Only for direct and not requested members with higher access level than MIMIMAL_ACCESS
+ # It returns true for non-active users
+ def has_user?(user)
+ return false unless user
+
+ group_members.non_invite.exists?(user: user)
+ end
+
def direct_members
GroupMember.active_without_invites_and_requests
.non_minimal_access
@@ -685,7 +707,11 @@ class Group < Namespace
end
def highest_group_member(user)
- GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
+ GroupMember
+ .where(source_id: self_and_ancestors_ids, user_id: user.id)
+ .non_request
+ .order(:access_level)
+ .last
end
def bots
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 618f9f986e8..8ebf24b1663 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -19,8 +19,8 @@ class Integration < ApplicationRecord
self.inheritance_column = :type_new
INTEGRATION_NAMES = %w[
- asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker datadog discord
- drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira
+ asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker
+ datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram
unify_circuit webex_teams youtrack zentao
@@ -638,7 +638,9 @@ class Integration < ApplicationRecord
end
def validate_belongs_to_project_or_group
- errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level?
+ return unless project_level? && group_level?
+
+ errors.add(:project_id, 'The integration cannot belong to both a project and a group')
end
def validate_recipients?
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
index a248a1aa561..152bcf934ae 100644
--- a/app/models/integrations/apple_app_store.rb
+++ b/app/models/integrations/apple_app_store.rb
@@ -45,7 +45,7 @@ module Integrations
section: SECTION_TYPE_CONFIGURATION,
title: -> { s_('AppleAppStore|Protected branches and tags only') },
description: -> { s_('AppleAppStore|Set variables on protected branches and tags only.') },
- checkbox_label: -> { s_('AppleAppStore|Set variables on protected branches and tags only.') }
+ checkbox_label: -> { s_('AppleAppStore|Set variables on protected branches and tags only') }
def self.title
'Apple App Store Connect'
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 9fe73f86be3..1c68d09aa2f 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -8,12 +8,14 @@ module Integrations
field :bamboo_url,
title: -> { s_('BambooService|Bamboo URL') },
placeholder: -> { s_('https://bamboo.example.com') },
- help: -> { s_('BambooService|Bamboo service root URL.') },
+ help: -> { s_('BambooService|Bamboo root URL.') },
+ description: -> { s_('Bamboo root URL (for example, `https://bamboo.example.com`).') },
exposes_secrets: true,
required: true
field :build_key,
help: -> { s_('BambooService|Bamboo build plan key.') },
+ description: -> { s_('Bamboo build plan key (for example, `KEY`).') },
non_empty_password_title: -> { s_('BambooService|Enter new build key') },
non_empty_password_help: -> { s_('BambooService|Leave blank to use your current build key.') },
placeholder: -> { _('KEY') },
@@ -21,12 +23,16 @@ module Integrations
is_secret: true
field :username,
- help: -> { s_('BambooService|The user with API access to the Bamboo server.') }
+ help: -> { s_('BambooService|User with API access to the Bamboo server.') },
+ description: -> { s_('User with API access to the Bamboo server.') },
+ required: true
field :password,
type: :password,
non_empty_password_title: -> { s_('ProjectService|Enter new password') },
- non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') }
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') },
+ description: -> { s_('Password of the user.') },
+ required: true
with_options if: :activated? do
validates :bamboo_url, presence: true, public_url: true
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 18268ed18f4..783311ca18d 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -15,6 +15,9 @@ module Integrations
field :token,
type: :password,
title: -> { _('Campfire token') },
+ description: -> do
+ _('API authentication token from Campfire. To get the token, sign in to Campfire and select **My info**.')
+ end,
help: -> { s_('CampfireService|API authentication token from Campfire.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
@@ -23,18 +26,22 @@ module Integrations
field :subdomain,
title: -> { _('Campfire subdomain (optional)') },
+ description: -> do
+ _("`.campfirenow.com` subdomain when you're signed in.")
+ end,
placeholder: '',
exposes_secrets: true,
help: -> do
format(ERB::Util.html_escape(
- s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.')
+ s_('CampfireService|%{code_open}.campfirenow.com%{code_close} subdomain.')
), code_open: '<code>'.html_safe, code_close: '</code>'.html_safe)
end
field :room,
title: -> { _('Campfire room ID (optional)') },
+ description: -> { _("ID portion of the Campfire room URL.") },
placeholder: '123456',
- help: -> { s_('CampfireService|From the end of the room URL.') }
+ help: -> { s_('CampfireService|ID portion of the Campfire room URL.') }
def self.title
'Campfire'
diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb
index 25287b53300..1737aa7ff61 100644
--- a/app/models/integrations/clickup.rb
+++ b/app/models/integrations/clickup.rb
@@ -32,8 +32,8 @@ module Integrations
'clickup'
end
- def fields
- super.select { _1.name.in?(%w[project_url issues_url]) }
+ def self.fields
+ super.select { %w[project_url issues_url].include?(_1.name) }
end
end
end
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index f97f1fd25c9..fcdc908ca67 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -10,7 +10,8 @@ module Integrations
validate :validate_confluence_url_is_cloud, if: :activated?
field :confluence_url,
- title: -> { _('Confluence Cloud Workspace URL') },
+ title: -> { _('Confluence Workspace URL') },
+ description: -> { _("URL of the Confluence Workspace hosted on `atlassian.net`.") },
placeholder: 'https://example.atlassian.net/wiki',
required: true
diff --git a/app/models/integrations/diffblue_cover.rb b/app/models/integrations/diffblue_cover.rb
new file mode 100644
index 00000000000..c0e0cae2b33
--- /dev/null
+++ b/app/models/integrations/diffblue_cover.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Integrations
+ class DiffblueCover < Integration
+ field :diffblue_license_key,
+ section: SECTION_TYPE_CONNECTION,
+ type: :password,
+ title: -> { s_('DiffblueCover|License key') },
+ description: -> { s_('DiffblueCover|Diffblue Cover license key.') },
+ non_empty_password_title: -> { s_('DiffblueCover|License key') },
+ non_empty_password_help: -> {
+ s_(
+ 'DiffblueCover|Leave blank to use your current license key.'
+ )
+ },
+ exposes_secrets: true,
+ required: true,
+ is_secret: true,
+ placeholder: 'XXXX-XXXX-XXXX-XXXX',
+ help: -> {
+ format(
+ s_(
+ 'DiffblueCover|Enter your Diffblue Cover license key or ' \
+ 'go to %{diffblue_link} to obtain a free trial license.'
+ ),
+ diffblue_link: diffblue_link
+ )
+ }
+
+ field :diffblue_access_token_name,
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('DiffblueCover|Name') },
+ description: -> { s_('DiffblueCover|Access token name used by Diffblue Cover in pipelines.') },
+ required: true,
+ placeholder: -> { s_('DiffblueCover|My token name') }
+
+ field :diffblue_access_token_secret,
+ section: SECTION_TYPE_CONFIGURATION,
+ type: :password,
+ title: -> { s_('DiffblueCover|Secret') },
+ description: -> { s_('DiffblueCover|Access token secret used by Diffblue Cover in pipelines.') },
+ non_empty_password_title: -> { s_('DiffblueCover|Secret') },
+ non_empty_password_help: -> { s_('DiffblueCover|Leave blank to use your current secret value.') },
+ required: true,
+ is_secret: true,
+ placeholder: 'glpat-XXXXXXXXXXXXXXXXXXXX' # gitleaks:allow
+
+ with_options if: :activated? do
+ validates :diffblue_license_key, presence: true
+ validates :diffblue_access_token_name, presence: true
+ validates :diffblue_access_token_secret, presence: true
+ end
+
+ def self.title
+ 'Diffblue Cover'
+ end
+
+ def self.description
+ s_('DiffblueCover|Automatically write comprehensive, human-like Java unit tests.')
+ end
+
+ def self.to_param
+ 'diffblue_cover'
+ end
+
+ def self.help
+ s_('DiffblueCover|Automatically write comprehensive, human-like Java unit tests.')
+ end
+
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/diffblue.svg')
+ end
+
+ def self.supported_events
+ []
+ end
+
+ def sections
+ [
+ {
+ type: SECTION_TYPE_CONNECTION,
+ title: s_('DiffblueCover|Integration details'),
+ description:
+ s_(
+ 'DiffblueCover|Diffblue Cover is a generative AI platform that automatically ' \
+ 'writes comprehensive, human-like Java unit tests. Integrate Diffblue ' \
+ 'Cover into your CI/CD workflow for fully autonomous operation.'
+ )
+ },
+ {
+ type: SECTION_TYPE_CONFIGURATION,
+ title: s_('DiffblueCover|Access token'),
+ description:
+ 'You must have a GitLab access token for Diffblue Cover to access your project. ' \
+ 'Use a GitLab access token with at least the Developer role and ' \
+ 'the <code>api</code> and <code>write_repository</code> permissions.'
+ }
+ ]
+ end
+
+ def execute(_data) end
+
+ def ci_variables
+ return [] unless activated?
+
+ [
+ { key: 'DIFFBLUE_LICENSE_KEY', value: diffblue_license_key, public: false, masked: true },
+ { key: 'DIFFBLUE_ACCESS_TOKEN_NAME', value: diffblue_access_token_name, public: false, masked: true },
+ { key: 'DIFFBLUE_ACCESS_TOKEN', value: diffblue_access_token_secret, public: false, masked: true }
+ ]
+ end
+
+ def testable?
+ false
+ end
+
+ def self.diffblue_link
+ ActionController::Base.helpers.link_to(
+ s_('DiffblueCover|Try Diffblue Cover'),
+ 'https://www.diffblue.com/try-cover/gitlab/',
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+ end
+ end
+end
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 7ce597389f0..f36170f91d0 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -8,17 +8,20 @@ module Integrations
field :webhook,
section: SECTION_TYPE_CONNECTION,
+ description: -> { _('Discord webhook (for example, `https://discord.com/api/webhooks/…`).') },
help: 'e.g. https://discord.com/api/webhooks/…',
required: true
field :notify_only_broken_pipelines,
type: :checkbox,
- section: SECTION_TYPE_CONFIGURATION
+ section: SECTION_TYPE_CONFIGURATION,
+ description: -> { _('Send notifications for broken pipelines.') }
field :branches_to_be_notified,
type: :select,
section: SECTION_TYPE_CONFIGURATION,
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ description: -> { _('Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default value is `default`.') },
choices: -> { branch_choices }
def self.title
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index 7408f86d231..e5360e58426 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -7,6 +7,7 @@ module Integrations
field :external_wiki_url,
section: SECTION_TYPE_CONNECTION,
title: -> { s_('ExternalWikiService|External wiki URL') },
+ description: -> { s_('ExternalWikiService|URL of the external wiki.') },
placeholder: -> { s_('ExternalWikiService|https://example.com/xxx/wiki/...') },
help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') },
required: true
diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb
index 746f68fdc4c..1d6d563e37f 100644
--- a/app/models/integrations/google_play.rb
+++ b/app/models/integrations/google_play.rb
@@ -18,19 +18,25 @@ module Integrations
field :package_name,
section: SECTION_TYPE_CONNECTION,
placeholder: 'com.example.myapp',
+ description: -> { _('Package name of the app in Google Play.') },
required: true
field :service_account_key_file_name,
section: SECTION_TYPE_CONNECTION,
- required: true
+ required: true,
+ description: -> { _('File name of the Google Play service account key.') }
- field :service_account_key, api_only: true
+ field :service_account_key,
+ required: true,
+ description: -> { _('Google Play service account key.') },
+ api_only: true
field :google_play_protected_refs,
type: :checkbox,
section: SECTION_TYPE_CONFIGURATION,
title: -> { s_('GooglePlayStore|Protected branches and tags only') },
- checkbox_label: -> { s_('GooglePlayStore|Only set variables on protected branches and tags') }
+ description: -> { _('Set variables on protected branches and tags only.') },
+ checkbox_label: -> { s_('GooglePlayStore|Set variables on protected branches and tags only') }
def self.title
s_('GooglePlay|Google Play')
@@ -48,10 +54,10 @@ module Integrations
# rubocop:disable Layout/LineLength
texts = [
- s_("Use the Google Play integration to connect to Google Play with fastlane in CI/CD pipelines."),
- s_("After you enable the integration, the following protected variable is created for CI/CD use:"),
+ s_("Use this integration to connect to Google Play with fastlane in CI/CD pipelines."),
+ s_("After you enable the integration, the following protected variables are created for CI/CD use:"),
variable_list.join('<br>'),
- s_(format("To generate a Google Play service account key and use this integration, see the <a href='%{url}' target='_blank'>integration documentation</a>.", url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/google_play'))).html_safe
+ s_(format("For more information, see the <a href='%{url}' target='_blank'>documentation</a>.", url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/google_play'))).html_safe
]
# rubocop:enable Layout/LineLength
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index cc570e49e36..a1621588cd6 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -10,6 +10,7 @@ module Integrations
field :url,
title: -> { s_('HarborIntegration|Harbor URL') },
+ description: -> { _('The base URL to the Harbor instance linked to the GitLab project. For example, `https://demo.goharbor.io`.') },
placeholder: 'https://demo.goharbor.io',
help: -> { s_('HarborIntegration|Base URL of the Harbor instance.') },
exposes_secrets: true,
@@ -17,16 +18,19 @@ module Integrations
field :project_name,
title: -> { s_('HarborIntegration|Harbor project name') },
+ description: -> { s_('HarborIntegration|The name of the project in the Harbor instance. For example, `testproject`.') },
help: -> { s_('HarborIntegration|The name of the project in Harbor.') },
required: true
field :username,
title: -> { s_('HarborIntegration|Harbor username') },
+ description: -> { s_('HarborIntegration|The username created in the Harbor interface.') },
required: true
field :password,
type: :password,
title: -> { s_('HarborIntegration|Harbor password') },
+ description: -> { s_('HarborIntegration|The password of the user.') },
help: -> { s_('HarborIntegration|Password for your Harbor username.') },
non_empty_password_title: -> { s_('HarborIntegration|Enter new Harbor password') },
non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') },
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index 361ff4afce8..e7be2b2a454 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -27,7 +27,7 @@ module Integrations
end
def self.webhook_help
- 'http://mattermost.example.com/hooks/'
+ 'http://mattermost.example.com/hooks/...'
end
override :configurable_channels?
diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb
index 29ed563a902..dcbda8d1ed0 100644
--- a/app/models/integrations/mattermost_slash_commands.rb
+++ b/app/models/integrations/mattermost_slash_commands.rb
@@ -8,8 +8,10 @@ module Integrations
field :token,
type: :password,
+ description: -> { _('The Mattermost token.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ required: true,
placeholder: ''
def testable?
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index 9f9614a84fd..0c1fd34fccf 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -18,7 +18,7 @@ module Integrations
end
def self.webhook_help
- 'https://hooks.slack.com/services/…'
+ 'https://hooks.slack.com/services/...'
end
private
diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb
index 1b4ab152b1d..7aaef0c22cc 100644
--- a/app/models/integrations/squash_tm.rb
+++ b/app/models/integrations/squash_tm.rb
@@ -7,12 +7,14 @@ module Integrations
field :url,
placeholder: 'https://your-instance.squashcloud.io/squash/plugin/xsquash4gitlab/webhook/issue',
title: -> { s_('SquashTmIntegration|Squash TM webhook URL') },
+ description: -> { s_('URL of the Squash TM webhook.') },
exposes_secrets: true,
required: true
field :token,
type: :password,
title: -> { s_('SquashTmIntegration|Secret token (optional)') },
+ description: -> { s_('Secret token.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
required: false
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index 932e588a829..4d825adb961 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -31,8 +31,8 @@ module Integrations
'youtrack'
end
- def fields
- super.select { _1.name.in?(%w[project_url issues_url]) }
+ def self.fields
+ super.select { %w[project_url issues_url].include?(_1.name) }
end
end
end
diff --git a/app/models/issue_email_participant.rb b/app/models/issue_email_participant.rb
index 9d7e2afa1d9..bb03b3d72e6 100644
--- a/app/models/issue_email_participant.rb
+++ b/app/models/issue_email_participant.rb
@@ -3,6 +3,7 @@
class IssueEmailParticipant < ApplicationRecord
include BulkInsertSafe
include Presentable
+ include CaseSensitivity
belongs_to :issue
@@ -10,6 +11,8 @@ class IssueEmailParticipant < ApplicationRecord
validates :issue, presence: true
validate :validate_email_format
+ scope :with_emails, ->(emails) { iwhere(email: emails) }
+
def validate_email_format
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
end
diff --git a/app/models/jira_connect_subscription.rb b/app/models/jira_connect_subscription.rb
index c74f75b2d8e..8ff89560f09 100644
--- a/app/models/jira_connect_subscription.rb
+++ b/app/models/jira_connect_subscription.rb
@@ -8,5 +8,5 @@ class JiraConnectSubscription < ApplicationRecord
validates :namespace, presence: true, uniqueness: { scope: :jira_connect_installation_id, message: 'has already been added' }
scope :preload_namespace_route, -> { preload(namespace: :route) }
- scope :for_project, -> (project) { where(namespace_id: project.namespace.self_and_ancestors) }
+ scope :for_project, -> (project) { where(namespace_id: project.namespace.self_and_ancestor_ids) }
end
diff --git a/app/models/label.rb b/app/models/label.rb
index d0d278b68fd..8fff42abd58 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -46,7 +46,6 @@ class Label < ApplicationRecord
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
scope :with_lock_on_merge, -> { where(lock_on_merge: true) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
- scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) }
scope :order_name_asc, -> { reorder(title: :asc) }
scope :order_name_desc, -> { reorder(title: :desc) }
scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
@@ -152,10 +151,6 @@ class Label < ApplicationRecord
nil
end
- def self.ids_on_board(board_id)
- on_board(board_id).pluck(:label_id)
- end
-
# Searches for labels with a matching title or description.
#
# This method uses ILIKE on PostgreSQL.
diff --git a/app/models/member.rb b/app/models/member.rb
index 25dae518406..8bec64932b3 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -276,9 +276,11 @@ class Member < ApplicationRecord
after_create :send_invite, if: :invite?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
+ after_create :update_two_factor_requirement, unless: :invite?
after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
after_destroy :destroy_notification_setting
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
+ after_destroy :update_two_factor_requirement, unless: :invite?
after_save :log_invitation_token_cleanup
after_commit :send_request, if: :request?, unless: :importing?, on: [:create]
@@ -286,6 +288,14 @@ class Member < ApplicationRecord
refresh_member_authorized_projects
end
+ after_create if: :update_organization_user? do
+ Organizations::OrganizationUser.upsert(
+ { organization_id: source.organization_id, user_id: user_id, access_level: :default },
+ unique_by: [:organization_id, :user_id],
+ on_duplicate: :skip # Do not change access_level, could make :owner :default
+ )
+ end
+
attribute :notification_level, default: -> { NotificationSetting.levels[:global] }
class << self
@@ -486,7 +496,10 @@ class Member < ApplicationRecord
strong_memoize(:highest_group_member) do
next unless user_id && source&.ancestors&.any?
- GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
+ GroupMember
+ .where(source: source.ancestors, user_id: user_id)
+ .non_request
+ .order(:access_level).last
end
end
@@ -498,6 +511,17 @@ class Member < ApplicationRecord
created_by&.name
end
+ def update_two_factor_requirement
+ return unless source.is_a?(Group)
+ return unless user
+
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288'
+ ) do
+ user.update_two_factor_requirement
+ end
+ end
+
private
# TODO: https://gitlab.com/groups/gitlab-org/-/epics/7054
@@ -513,7 +537,7 @@ class Member < ApplicationRecord
end
def send_invite
- # override in subclass
+ run_after_commit_or_now { notification_service.invite_member(self, @raw_invite_token) }
end
def send_request
@@ -522,10 +546,26 @@ class Member < ApplicationRecord
end
def post_create_hook
+ # The creator of a personal project gets added as a `ProjectMember`
+ # with `OWNER` access during creation of a personal project,
+ # but we do not want to trigger notifications to the same person who created the personal project.
+ unless source.is_a?(Project) && source.personal_namespace_holder?(user)
+ event_service.join_source(source, user)
+ run_after_commit_or_now { notification_service.new_member(self) }
+ end
+
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
+ if saved_change_to_access_level?
+ run_after_commit { notification_service.updated_member_access_level(self) }
+ end
+
+ if saved_change_to_expires_at?
+ run_after_commit { notification_service.updated_member_expiration(self) }
+ end
+
system_hook_service.execute_hooks_for(self, :update)
end
@@ -548,6 +588,12 @@ class Member < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def after_accept_invite
+ run_after_commit_or_now do
+ notification_service.accept_invite(self)
+ end
+
+ update_two_factor_requirement
+
post_create_hook
end
@@ -578,7 +624,12 @@ class Member < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def notifiable_options
- {}
+ case source
+ when Group
+ { group: source }
+ when Project
+ { project: source }
+ end
end
def higher_access_level_than_group
@@ -617,12 +668,22 @@ class Member < ApplicationRecord
user&.project_bot?
end
+ def update_organization_user?
+ return false unless Feature.enabled?(:update_organization_users, source.root_ancestor, type: :gitlab_com_derisk)
+
+ !invite? && source.organization.present?
+ end
+
def log_invitation_token_cleanup
return true unless Gitlab.com? && invite? && invite_accepted_at?
error = StandardError.new("Invitation token is present but invite was already accepted!")
Gitlab::ErrorTracking.track_exception(error, attributes.slice(%w["invite_accepted_at created_at source_type source_id user_id id"]))
end
+
+ def event_service
+ EventCreateService.new # rubocop:todo CodeReuse/ServiceClass -- Legacy, convert to value object eventually
+ end
end
Member.prepend_mod_with('Member')
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index e3ead1b04d0..b04fb1f6768 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -18,25 +18,12 @@ class GroupMember < Member
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
- scope :of_groups, ->(groups) { where(source_id: groups&.select(:id)) }
+ scope :of_groups, ->(groups) { where(source_id: groups) }
scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> { group(:source_id).count }
- after_create :update_two_factor_requirement, unless: :invite?
- after_destroy :update_two_factor_requirement, unless: :invite?
-
attr_accessor :last_owner
- def update_two_factor_requirement
- return unless user
-
- Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
- %w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288'
- ) do
- user.update_two_factor_requirement
- end
- end
-
# For those who get to see a modal with a role dropdown, here are the options presented
def self.permissible_access_level_roles(_, _)
# This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
@@ -56,10 +43,6 @@ class GroupMember < Member
Group.sti_name
end
- def notifiable_options
- { group: group }
- end
-
def last_owner_of_the_group?
return false unless access_level == Gitlab::Access::OWNER
return last_owner unless last_owner.nil?
@@ -87,40 +70,6 @@ class GroupMember < Member
super
end
-
- def send_invite
- run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
-
- super
- end
-
- def post_create_hook
- run_after_commit_or_now { notification_service.new_group_member(self) }
-
- super
- end
-
- def post_update_hook
- if saved_change_to_access_level?
- run_after_commit { notification_service.update_group_member(self) }
- end
-
- if saved_change_to_expires_at?
- run_after_commit { notification_service.updated_group_member_expiration(self) }
- end
-
- super
- end
-
- def after_accept_invite
- run_after_commit_or_now do
- notification_service.accept_group_invite(self)
- end
-
- update_two_factor_requirement
-
- super
- end
end
GroupMember.prepend_mod_with('GroupMember')
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index f52fef9e247..a2927238e54 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -72,10 +72,6 @@ class ProjectMember < Member
source
end
- def notifiable_options
- { project: project }
- end
-
def holder_of_the_personal_namespace?
project.personal_namespace_holder?(user)
end
@@ -116,32 +112,6 @@ class ProjectMember < Member
self.member_namespace_id = project&.project_namespace_id
end
- def send_invite
- run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) }
-
- super
- end
-
- def post_create_hook
- # The creator of a personal project gets added as a `ProjectMember`
- # with `OWNER` access during creation of a personal project,
- # but we do not want to trigger notifications to the same person who created the personal project.
- unless project.personal_namespace_holder?(user)
- event_service.join_project(self.project, self.user)
- run_after_commit_or_now { notification_service.new_project_member(self) }
- end
-
- super
- end
-
- def post_update_hook
- if saved_change_to_access_level?
- run_after_commit { notification_service.update_project_member(self) }
- end
-
- super
- end
-
def post_destroy_hook
if expired?
event_service.expired_leave_project(self.project, self.user)
@@ -151,20 +121,6 @@ class ProjectMember < Member
super
end
-
- def after_accept_invite
- run_after_commit_or_now do
- notification_service.accept_project_invite(self)
- end
-
- super
- end
-
- # rubocop: disable CodeReuse/ServiceClass
- def event_service
- EventCreateService.new
- end
- # rubocop: enable CodeReuse/ServiceClass
end
ProjectMember.prepend_mod_with('ProjectMember')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f9af342f47f..ae68a36c8d2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1716,8 +1716,6 @@ class MergeRequest < ApplicationRecord
actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test))
end
- # rubocop: disable Metrics/AbcSize
- # Delete a rubocop annotation once FF truncate_ci_merge_request_description is cleaned up
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
@@ -1730,14 +1728,9 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED', value: ProtectedBranch.protected?(target_project, target_branch).to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
- if ::Feature.enabled?(:truncate_ci_merge_request_description)
- mr_description, mr_description_truncated = truncate_mr_description
- variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION', value: mr_description)
- variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED', value: mr_description_truncated)
- else
- variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION', value: description)
- end
-
+ mr_description, mr_description_truncated = truncate_mr_description
+ variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION', value: mr_description)
+ variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED', value: mr_description_truncated)
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
@@ -1745,8 +1738,6 @@ class MergeRequest < ApplicationRecord
variables.concat(source_project_variables)
end
end
- # rubocop: enable Metrics/AbcSize
- # Delete a rubocop annotation once FF truncate_ci_merge_request_description is cleaned up
def compare_test_reports
unless has_test_reports?
@@ -2102,8 +2093,12 @@ class MergeRequest < ApplicationRecord
true
end
+ def allows_multiple_assignees?
+ project.allows_multiple_merge_request_assignees?
+ end
+
def allows_multiple_reviewers?
- false
+ project.allows_multiple_merge_request_reviewers?
end
def supports_assignee?
@@ -2198,6 +2193,8 @@ class MergeRequest < ApplicationRecord
attr_accessor :skip_fetch_ref
def merge_base_pipelines
+ return ::Ci::Pipeline.none unless actual_head_pipeline&.target_sha
+
target_branch_pipelines_for(sha: actual_head_pipeline.target_sha)
end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 3c592c0008f..6d6c0ee07af 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class MergeRequest::Metrics < ApplicationRecord
- include DatabaseEventTracking
-
belongs_to :merge_request, inverse_of: :metrics
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
belongs_to :latest_closed_by, class_name: 'User'
@@ -33,8 +31,7 @@ class MergeRequest::Metrics < ApplicationRecord
RETURNING id, #{inserted_columns.join(', ')}
SQL
- result = connection.execute(sql).first
- new(result).publish_database_create_event
+ connection.execute(sql)
end
end
@@ -48,31 +45,6 @@ class MergeRequest::Metrics < ApplicationRecord
with_valid_time_to_merge
.pick(time_to_merge_expression)
end
-
- SNOWPLOW_ATTRIBUTES = %i[
- id
- merge_request_id
- latest_build_started_at
- latest_build_finished_at
- first_deployed_to_production_at
- merged_at
- created_at
- updated_at
- pipeline_id
- merged_by_id
- latest_closed_by_id
- latest_closed_at
- first_comment_at
- first_commit_at
- last_commit_at
- diff_size
- modified_paths_size
- commits_count
- first_approved_at
- first_reassigned_at
- added_lines
- removed_lines
- ].freeze
end
MergeRequest::Metrics.prepend_mod_with('MergeRequest::Metrics')
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 0b183131a47..47102418152 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -196,6 +196,7 @@ class MergeRequestDiff < ApplicationRecord
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
after_create_commit :set_as_latest_diff, unless: :importing?
+ after_create_commit :trigger_diff_generated_subscription, unless: :importing?
after_save :update_external_diff_store
after_save :set_count_columns
@@ -258,6 +259,12 @@ class MergeRequestDiff < ApplicationRecord
.update_all(latest_merge_request_diff_id: self.id)
end
+ def trigger_diff_generated_subscription
+ return unless Feature.enabled?(:merge_request_diff_generated_subscription, merge_request.project)
+
+ GraphqlTriggers.merge_request_diff_generated(merge_request)
+ end
+
def ensure_commit_shas
self.start_commit_sha ||= merge_request.target_branch_sha
@@ -439,6 +446,8 @@ class MergeRequestDiff < ApplicationRecord
)
end
+ diff_options[:generated_files] = comparison.generated_files if diff_options[:collapse_generated]
+
Gitlab::Metrics.measure(:diffs_comparison) do
comparison.diffs(diff_options)
end
@@ -452,18 +461,25 @@ class MergeRequestDiff < ApplicationRecord
fetching_repository_diffs({}) do |comparison|
reorder_diff_files!
+ collapse_generated = Feature.enabled?(:collapse_generated_diff_files, project)
+ diff_options = { collapse_generated: collapse_generated }
+
collection = Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff.new(
self,
page,
- per_page
+ per_page,
+ diff_options
)
if comparison
+ diff_options[:generated_files] = comparison.generated_files if collapse_generated
+
comparison.diffs(
- paths: collection.diff_paths,
- page: collection.current_page,
- per_page: collection.limit_value,
- count: collection.total_count
+ diff_options.merge(
+ paths: collection.diff_paths,
+ page: collection.current_page,
+ per_page: collection.limit_value,
+ count: collection.total_count)
)
else
collection
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index ad6c6b7b3bf..456c23df0e0 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -3,6 +3,7 @@
module Ml
class Experiment < ApplicationRecord
include AtomicInternalId
+ include Sortable
PACKAGE_PREFIX = 'ml_experiment_'
@@ -15,6 +16,8 @@ module Ml
has_many :candidates, class_name: 'Ml::Candidate'
has_many :metadata, class_name: 'Ml::ExperimentMetadata'
+ scope :including_project, -> { includes(:project) }
+ scope :by_project, ->(project) { where(project: project) }
scope :with_candidate_count, -> {
left_outer_joins(:candidates)
.select("ml_experiments.*, count(ml_candidates.id) as candidate_count")
diff --git a/app/models/ml/model_metadata.rb b/app/models/ml/model_metadata.rb
index 9c4273c629c..9695621e47d 100644
--- a/app/models/ml/model_metadata.rb
+++ b/app/models/ml/model_metadata.rb
@@ -3,7 +3,7 @@
module Ml
class ModelMetadata < ApplicationRecord
validates :name,
- length: { maximum: 250 },
+ length: { maximum: 255 },
presence: true,
uniqueness: { scope: :model, message: ->(metadata, _) { "'#{metadata.name}' already taken" } }
validates :value, length: { maximum: 5000 }, presence: true
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index 58da57f27d6..1b3313c803a 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -21,12 +21,25 @@ module Ml
belongs_to :project
belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true
has_one :candidate, class_name: 'Ml::Candidate'
+ has_many :metadata, class_name: 'Ml::ModelVersionMetadata'
delegate :name, to: :model
scope :order_by_model_id_id_desc, -> { order('model_id, id DESC') }
scope :latest_by_model, -> { order_by_model_id_id_desc.select('DISTINCT ON (model_id) *') }
+ def add_metadata(metadata_key_value)
+ return unless metadata_key_value.present?
+
+ metadata_key_value.each do |entry|
+ metadata.create!(
+ project_id: project_id,
+ name: entry[:key],
+ value: entry[:value]
+ )
+ end
+ end
+
class << self
def find_or_create!(model, version, package, description)
create_with(package: package, description: description)
diff --git a/app/models/ml/model_version_metadata.rb b/app/models/ml/model_version_metadata.rb
new file mode 100644
index 00000000000..61810786091
--- /dev/null
+++ b/app/models/ml/model_version_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelVersionMetadata < ApplicationRecord
+ validates :name,
+ length: { maximum: 255 },
+ presence: true,
+ uniqueness: { scope: :model_version, message: ->(metadata, _) { "'#{metadata.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :project, optional: false
+ belongs_to :model_version, class_name: 'Ml::ModelVersion', optional: false
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index c665c2278a5..238556f0cf0 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -12,6 +12,7 @@ class Namespace < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
+ include Namespaces::Traversal::Cached
include EachBatch
include BlocksUnsafeSerialization
include Ci::NamespaceSettings
@@ -45,6 +46,7 @@ class Namespace < ApplicationRecord
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :non_archived_projects, -> { where.not(archived: true) }, class_name: 'Project'
has_many :project_statistics
has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true
has_one :ci_cd_settings, inverse_of: :namespace, class_name: 'NamespaceCiCdSetting', autosave: true
@@ -55,6 +57,9 @@ class Namespace < ApplicationRecord
has_one :namespace_ldap_settings, inverse_of: :namespace, class_name: 'Namespaces::LdapSetting', autosave: true
+ has_one :namespace_descendants, class_name: 'Namespaces::Descendants'
+ accepts_nested_attributes_for :namespace_descendants, allow_destroy: true
+
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
has_many :pending_builds, class_name: 'Ci::PendingBuild'
@@ -263,6 +268,28 @@ class Namespace < ApplicationRecord
end
end
+ # This should be kept in sync with the frontend filtering in
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/5d34e3488faa3982d30d7207773991c1e0b6368a/app/assets/javascripts/gfm_auto_complete.js#L68 and
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/5d34e3488faa3982d30d7207773991c1e0b6368a/app/assets/javascripts/gfm_auto_complete.js#L1053
+ def gfm_autocomplete_search(query)
+ without_project_namespaces
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+ .joins(:route)
+ .where(
+ "REPLACE(routes.name, ' ', '') ILIKE :pattern OR routes.path ILIKE :pattern",
+ pattern: "%#{sanitize_sql_like(query)}%"
+ )
+ .order(
+ Arel.sql(sanitize_sql(
+ [
+ "CASE WHEN starts_with(REPLACE(routes.name, ' ', ''), :pattern) OR starts_with(routes.path, :pattern) THEN 1 ELSE 2 END",
+ { pattern: query }
+ ]
+ )),
+ 'routes.path'
+ )
+ end
+
def clean_path(path, limited_to: Namespace.all)
slug = Gitlab::Slug::Path.new(path).generate
path = Namespaces::RandomizedSuffixPath.new(slug)
diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb
index a5a393ad8a2..5f5bef4409c 100644
--- a/app/models/namespace/package_setting.rb
+++ b/app/models/namespace/package_setting.rb
@@ -12,7 +12,7 @@ class Namespace::PackageSetting < ApplicationRecord
PackageSettingNotImplemented = Class.new(StandardError)
- PACKAGES_WITH_SETTINGS = %w[maven generic nuget].freeze
+ PACKAGES_WITH_SETTINGS = %w[maven generic nuget terraform_module].freeze
belongs_to :namespace, inverse_of: :package_setting_relation
@@ -24,6 +24,14 @@ class Namespace::PackageSetting < ApplicationRecord
validates :nuget_duplicates_allowed, inclusion: { in: [true, false] }
validates :nuget_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
validates :nuget_symbol_server_enabled, inclusion: { in: [true, false] }
+ validates :terraform_module_duplicates_allowed, inclusion: { in: [true, false] }
+ validates :terraform_module_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
+
+ scope :namespace_id_in, ->(namespace_ids) { where(namespace_id: namespace_ids) }
+ scope :with_terraform_module_duplicates_allowed_or_exception_regex, -> do
+ where(terraform_module_duplicates_allowed: true)
+ .or(where.not(terraform_module_duplicate_exception_regex: ''))
+ end
class << self
def duplicates_allowed?(package)
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 0263942116d..e61e5a7f37e 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -4,9 +4,13 @@ class NamespaceSetting < ApplicationRecord
include CascadingNamespaceSettingAttribute
include Sanitizable
include ChronicDurationAttribute
+ include IgnorableColumns
+
+ ignore_column :project_import_level, remove_with: '16.10', remove_after: '2024-02-22'
cascading_attr :delayed_project_removal
cascading_attr :toggle_security_policy_custom_ci
+ cascading_attr :toggle_security_policies_policy_scope
belongs_to :namespace, inverse_of: :namespace_settings
diff --git a/app/models/namespaces/descendants.rb b/app/models/namespaces/descendants.rb
new file mode 100644
index 00000000000..8444cea9848
--- /dev/null
+++ b/app/models/namespaces/descendants.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class Descendants < ApplicationRecord
+ self.table_name = :namespace_descendants
+
+ belongs_to :namespace
+
+ validates :namespace_id, uniqueness: true
+
+ def self.expire_for(namespace_ids)
+ # Union:
+ # - Look up all parent ids including the given ids via traversal_ids
+ # - Include the given ids to handle the case when the namespaces records are already deleted
+ sql = <<~SQL
+ WITH namespace_ids AS MATERIALIZED (
+ (
+ SELECT ids.id
+ FROM namespaces, UNNEST(traversal_ids) ids(id)
+ WHERE namespaces.id IN (?)
+ ) UNION
+ (SELECT UNNEST(ARRAY[?]) AS id)
+ )
+ UPDATE namespace_descendants SET outdated_at = ? FROM namespace_ids WHERE namespace_descendants.namespace_id = namespace_ids.id
+ SQL
+
+ connection.execute(sanitize_sql_array([sql, namespace_ids, namespace_ids, Time.current]))
+ end
+ end
+end
diff --git a/app/models/namespaces/traversal/cached.rb b/app/models/namespaces/traversal/cached.rb
new file mode 100644
index 00000000000..55eaaa4667e
--- /dev/null
+++ b/app/models/namespaces/traversal/cached.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module Cached
+ extend ActiveSupport::Concern
+ extend Gitlab::Utils::Override
+
+ included do
+ after_destroy :invalidate_descendants_cache
+ end
+
+ private
+
+ override :sync_traversal_ids
+ def sync_traversal_ids
+ super
+ return if is_a?(Namespaces::UserNamespace)
+ return unless Feature.enabled?(:namespace_descendants_cache_expiration, self, type: :gitlab_com_derisk)
+
+ ids = [id]
+ ids.concat((saved_changes[:parent_id] - [parent_id]).compact) if saved_changes[:parent_id]
+ Namespaces::Descendants.expire_for(ids)
+ end
+
+ def invalidate_descendants_cache
+ return if is_a?(Namespaces::UserNamespace)
+ return unless Feature.enabled?(:namespace_descendants_cache_expiration, self, type: :gitlab_com_derisk)
+
+ Namespaces::Descendants.expire_for([parent_id, id].compact)
+ end
+ end
+ end
+end
diff --git a/app/models/onboarding/completion.rb b/app/models/onboarding/completion.rb
index afbd671f82e..53781e112ae 100644
--- a/app/models/onboarding/completion.rb
+++ b/app/models/onboarding/completion.rb
@@ -3,7 +3,6 @@
module Onboarding
class Completion
include Gitlab::Utils::StrongMemoize
- include Gitlab::Experiment::Dsl
ACTION_PATHS = [
:pipeline_created,
@@ -12,6 +11,7 @@ module Onboarding
:code_owners_enabled,
:issue_created,
:git_write,
+ :code_added,
:merge_request_created,
:user_added,
:license_scanning_run,
@@ -35,20 +35,11 @@ module Onboarding
end
def completed?(column)
- if column == :code_added
- repository.commit_count > 1 || repository.branch_count > 1
- else
- attributes[column].present?
- end
+ attributes[column].present?
end
private
- def repository
- project.repository
- end
- strong_memoize_attr :repository
-
def attributes
onboarding_progress.attributes.symbolize_keys
end
@@ -60,8 +51,7 @@ module Onboarding
strong_memoize_attr :onboarding_progress
def action_columns
- [:code_added] +
- ACTION_PATHS.map { |action_key| ::Onboarding::Progress.column_name(action_key) }
+ ACTION_PATHS.map { |action_key| ::Onboarding::Progress.column_name(action_key) }
end
strong_memoize_attr :action_columns
diff --git a/app/models/onboarding/progress.rb b/app/models/onboarding/progress.rb
index 83030732c6a..b6628843821 100644
--- a/app/models/onboarding/progress.rb
+++ b/app/models/onboarding/progress.rb
@@ -32,7 +32,8 @@ module Onboarding
:secure_api_fuzzing_run,
:secure_cluster_image_scanning_run,
:license_scanning_run,
- :promote_ultimate_features
+ :promote_ultimate_features,
+ :code_added
].freeze
scope :incomplete_actions, ->(actions) do
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 764378a5d19..df6f0109d57 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Organizations
- class Organization < ApplicationRecord
+ class Organization < MainClusterwide::ApplicationRecord
DEFAULT_ORGANIZATION_ID = 1
scope :without_default, -> { where.not(id: DEFAULT_ORGANIZATION_ID) }
@@ -16,6 +16,8 @@ module Organizations
has_one :organization_detail, inverse_of: :organization, autosave: true
has_many :organization_users, inverse_of: :organization
+ # if considering disable_joins on the below see:
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140343#note_1705047949
has_many :users, through: :organization_users, inverse_of: :organizations
validates :name,
@@ -28,7 +30,7 @@ module Organizations
'organizations/path': true,
length: { minimum: 2, maximum: 255 }
- delegate :description, :avatar, :avatar_url, to: :organization_detail
+ delegate :description, :description_html, :avatar, :avatar_url, :remove_avatar!, to: :organization_detail
accepts_nested_attributes_for :organization_detail
@@ -52,6 +54,10 @@ module Organizations
organization_users.exists?(user: user)
end
+ def owner?(user)
+ organization_users.owners.exists?(user: user)
+ end
+
def web_url(only_path: nil)
Gitlab::UrlBuilder.build(self, only_path: only_path)
end
diff --git a/app/models/organizations/organization_detail.rb b/app/models/organizations/organization_detail.rb
index b69ec5eae76..018e7579c5b 100644
--- a/app/models/organizations/organization_detail.rb
+++ b/app/models/organizations/organization_detail.rb
@@ -6,7 +6,7 @@ module Organizations
include Avatarable
include WithUploads
- cache_markdown_field :description
+ cache_markdown_field :description, pipeline: :description
belongs_to :organization, inverse_of: :organization_detail
diff --git a/app/models/organizations/organization_user.rb b/app/models/organizations/organization_user.rb
index 5aa1133b017..9e06870dcc6 100644
--- a/app/models/organizations/organization_user.rb
+++ b/app/models/organizations/organization_user.rb
@@ -4,5 +4,17 @@ module Organizations
class OrganizationUser < ApplicationRecord
belongs_to :organization, inverse_of: :organization_users, optional: false
belongs_to :user, inverse_of: :organization_users, optional: false
+
+ validates :user, uniqueness: { scope: :organization_id }
+ validates :access_level, presence: true
+
+ enum access_level: {
+ # Until we develop more access_levels, we really don't know if the default access_level will be what we think of
+ # as a guest. For now, we'll set to same value as guest, but call it default to denote the current ambivalence.
+ default: Gitlab::Access::GUEST,
+ owner: Gitlab::Access::OWNER
+ }
+
+ scope :owners, -> { where(access_level: Gitlab::Access::OWNER) }
end
end
diff --git a/app/models/pages/project_settings.rb b/app/models/pages/project_settings.rb
new file mode 100644
index 00000000000..96e5bb8e98e
--- /dev/null
+++ b/app/models/pages/project_settings.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Pages
+ class ProjectSettings
+ def initialize(project)
+ @project = project
+ end
+
+ def url = url_builder.pages_url(with_unique_domain: true)
+
+ def deployments = project.pages_deployments.active
+
+ def unique_domain_enabled? = project.project_setting.pages_unique_domain_enabled?
+
+ def force_https? = project.pages_https_only?
+
+ private
+
+ attr_reader :project
+
+ def url_builder
+ @url_builder ||= ::Gitlab::Pages::UrlBuilder.new(project)
+ end
+ end
+end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index e8b186234af..a360b705805 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -68,6 +68,14 @@ class PagesDeployment < ApplicationRecord
update(deleted_at: Time.now.utc)
end
+ def url
+ base_url = ::Gitlab::Pages::UrlBuilder
+ .new(project)
+ .pages_url(with_unique_domain: true)
+
+ File.join(base_url.to_s, path_prefix.to_s)
+ end
+
private
def set_size
diff --git a/app/models/project.rb b/app/models/project.rb
index 7b996457c0d..8f82a947ba6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -208,6 +208,7 @@ class Project < ApplicationRecord
has_one :custom_issue_tracker_integration, class_name: 'Integrations::CustomIssueTracker'
has_one :datadog_integration, class_name: 'Integrations::Datadog'
has_one :container_registry_data_repair_detail, class_name: 'ContainerRegistry::DataRepairDetail'
+ has_one :diffblue_cover_integration, class_name: 'Integrations::DiffblueCover'
has_one :discord_integration, class_name: 'Integrations::Discord'
has_one :drone_ci_integration, class_name: 'Integrations::DroneCi'
has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush'
@@ -334,7 +335,7 @@ class Project < ApplicationRecord
has_many :authorized_users, -> { allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') },
through: :project_authorizations, source: :user, class_name: 'User'
- has_many :project_members, -> { where(requested_at: nil) },
+ has_many :project_members, -> { non_request },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
has_many :namespace_members, ->(project) { where(requested_at: nil).unscope(where: %i[source_id source_type]) },
@@ -508,6 +509,7 @@ class Project < ApplicationRecord
delegate :members, prefix: true
delegate :add_member, :add_members, :member?
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role
+ delegate :has_user?
end
with_options to: :namespace do
@@ -749,6 +751,7 @@ class Project < ApplicationRecord
preload(:project_feature, :route, namespace: [:route, :owner])
}
+ scope :with_name, -> (name) { where(name: name) }
scope :created_by, -> (user) { where(creator: user) }
scope :imported_from, -> (type) { where(import_type: type) }
scope :imported, -> { where.not(import_type: nil) }
@@ -3205,6 +3208,21 @@ class Project < ApplicationRecord
end
strong_memoize_attr :code_suggestions_enabled?
+ # Overridden in EE
+ def allows_multiple_merge_request_assignees?
+ false
+ end
+
+ # Overridden in EE
+ def allows_multiple_merge_request_reviewers?
+ false
+ end
+
+ # Overridden in EE
+ def on_demand_dast_available?
+ false
+ end
+
private
# overridden in EE
@@ -3226,8 +3244,11 @@ class Project < ApplicationRecord
if @topic_list != self.topic_list
self.topics.delete_all
- self.topics = @topic_list.map do |topic|
- Projects::Topic.where('lower(name) = ?', topic.downcase).order(total_projects_count: :desc).first_or_create(name: topic, title: topic)
+ self.topics = @topic_list.map do |topic_name|
+ Projects::Topic
+ .where('lower(name) = ?', topic_name.downcase)
+ .order(total_projects_count: :desc)
+ .first_or_create(name: topic_name, title: topic_name, slug: Gitlab::Slug::Path.new(topic_name).generate)
end
end
@@ -3438,7 +3459,7 @@ class Project < ApplicationRecord
def check_project_export_limit!
return if Gitlab::CurrentSettings.current_application_settings.max_export_size == 0
- if self.statistics.storage_size > Gitlab::CurrentSettings.current_application_settings.max_export_size.megabytes
+ if self.statistics.export_size > Gitlab::CurrentSettings.current_application_settings.max_export_size.megabytes
raise ExportLimitExceeded, _('The project size exceeds the export limit.')
end
end
diff --git a/app/models/project_authorizations/changes.rb b/app/models/project_authorizations/changes.rb
index ac52bdfdb07..26f5366ad5e 100644
--- a/app/models/project_authorizations/changes.rb
+++ b/app/models/project_authorizations/changes.rb
@@ -21,6 +21,7 @@ module ProjectAuthorizations
@authorizations_to_add = []
@affected_project_ids = Set.new
@removed_user_ids = Set.new
+ @added_user_ids = Set.new
yield self
end
@@ -61,6 +62,7 @@ module ProjectAuthorizations
def add_authorizations
insert_all_in_batches(authorizations_to_add)
@affected_project_ids += authorizations_to_add.pluck(:project_id)
+ @added_user_ids += authorizations_to_add.pluck(:user_id)
end
def delete_authorizations_for_user
@@ -139,23 +141,51 @@ module ProjectAuthorizations
end
def publish_events
+ publish_changed_event
+ publish_removed_event
+ publish_added_event
+ end
+
+ def publish_changed_event
+ # This event is used to add policy approvers to approval rules by re-syncing all project policies which is costly.
+ # If the feature flag below is enabled, the policies won't be re-synced and
+ # the approvers will be added via `AuthorizationsAddedEvent`.
+ return if ::Feature.enabled?(:add_policy_approvers_to_rules)
+
@affected_project_ids.each do |project_id|
::Gitlab::EventStore.publish(
::ProjectAuthorizations::AuthorizationsChangedEvent.new(data: { project_id: project_id })
)
end
- return if ::Feature.disabled?(:user_approval_rules_removal) || @removed_user_ids.blank?
+ end
- @affected_project_ids.each do |project_id|
- @removed_user_ids.to_a.each_slice(EVENT_USER_BATCH_SIZE).each do |user_ids_batch|
- ::Gitlab::EventStore.publish(
- ::ProjectAuthorizations::AuthorizationsRemovedEvent.new(data: {
- project_id: project_id,
- user_ids: user_ids_batch
- })
- )
+ def publish_removed_event
+ return if @removed_user_ids.none?
+
+ events = @affected_project_ids.flat_map do |project_id|
+ @removed_user_ids.to_a.each_slice(EVENT_USER_BATCH_SIZE).map do |user_ids_batch|
+ ::ProjectAuthorizations::AuthorizationsRemovedEvent.new(data: {
+ project_id: project_id,
+ user_ids: user_ids_batch
+ })
+ end
+ end
+ ::Gitlab::EventStore.publish_group(events)
+ end
+
+ def publish_added_event
+ return if ::Feature.disabled?(:add_policy_approvers_to_rules)
+ return if @added_user_ids.none?
+
+ events = @affected_project_ids.flat_map do |project_id|
+ @added_user_ids.to_a.each_slice(EVENT_USER_BATCH_SIZE).map do |user_ids_batch|
+ ::ProjectAuthorizations::AuthorizationsAddedEvent.new(data: {
+ project_id: project_id,
+ user_ids: user_ids_batch
+ })
end
end
+ ::Gitlab::EventStore.publish_group(events)
end
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 942f20f6e5e..f89894b77a8 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -145,6 +145,11 @@ class ProjectStatistics < ApplicationRecord
bulk_increment_counter(key, increments)
end
+ # Build artifacts & packages are not included in the project export
+ def export_size
+ storage_size - build_artifacts_size - packages_size
+ end
+
private
def incrementable_attribute?(key)
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 5078642ea3a..3af9f946243 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -172,6 +172,13 @@ class ProjectTeam
max_member_access(user.id) >= min_access_level
end
+ # Only for direct and not invited members
+ def has_user?(user)
+ return false unless user
+
+ project.project_members.non_invite.exists?(user: user)
+ end
+
def human_max_access(user_id)
Gitlab::Access.human_access(max_member_access(user_id))
end
diff --git a/app/models/projects/project_topic.rb b/app/models/projects/project_topic.rb
index 7021a48646a..7833c1ebf24 100644
--- a/app/models/projects/project_topic.rb
+++ b/app/models/projects/project_topic.rb
@@ -4,5 +4,7 @@ module Projects
class ProjectTopic < ApplicationRecord
belongs_to :project
belongs_to :topic, counter_cache: :total_projects_count
+
+ validates :topic_id, uniqueness: { scope: [:project_id] }
end
end
diff --git a/app/models/projects/repository_storage_move.rb b/app/models/projects/repository_storage_move.rb
index ae815bf366d..95fd78e8941 100644
--- a/app/models/projects/repository_storage_move.rb
+++ b/app/models/projects/repository_storage_move.rb
@@ -17,11 +17,7 @@ module Projects
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
- Projects::UpdateRepositoryStorageWorker.perform_async(
- project_id,
- destination_storage_name,
- id
- )
+ Projects::UpdateRepositoryStorageWorker.perform_async(id)
end
private
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index 347d65841ed..a3622150351 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -7,9 +7,18 @@ module Projects
include Avatarable
include Gitlab::SQL::Pattern
+ SLUG_ALLOWED_REGEX = %r{\A[a-zA-Z0-9_\-.]+\z}
+
validates :name, presence: true, length: { maximum: 255 }
validates :name, uniqueness: { case_sensitive: false }, if: :name_changed?
validate :validate_name_format, if: :name_changed?
+
+ validates :slug,
+ length: { maximum: 255 },
+ uniqueness: { case_sensitive: false },
+ format: { with: SLUG_ALLOWED_REGEX, message: "can contain only letters, digits, '_', '-', '.'" },
+ if: :slug_changed?
+
validates :title, presence: true, length: { maximum: 255 }, on: :create
validates :description, length: { maximum: 1024 }
diff --git a/app/models/release.rb b/app/models/release.rb
index 1cd623e1254..7bacc69f038 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -54,6 +54,7 @@ class Release < ApplicationRecord
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) }
scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) }
+ scope :unpublished, -> { where(release_published_at: nil) }
scope :for_projects, ->(projects) { where(project_id: projects) }
scope :by_tag, ->(tag) { where(tag: tag) }
@@ -66,6 +67,7 @@ class Release < ApplicationRecord
delegate :repository, to: :project
MAX_NUMBER_TO_DISPLAY = 3
+ MAX_NUMBER_TO_PUBLISH = 5000
class << self
# In the future, we should support `order_by=semver`;
@@ -97,6 +99,10 @@ class Release < ApplicationRecord
.from("(VALUES #{project_ids_list}) projects (id)")
.joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{Release.table_name} ON TRUE")
end
+
+ def waiting_for_publish_event
+ unpublished.released_within_2hrs.joins(:project).merge(Project.with_feature_enabled(:releases)).limit(MAX_NUMBER_TO_PUBLISH)
+ end
end
def to_param
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index ad1ce740c89..e912e57f39e 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -45,7 +45,7 @@ class ResourceLabelEvent < ResourceEvent
end
def group
- issuable.group if issuable.respond_to?(:group)
+ issuable.resource_parent if issuable.resource_parent.is_a?(Group)
end
def outdated_markdown?
@@ -93,7 +93,9 @@ class ResourceLabelEvent < ResourceEvent
end
def label_url_method
- issuable.is_a?(MergeRequest) ? :project_merge_requests_url : :project_issues_url
+ return :project_merge_requests_url if issuable.is_a?(MergeRequest)
+
+ issuable.project_id.nil? ? :group_work_items_url : :project_issues_url
end
def broadcast_notes_changed
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index d305a4ace51..2b93334f721 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ResourceMilestoneEvent < ResourceTimeboxEvent
+ include EachBatch
+
belongs_to :milestone
scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
diff --git a/app/models/route.rb b/app/models/route.rb
index 652c33a673c..1fa0005ffb4 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -3,6 +3,7 @@
class Route < MainClusterwide::ApplicationRecord
include CaseSensitivity
include Gitlab::SQL::Pattern
+ include EachBatch
belongs_to :source, polymorphic: true, inverse_of: :route # rubocop:disable Cop/PolymorphicAssociations
belongs_to :namespace, inverse_of: :namespace_route
@@ -26,30 +27,39 @@ class Route < MainClusterwide::ApplicationRecord
def rename_descendants
return unless saved_change_to_path? || saved_change_to_name?
- descendant_routes = self.class.inside_path(path_before_last_save)
+ if Feature.disabled?(:batch_route_updates, Feature.current_request, type: :gitlab_com_derisk)
+ descendant_routes = self.class.inside_path(path_before_last_save)
- descendant_routes.each do |route|
- attributes = {}
+ descendant_routes.each do |route|
+ attributes = {}
- if saved_change_to_path? && route.path.present?
- attributes[:path] = route.path.sub(path_before_last_save, path)
- end
+ if saved_change_to_path? && route.path.present?
+ attributes[:path] = route.path.sub(path_before_last_save, path)
+ end
- if saved_change_to_name? && name_before_last_save.present? && route.name.present?
- attributes[:name] = route.name.sub(name_before_last_save, name)
- end
+ if saved_change_to_name? && name_before_last_save.present? && route.name.present?
+ attributes[:name] = route.name.sub(name_before_last_save, name)
+ end
- next if attributes.empty?
+ next if attributes.empty?
- old_path = route.path
+ old_path = route.path
- # Callbacks must be run manually
- route.update_columns(attributes.merge(updated_at: Time.current))
+ # Callbacks must be run manually
+ route.update_columns(attributes.merge(updated_at: Time.current))
+
+ # We are not calling route.delete_conflicting_redirects here, in hopes
+ # of avoiding deadlocks. The parent (self, in this method) already
+ # called it, which deletes conflicts for all descendants.
+ route.create_redirect(old_path) if attributes[:path]
+ end
+ else
+ changes = {
+ path: { saved: saved_change_to_path?, old_value: path_before_last_save },
+ name: { saved: saved_change_to_name?, old_value: name_before_last_save }
+ }
- # We are not calling route.delete_conflicting_redirects here, in hopes
- # of avoiding deadlocks. The parent (self, in this method) already
- # called it, which deletes conflicts for all descendants.
- route.create_redirect(old_path) if attributes[:path]
+ Routes::RenameDescendantsService.new(self).execute(changes) # rubocop: disable CodeReuse/ServiceClass -- Need a service class to encapsulate all the logic.
end
end
diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb
index 7ae44ac6aa1..6955f178bea 100644
--- a/app/models/service_desk/custom_email_credential.rb
+++ b/app/models/service_desk/custom_email_credential.rb
@@ -61,12 +61,13 @@ module ServiceDesk
def validate_smtp_address
# Addressable::URI always needs a scheme otherwise it interprets the host as the path
- Gitlab::UrlBlocker.validate!("smtp://#{smtp_address}",
+ Gitlab::HTTP_V2::UrlBlocker.validate!("smtp://#{smtp_address}",
schemes: %w[smtp],
ascii_only: true,
enforce_sanitization: true,
allow_localhost: false,
- allow_local_network: !::Gitlab.com? # rubocop:disable Gitlab/AvoidGitlabInstanceChecks -- self-managed may also use local network
+ allow_local_network: !::Gitlab.com?, # rubocop:disable Gitlab/AvoidGitlabInstanceChecks -- self-managed may also use local network
+ deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?
)
rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
errors.add(:smtp_address, e)
diff --git a/app/models/snippets/repository_storage_move.rb b/app/models/snippets/repository_storage_move.rb
index 9db25ef4fc5..794caefb77d 100644
--- a/app/models/snippets/repository_storage_move.rb
+++ b/app/models/snippets/repository_storage_move.rb
@@ -16,11 +16,7 @@ module Snippets
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
- Snippets::UpdateRepositoryStorageWorker.perform_async(
- snippet_id,
- destination_storage_name,
- id
- )
+ Snippets::UpdateRepositoryStorageWorker.perform_async(id)
end
private
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index 672a6d64127..f0855fc9f1c 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -137,12 +137,13 @@ class SshHostKey
end
def normalize_url(url)
- url, real_hostname = Gitlab::UrlBlocker.validate!(
+ url, real_hostname = Gitlab::HTTP_V2::UrlBlocker.validate!(
url,
schemes: %w[ssh],
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- dns_rebind_protection: Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
+ dns_rebind_protection: Gitlab::CurrentSettings.dns_rebinding_protection_enabled?,
+ deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?
)
# When DNS rebinding protection is required, the hostname is replaced by the
diff --git a/app/models/time_tracking/timelog_category.rb b/app/models/time_tracking/timelog_category.rb
index 67565039acd..295304f6e99 100644
--- a/app/models/time_tracking/timelog_category.rb
+++ b/app/models/time_tracking/timelog_category.rb
@@ -9,6 +9,8 @@ module TimeTracking
belongs_to :namespace, foreign_key: 'namespace_id'
+ has_many :timelogs
+
strip_attributes! :name
validates :namespace, presence: true
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index 0ae7790eef9..ffb88b7ebea 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -20,6 +20,7 @@ class Timelog < ApplicationRecord
belongs_to :project
belongs_to :user
belongs_to :note
+ belongs_to :timelog_category, optional: true, class_name: 'TimeTracking::TimelogCategory'
scope :in_group, -> (group) do
joins(:project).where(projects: { namespace: group.self_and_descendants })
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 030e7d9e85f..d62e5c1b368 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -18,8 +18,15 @@ class Tree
ref = ExtractsRef::RefExtractor.qualify_ref(@sha, ref_type)
- @entries, @cursor = Gitlab::Git::Tree.where(git_repo, ref, @path, recursive, skip_flat_paths, rescue_not_found,
- pagination_params)
+ @entries, @cursor = Gitlab::Git::Tree.tree_entries(
+ repository: git_repo,
+ sha: ref,
+ path: @path,
+ recursive: recursive,
+ skip_flat_paths: skip_flat_paths,
+ rescue_not_found: rescue_not_found,
+ pagination_params: pagination_params
+ )
@entries.each do |entry|
entry.ref_type = self.ref_type
diff --git a/app/models/user.rb b/app/models/user.rb
index c36898aaf70..c9873975cc9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -151,7 +151,7 @@ class User < MainClusterwide::ApplicationRecord
# Namespace for personal projects
has_one :namespace,
-> { where(type: Namespaces::UserNamespace.sti_name) },
- required: true,
+ required: false,
dependent: :destroy, # rubocop:disable Cop/ActiveRecordDependent
foreign_key: :owner_id,
inverse_of: :owner,
@@ -270,7 +270,8 @@ class User < MainClusterwide::ApplicationRecord
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user
- has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users
+ has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users,
+ disable_joins: true
has_one :status, class_name: 'UserStatus'
has_one :user_preference
@@ -284,8 +285,6 @@ class User < MainClusterwide::ApplicationRecord
has_many :reviews, foreign_key: :author_id, inverse_of: :author
- has_many :in_product_marketing_emails, class_name: '::Users::InProductMarketingEmail'
-
has_many :timelogs
has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
@@ -304,6 +303,10 @@ class User < MainClusterwide::ApplicationRecord
# Validations
#
# Note: devise :validatable above adds validations for :email and :password
+ validates :username,
+ presence: true,
+ exclusion: { in: Gitlab::PathRegex::TOP_LEVEL_ROUTES, message: N_('%{value} is a reserved name') }
+ validates :username, uniqueness: true, unless: :namespace
validates :name, presence: true, length: { maximum: 255 }
validates :first_name, length: { maximum: 127 }
validates :last_name, length: { maximum: 127 }
@@ -314,10 +317,9 @@ class User < MainClusterwide::ApplicationRecord
validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
- validates :username, presence: true
validate :check_password_weakness, if: :encrypted_password_changed?
- validates :namespace, presence: true
+ validates :namespace, presence: true, unless: :optional_namespace?
validate :namespace_move_dir_allowed, if: :username_changed?, unless: :new_record?
validate :unique_email, if: :email_changed?
@@ -591,6 +593,8 @@ class User < MainClusterwide::ApplicationRecord
scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) }
scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) }
scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first, arel_table[:id].desc) }
+ scope :ordered_by_id_desc, -> { reorder(arel_table[:id].desc) }
+
scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', Gitlab::CurrentSettings.deactivate_dormant_users_period.day.ago.to_date) }
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
@@ -847,6 +851,25 @@ class User < MainClusterwide::ApplicationRecord
scope.reorder(order)
end
+ # This should be kept in sync with the frontend filtering in
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/5d34e3488faa3982d30d7207773991c1e0b6368a/app/assets/javascripts/gfm_auto_complete.js#L68 and
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/5d34e3488faa3982d30d7207773991c1e0b6368a/app/assets/javascripts/gfm_auto_complete.js#L1053
+ def gfm_autocomplete_search(query)
+ where(
+ "REPLACE(users.name, ' ', '') ILIKE :pattern OR users.username ILIKE :pattern",
+ pattern: "%#{sanitize_sql_like(query)}%"
+ ).order(
+ Arel.sql(sanitize_sql(
+ [
+ "CASE WHEN starts_with(REPLACE(users.name, ' ', ''), :pattern) OR starts_with(users.username, :pattern) THEN 1 ELSE 2 END",
+ { pattern: query }
+ ]
+ )),
+ :username,
+ :id
+ )
+ end
+
# Limits the result set to users _not_ in the given query/list of IDs.
#
# users - The list of users to ignore. This can be an
@@ -1302,7 +1325,13 @@ class User < MainClusterwide::ApplicationRecord
end
def can_create_project?
- projects_limit_left > 0
+ projects_limit_left > 0 && allow_user_to_create_group_and_project?
+ end
+
+ def allow_user_to_create_group_and_project?
+ return true if Gitlab::CurrentSettings.allow_project_creation_for_guest_and_below
+
+ highest_role > Gitlab::Access::GUEST
end
def can_create_group?
@@ -1596,12 +1625,6 @@ class User < MainClusterwide::ApplicationRecord
if namespace
namespace.path = username if username_changed?
namespace.name = name if name_changed?
- elsif Feature.disabled?(:create_personal_ns_outside_model, Feature.current_request)
- # TODO: we should no longer need the `type` parameter once we can make the
- # the `has_one :namespace` association use the correct class.
- # issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070
- namespace = build_namespace(path: username, name: name, type: ::Namespaces::UserNamespace.sti_name)
- namespace.build_namespace_settings
end
end
@@ -1623,6 +1646,9 @@ class User < MainClusterwide::ApplicationRecord
self.errors.add(:base, :username_exists_as_a_different_namespace)
else
namespace_path_errors.each do |msg|
+ # Already handled by username validation.
+ next if msg.ends_with?('is a reserved name')
+
self.errors.add(:username, msg)
end
end
@@ -2300,6 +2326,10 @@ class User < MainClusterwide::ApplicationRecord
private
+ def optional_namespace?
+ Feature.enabled?(:optional_personal_namespace, self)
+ end
+
def block_or_ban
user_scores = Abuse::UserTrustScore.new(self)
if user_scores.spammer? && account_age_in_days < 7
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index c32414be312..8d330e4eb6e 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -79,7 +79,9 @@ module Users
vulnerability_report_grouping: 77, # EE-only
new_nav_for_everyone_callout: 78,
code_suggestions_ga_non_owner_alert: 79, # EE-only
- duo_chat_callout: 80 # EE-only
+ duo_chat_callout: 80, # EE-only
+ code_suggestions_ga_owner_alert: 81, # EE-only
+ product_analytics_dashboard_feedback: 82 # EE-only
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 6d0a22c8b0a..33e7ba72d5a 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -8,7 +8,7 @@ module Users
self.table_name = 'user_credit_card_validations'
- ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.8', remove_after: '2023-12-22'
+ ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.9', remove_after: '2024-01-22'
attr_accessor :last_digits, :network, :holder_name, :expiration_date
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
deleted file mode 100644
index 5362a726ff5..00000000000
--- a/app/models/users/in_product_marketing_email.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class InProductMarketingEmail < ApplicationRecord
- include BulkInsertSafe
-
- belongs_to :user
-
- validates :user, presence: true
- validates :track, presence: true
- validates :series, presence: true
-
- validates :user_id, uniqueness: {
- scope: [:track, :series],
- message: 'track series email has already been sent'
- }, if: -> { track.present? }
-
- enum track: {
- create: 0,
- verify: 1,
- trial: 2,
- team: 3,
- experience: 4,
- team_short: 5,
- trial_short: 6,
- admin_verify: 7,
- invite_team: 8
- }, _suffix: true
-
- # Tracks we don't send emails for (e.g. unsuccessful experiment). These
- # are kept since we already have DB records that use the enum value.
- INACTIVE_TRACK_NAMES = %w[invite_team experience].freeze
- ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES)
-
- scope :for_user_with_track_and_series, ->(user, track, series) do
- where(user: user, track: track, series: series)
- end
-
- scope :without_track_and_series, ->(track, series) do
- join_condition = for_user.and(for_track_and_series(track, series))
- users_without_records(join_condition)
- end
-
- def self.users_table
- User.arel_table
- end
-
- def self.distinct_users_sql
- name = users_table.name
- Arel.sql("DISTINCT ON(#{name}.id) #{name}.*")
- end
-
- def self.users_without_records(condition)
- arel_join = users_table.join(arel_table, Arel::Nodes::OuterJoin).on(condition)
- joins(arel_join.join_sources)
- .where(in_product_marketing_emails: { id: nil })
- .select(distinct_users_sql)
- end
-
- def self.for_user
- arel_table[:user_id].eq(users_table[:id])
- end
-
- def self.for_track_and_series(track, series)
- arel_table[:track].eq(ACTIVE_TRACKS[track])
- .and(arel_table[:series]).eq(series)
- end
-
- def self.save_cta_click(user, track, series)
- email = for_user_with_track_and_series(user, track, series).take
-
- email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
- end
- end
-end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index 072b75a1c90..ffb8d3a95a2 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -4,12 +4,17 @@ module Users
class PhoneNumberValidation < ApplicationRecord
include IgnorableColumns
+ # SMS send attempts subsequent to the first one will have wait times of 1
+ # min, 3 min, 5 min after each one respectively. Wait time between the fifth
+ # attempt and so on will be 10 minutes.
+ SMS_SEND_WAIT_TIMES = [1.minute, 3.minutes, 5.minutes, 10.minutes].freeze
+
self.primary_key = :user_id
self.table_name = 'user_phone_number_validations'
ignore_column :verification_attempts, remove_with: '16.7', remove_after: '2023-11-17'
- belongs_to :user, foreign_key: :user_id
+ belongs_to :user
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id
validates :country, presence: true, length: { maximum: 3 }
@@ -26,13 +31,24 @@ module Users
presence: true,
format: {
with: /\A\d+\Z/,
- message: -> (object, data) { _('can contain only digits') }
+ message: ->(_object, _data) { _('can contain only digits') }
},
length: { maximum: 12 }
validates :telesign_reference_xid, length: { maximum: 255 }
- scope :for_user, -> (user_id) { where(user_id: user_id) }
+ scope :for_user, ->(user_id) { where(user_id: user_id) }
+
+ scope :similar_to, ->(phone_number_validation) do
+ where(
+ international_dial_code: phone_number_validation.international_dial_code,
+ phone_number: phone_number_validation.phone_number
+ )
+ end
+
+ def similar_records
+ self.class.similar_to(self).includes(:user)
+ end
def self.related_to_banned_user?(international_dial_code, phone_number)
joins(:banned_user)
@@ -51,5 +67,18 @@ module Users
def validated?
validated_at.present?
end
+
+ def sms_send_allowed_after
+ return unless Feature.enabled?(:sms_send_wait_time, user)
+
+ # first send is allowed anytime
+ return if sms_send_count < 1
+ return unless sms_sent_at
+
+ max_wait_time = SMS_SEND_WAIT_TIMES.last
+ wait_time = SMS_SEND_WAIT_TIMES.fetch(sms_send_count - 1, max_wait_time)
+
+ sms_sent_at + wait_time
+ end
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 77f684e3578..f1d007e8167 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -5,7 +5,7 @@ class WorkItem < Issue
COMMON_QUICK_ACTIONS_COMMANDS = [
:title, :reopen, :close, :cc, :tableflip, :shrug, :type, :promote_to, :checkin_reminder,
- :subscribe, :unsubscribe, :confidential, :award
+ :subscribe, :unsubscribe, :confidential, :award, :react
].freeze
self.table_name = 'issues'
diff --git a/app/models/work_items/hierarchy_restriction.rb b/app/models/work_items/hierarchy_restriction.rb
index a253447a8db..f74f2f037b1 100644
--- a/app/models/work_items/hierarchy_restriction.rb
+++ b/app/models/work_items/hierarchy_restriction.rb
@@ -7,8 +7,17 @@ module WorkItems
belongs_to :parent_type, class_name: 'WorkItems::Type'
belongs_to :child_type, class_name: 'WorkItems::Type'
+ after_destroy :clear_parent_type_cache!
+ after_save :clear_parent_type_cache!
+
validates :parent_type, presence: true
validates :child_type, presence: true
validates :child_type, uniqueness: { scope: :parent_type_id }
+
+ private
+
+ def clear_parent_type_cache!
+ parent_type.clear_reactive_cache!
+ end
end
end
diff --git a/app/models/work_items/widget_definition.rb b/app/models/work_items/widget_definition.rb
index f25c951406f..2637a7c8185 100644
--- a/app/models/work_items/widget_definition.rb
+++ b/app/models/work_items/widget_definition.rb
@@ -32,7 +32,9 @@ module WorkItems
notifications: 14,
current_user_todos: 15,
award_emoji: 16,
- linked_items: 17
+ linked_items: 17,
+ color: 18, # EE-only
+ rolledup_dates: 19 # EE-only
}
def self.available_widgets
diff --git a/app/models/work_items/widgets/notes.rb b/app/models/work_items/widgets/notes.rb
index bde94ea8f43..67ee19f4947 100644
--- a/app/models/work_items/widgets/notes.rb
+++ b/app/models/work_items/widgets/notes.rb
@@ -4,8 +4,18 @@ module WorkItems
module Widgets
class Notes < Base
delegate :notes, to: :work_item
+ delegate :discussion_locked, to: :work_item
+
delegate_missing_to :work_item
+ def self.quick_action_commands
+ [:lock, :unlock]
+ end
+
+ def self.quick_action_params
+ [:discussion_locked]
+ end
+
def declarative_policy_delegate
work_item
end