diff options
Diffstat (limited to 'app/models')
163 files changed, 1179 insertions, 859 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 7dbc95c251b..b16c4a2b353 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -83,24 +83,26 @@ class ActiveSession is_impersonated: request.session[:impersonator_id].present? ) - redis.pipelined do |pipeline| - pipeline.setex( - key_name(user.id, session_private_id), - expiry, - active_user_session.dump - ) - - # Deprecated legacy format - temporary to support mixed deployments - pipeline.setex( - key_name_v1(user.id, session_private_id), - expiry, - Marshal.dump(active_user_session) - ) - - pipeline.sadd( - lookup_key_name(user.id), - session_private_id - ) + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + redis.pipelined do |pipeline| + pipeline.setex( + key_name(user.id, session_private_id), + expiry, + active_user_session.dump + ) + + # Deprecated legacy format - temporary to support mixed deployments + pipeline.setex( + key_name_v1(user.id, session_private_id), + expiry, + Marshal.dump(active_user_session) + ) + + pipeline.sadd?( + lookup_key_name(user.id), + session_private_id + ) + end end end end @@ -298,7 +300,7 @@ class ActiveSession session_ids_and_entries.each do |session_id, entry| next if entry - pipeline.srem(lookup_key, session_id) + pipeline.srem?(lookup_key, session_id) removed << session_id end end diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index 0c3b1679dc3..b2686924363 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -13,8 +13,7 @@ module AlertManagement key: Settings.attr_encrypted_db_key_base_32, algorithm: 'aes-256-gcm' - default_value_for(:endpoint_identifier, allows_nil: false) { SecureRandom.hex(8) } - default_value_for(:token) { generate_token } + attribute :endpoint_identifier, default: -> { SecureRandom.hex(8) } validates :project, presence: true validates :active, inclusion: { in: [true, false] } diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 00a95070691..bd948c2c32a 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -6,6 +6,16 @@ class Appearance < ApplicationRecord include ObjectStorage::BackgroundMove include WithUploads + attribute :title, default: '' + attribute :description, default: '' + attribute :new_project_guidelines, default: '' + attribute :profile_image_guidelines, default: '' + attribute :header_message, default: '' + attribute :footer_message, default: '' + attribute :message_background_color, default: '#E75E40' + attribute :message_font_color, default: '#FFFFFF' + attribute :email_header_and_footer_enabled, default: false + cache_markdown_field :description cache_markdown_field :new_project_guidelines cache_markdown_field :profile_image_guidelines @@ -20,16 +30,6 @@ class Appearance < ApplicationRecord validate :single_appearance_row, on: :create - default_value_for :title, '' - default_value_for :description, '' - default_value_for :new_project_guidelines, '' - default_value_for :profile_image_guidelines, '' - default_value_for :header_message, '' - default_value_for :footer_message, '' - default_value_for :message_background_color, '#E75E40' - default_value_for :message_font_color, '#FFFFFF' - default_value_for :email_header_and_footer_enabled, false - mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader mount_uploader :favicon, FaviconUploader diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 361b1a8dca9..adbbddd635c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -20,6 +20,7 @@ class ApplicationSetting < ApplicationRecord 'Admin Area > Settings > General > Kroki' enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true + enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 } add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required } add_authentication_token_field :health_check_access_token @@ -74,9 +75,9 @@ class ApplicationSetting < ApplicationRecord cache_markdown_field :shared_runners_text, pipeline: :plain_markdown cache_markdown_field :after_sign_up_text - default_value_for :id, 1 - default_value_for :repository_storages_weighted, {} - default_value_for :kroki_formats, {} + attribute :id, default: 1 + attribute :repository_storages_weighted, default: -> { {} } + attribute :kroki_formats, default: -> { {} } chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds @@ -317,6 +318,7 @@ class ApplicationSetting < ApplicationRecord less_than_or_equal_to: Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND } validates :user_default_internal_regex, js_regex: true, allow_nil: true + validates :default_preferred_language, presence: true, inclusion: { in: Gitlab::I18n.available_locales } validates :personal_access_token_prefix, format: { with: %r{\A[a-zA-Z0-9_+=/@:.-]+\z}, @@ -527,6 +529,11 @@ class ApplicationSetting < ApplicationRecord length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, allow_blank: true + validates :jira_connect_proxy_url, + length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, + allow_blank: true, + public_url: true + 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 @@ -632,10 +639,6 @@ class ApplicationSetting < ApplicationRecord validates :inactive_projects_send_warning_email_after_months, numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months } - validates :cube_api_base_url, - addressable_url: { allow_localhost: true, allow_local_network: false }, - allow_blank: true - attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, @@ -675,10 +678,15 @@ class ApplicationSetting < ApplicationRecord attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm attr_encrypted :jitsu_administrator_password, encryption_options_base_32_aes_256_gcm + attr_encrypted :telesign_customer_xid, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) + attr_encrypted :telesign_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) validates :disable_feed_token, inclusion: { in: [true, false], message: N_('must be a boolean value') } + validates :disable_admin_oauth_scopes, + 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 diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index dee4bd07fd9..308c05d638c 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -62,6 +62,7 @@ module ApplicationSettingImplementation diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, diff_max_files: Commit::DEFAULT_MAX_DIFF_FILES_SETTING, diff_max_lines: Commit::DEFAULT_MAX_DIFF_LINES_SETTING, + disable_admin_oauth_scopes: false, disable_feed_token: false, disabled_oauth_sign_in_sources: [], dns_rebinding_protection_enabled: true, @@ -103,6 +104,7 @@ module ApplicationSettingImplementation invisible_captcha_enabled: false, issues_create_limit: 300, jira_connect_application_key: nil, + jira_connect_proxy_url: nil, local_markdown_version: 0, login_recaptcha_protection_enabled: false, mailgun_signing_key: nil, diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb index a84a3454a27..0b652984630 100644 --- a/app/models/awareness_session.rb +++ b/app/models/awareness_session.rb @@ -63,16 +63,18 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass user_key = user_sessions_key(user.id) with_redis do |redis| - redis.pipelined do |pipeline| - pipeline.sadd(user_key, id_i) - pipeline.expire(user_key, USER_LIFETIME.to_i) + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + redis.pipelined do |pipeline| + pipeline.sadd?(user_key, id_i) + pipeline.expire(user_key, USER_LIFETIME.to_i) - pipeline.zadd(users_key, timestamp.to_f, user.id) + pipeline.zadd(users_key, timestamp.to_f, user.id) - # We also mark for expiry when a session key is created (first user joins), - # because some users might never actively leave a session and the key could - # therefore become stale, w/o us noticing. - reset_session_expiry(pipeline) + # We also mark for expiry when a session key is created (first user joins), + # because some users might never actively leave a session and the key could + # therefore become stale, w/o us noticing. + reset_session_expiry(pipeline) + end end end @@ -83,26 +85,33 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass user_key = user_sessions_key(user.id) with_redis do |redis| - redis.pipelined do |pipeline| - pipeline.srem(user_key, id_i) - pipeline.zrem(users_key, user.id) + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + redis.pipelined do |pipeline| + pipeline.srem?(user_key, id_i) + pipeline.zrem(users_key, user.id) + end end # cleanup orphan sessions and users # # this needs to be a second pipeline due to the delete operations being # dependent on the result of the cardinality checks - user_sessions_count, session_users_count = redis.pipelined do |pipeline| - pipeline.scard(user_key) - pipeline.zcard(users_key) - end + user_sessions_count, session_users_count = + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + redis.pipelined do |pipeline| + pipeline.scard(user_key) + pipeline.zcard(users_key) + end + end - redis.pipelined do |pipeline| - pipeline.del(user_key) unless user_sessions_count > 0 + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + redis.pipelined do |pipeline| + pipeline.del(user_key) unless user_sessions_count > 0 - unless session_users_count > 0 - pipeline.del(users_key) - @id = nil + unless session_users_count > 0 + pipeline.del(users_key) + @id = nil + end end end end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 1f921c71984..c5a234ffa69 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -23,8 +23,8 @@ class BroadcastMessage < ApplicationRecord validates :color, allow_blank: true, color: true validates :font, allow_blank: true, color: true - default_value_for :color, '#E75E40' - default_value_for :font, '#FFFFFF' + attribute :color, default: '#E75E40' + attribute :font, default: '#FFFFFF' CACHE_KEY = 'broadcast_message_current_json' BANNER_CACHE_KEY = 'broadcast_message_current_banner_json' diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 323d759510e..d6051d70503 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -21,7 +21,6 @@ module Ci has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id - has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline validates :ref, presence: true @@ -58,11 +57,7 @@ module Ci end def retryable? - return false unless Feature.enabled?(:ci_recreate_downstream_pipeline, project) - - return false if failed? && (pipeline_loop_detected? || reached_max_descendant_pipelines_depth?) - - super + false end def self.with_preloads @@ -183,7 +178,7 @@ module Ci false end - def prevent_rollback_deployment? + def outdated_deployment? false end @@ -288,7 +283,11 @@ module Ci return [] unless forward_yaml_variables? yaml_variables.to_a.map do |hash| - { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) } + if hash[:raw] && ci_raw_variables_in_yaml_config_enabled? + { key: hash[:key], value: hash[:value], raw: true } + else + { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) } + end end end @@ -296,7 +295,11 @@ module Ci return [] unless forward_pipeline_variables? pipeline.variables.to_a.map do |variable| - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } + if variable.raw? && ci_raw_variables_in_yaml_config_enabled? + { key: variable.key, value: variable.value, raw: true } + else + { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } + end end end @@ -305,7 +308,11 @@ module Ci return [] unless pipeline.pipeline_schedule pipeline.pipeline_schedule.variables.to_a.map do |variable| - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } + if variable.raw? && ci_raw_variables_in_yaml_config_enabled? + { key: variable.key, value: variable.value, raw: true } + else + { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } + end end end @@ -324,6 +331,12 @@ module Ci result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result end end + + def ci_raw_variables_in_yaml_config_enabled? + strong_memoize(:ci_raw_variables_in_yaml_config_enabled) do + ::Feature.enabled?(:ci_raw_variables_in_yaml_config, project) + end + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b8511536e32..f44ba124fe2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -72,33 +72,6 @@ module Ci delegate :trigger_short_token, to: :trigger_request, allow_nil: true delegate :ensure_persistent_ref, to: :pipeline - ## - # Since Gitlab 11.5, deployments records started being created right after - # `ci_builds` creation. We can look up a relevant `environment` through - # `deployment` relation today. - # (See more https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22380) - # - # Since Gitlab 12.9, we started persisting the expanded environment name to - # avoid repeated variables expansion in `action: stop` builds as well. - def persisted_environment - return unless has_environment? - - strong_memoize(:persisted_environment) do - # This code path has caused N+1s in the past, since environments are only indirectly - # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445 - # We therefore batch-load them to prevent dormant N+1s until we found a proper solution. - BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args| - Environment.where(name: names, project: args[:key]).find_each do |environment| - loader.call(environment.name, environment) - end - end - end - end - - def persisted_environment=(environment) - strong_memoize(:persisted_environment) { environment } - end - serialize :options # rubocop:disable Cop/ActiveRecordSerialize serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize @@ -199,7 +172,7 @@ module Ci add_authentication_token_field :token, encrypted: :required - before_save :ensure_token + before_save :ensure_token, unless: :assign_token_on_scheduling? after_save :stick_build_if_status_changed @@ -218,10 +191,6 @@ module Ci preload(:job_artifacts_archive, :job_artifacts, :tags, project: [:namespace]) end - def extra_accessors - [] - end - def clone_accessors %i[pipeline project ref tag options name allow_failure stage stage_idx trigger_request @@ -278,6 +247,14 @@ module Ci !build.waiting_for_deployment_approval? # If false is returned, it stops the transition end + before_transition any => [:pending] do |build, transition| + if build.assign_token_on_scheduling? + build.ensure_token + end + + true + end + after_transition created: :scheduled do |build| build.run_after_commit do Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id) @@ -445,9 +422,10 @@ module Ci manual? && starts_environment? && deployment&.blocked? end - def prevent_rollback_deployment? - strong_memoize(:prevent_rollback_deployment) do + def outdated_deployment? + strong_memoize(:outdated_deployment) do starts_environment? && + incomplete? && project.ci_forward_deployment_enabled? && deployment&.older_than_last_successful_deployment? end @@ -494,8 +472,34 @@ module Ci Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet end + def persisted_environment + return unless has_environment_keyword? + + strong_memoize(:persisted_environment) do + # This code path has caused N+1s in the past, since environments are only indirectly + # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445 + # We therefore batch-load them to prevent dormant N+1s until we found a proper solution. + BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args| + Environment.where(name: names, project: args[:key]).find_each do |environment| + loader.call(environment.name, environment) + end + end + end + end + + def persisted_environment=(environment) + strong_memoize(:persisted_environment) { environment } + end + + # If build.persisted_environment is a BatchLoader, we need to remove + # the method proxy in order to clone into new item here + # https://github.com/exAspArk/batch-loader/issues/31 + def actual_persisted_environment + persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment + end + def expanded_environment_name - return unless has_environment? + return unless has_environment_keyword? strong_memoize(:expanded_environment_name) do # We're using a persisted expanded environment name in order to avoid @@ -509,7 +513,7 @@ module Ci end def expanded_kubernetes_namespace - return unless has_environment? + return unless has_environment_keyword? namespace = options.dig(:environment, :kubernetes, :namespace) @@ -520,16 +524,16 @@ module Ci end end - def has_environment? + def has_environment_keyword? environment.present? end def starts_environment? - has_environment? && self.environment_action == 'start' + has_environment_keyword? && self.environment_action == 'start' end def stops_environment? - has_environment? && self.environment_action == 'stop' + has_environment_keyword? && self.environment_action == 'stop' end def environment_action @@ -971,7 +975,7 @@ module Ci def collect_codequality_reports!(codequality_report) each_report(Ci::JobArtifact.file_types_for_report(:codequality)) do |file_type, blob| - Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report) + Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report, { project: project, commit_sha: pipeline.sha }) end codequality_report @@ -1043,7 +1047,8 @@ module Ci # TODO: Have `debug_mode?` check against data on sent back from runner # to capture all the ways that variables can be set. # See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955) - variables['CI_DEBUG_TRACE']&.value&.casecmp('true') == 0 + variables['CI_DEBUG_TRACE']&.value&.casecmp('true') == 0 || + variables['CI_DEBUG_SERVICES']&.value&.casecmp('true') == 0 end def drop_with_exit_code!(failure_reason, exit_code) @@ -1131,6 +1136,10 @@ module Ci end end + def assign_token_on_scheduling? + ::Feature.enabled?(:ci_assign_job_token_on_scheduling, project) + end + protected def run_status_commit_hooks! @@ -1185,7 +1194,7 @@ module Ci def environment_status strong_memoize(:environment_status) do - if has_environment? && merge_request + if has_environment_keyword? && merge_request EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha) end end @@ -1205,8 +1214,6 @@ module Ci def legacy_jwt_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - break variables unless Feature.enabled?(:ci_job_jwt, project) - jwt = Gitlab::Ci::Jwt.for_build(self) jwt_v2 = Gitlab::Ci::JwtV2.for_build(self) variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true) diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 33092e881f0..2f28509f812 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -5,6 +5,7 @@ module Ci # Data that should be persisted forever, should be stored with Ci::Build model. class BuildMetadata < Ci::ApplicationRecord BuildTimeout = Struct.new(:value, :source) + ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_metadata_routing_table include Ci::Partitionable include Presentable @@ -13,7 +14,12 @@ module Ci self.table_name = 'ci_builds_metadata' self.primary_key = 'id' - partitionable scope: :build + self.sequence_name = 'ci_builds_metadata_id_seq' + + partitionable scope: :build, through: { + table: :p_ci_builds_metadata, + flag: ROUTING_FEATURE_FLAG + } belongs_to :build, class_name: 'CommitStatus' belongs_to :project @@ -24,9 +30,9 @@ module Ci validates :id_tokens, json_schema: { filename: 'build_metadata_id_tokens' } validates :secrets, json_schema: { filename: 'build_metadata_secrets' } - serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize - serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize - serialize :runtime_runner_features, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize + attribute :config_options, :sym_jsonb + attribute :config_variables, :sym_jsonb + attribute :runtime_runner_features, :sym_jsonb chronic_duration_attr_reader :timeout_human_readable, :timeout @@ -50,7 +56,7 @@ module Ci end def set_cancel_gracefully - runtime_runner_features.merge!( { cancel_gracefully: true } ) + runtime_runner_features.merge!({ cancel_gracefully: true }) end def cancel_gracefully? diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 221a2284106..7baa98b59f9 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -10,7 +10,7 @@ module Ci belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id - default_value_for :data_store, :redis_trace_chunks + attribute :data_store, default: :redis_trace_chunks after_create { metrics.increment_trace_operation(operation: :chunked) } diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index e11edbda6dc..508aaa5a63c 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -5,6 +5,7 @@ module Ci include Ci::HasVariable include Presentable include Ci::Maskable + include Ci::RawVariable prepend HasEnvironmentScope belongs_to :group, class_name: "::Group" diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb index da9d4dea537..3e572dbe18f 100644 --- a/app/models/ci/instance_variable.rb +++ b/app/models/ci/instance_variable.rb @@ -5,6 +5,7 @@ module Ci extend Gitlab::ProcessMemoryCache::Helper include Ci::NewHasVariable include Ci::Maskable + include Ci::RawVariable include Limitable self.limit_name = 'ci_instance_level_variables' diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb index 44bd3fe8901..332a78b66ae 100644 --- a/app/models/ci/job_variable.rb +++ b/app/models/ci/job_variable.rb @@ -3,6 +3,7 @@ module Ci class JobVariable < Ci::ApplicationRecord include Ci::NewHasVariable + include Ci::RawVariable include BulkInsertSafe belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index cc5ba41191b..020f5cf9d8e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -121,7 +121,7 @@ module Ci accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :full_path, to: :project, prefix: true - delegate :title, to: :pipeline_metadata, allow_nil: true + delegate :name, to: :pipeline_metadata, allow_nil: true validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } @@ -183,7 +183,11 @@ module Ci end event :succeed do - transition any - [:success] => :success + # A success pipeline can also be retried, for example; a pipeline with a failed manual job. + # When retrying the pipeline, the status of the pipeline is not changed because the failed + # manual job transitions to the `manual` status. + # More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98967#note_1144718316 + transition any => :success end event :cancel do diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb index c96b395b45f..2bd206c5ca5 100644 --- a/app/models/ci/pipeline_metadata.rb +++ b/app/models/ci/pipeline_metadata.rb @@ -9,6 +9,6 @@ module Ci validates :pipeline, presence: true validates :project, presence: true - validates :title, presence: true, length: { minimum: 1, maximum: 255 } + validates :name, presence: true, length: { minimum: 1, maximum: 255 } end end diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb index 84a24609cc7..718ed14edeb 100644 --- a/app/models/ci/pipeline_schedule_variable.rb +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -3,6 +3,7 @@ module Ci class PipelineScheduleVariable < Ci::ApplicationRecord include Ci::HasVariable + include Ci::RawVariable belongs_to :pipeline_schedule diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 6e4418bc360..8e83b41cd0b 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -4,6 +4,7 @@ module Ci class PipelineVariable < Ci::ApplicationRecord include Ci::Partitionable include Ci::HasVariable + include Ci::RawVariable belongs_to :pipeline diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 09dc9d4bce1..eb805ffae0a 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -7,6 +7,7 @@ module Ci extend ::Gitlab::Utils::Override has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable + has_one :sourced_pipeline, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id, inverse_of: :source_job belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb index ffff7eebbee..df38398e5a9 100644 --- a/app/models/ci/secure_file.rb +++ b/app/models/ci/secure_file.rb @@ -27,7 +27,7 @@ module Ci serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize - default_value_for(:file_store) { Ci::SecureFileUploader.default_store } + attribute :file_store, default: -> { Ci::SecureFileUploader.default_store } mount_file_store_uploader Ci::SecureFileUploader diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index c80c2ebe69a..f4e17b5d812 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -5,6 +5,7 @@ module Ci include Ci::HasVariable include Presentable include Ci::Maskable + include Ci::RawVariable prepend HasEnvironmentScope belongs_to :project diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 2a051233de2..11f84940c38 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -15,11 +15,8 @@ module Clusters include ::Clusters::Concerns::ApplicationVersion include ::Clusters::Concerns::ApplicationData - default_value_for :version, VERSION - - default_value_for :email do |cert_manager| - cert_manager.cluster&.user&.email - end + attribute :version, default: VERSION + after_initialize :set_default_email, if: :new_record? validates :email, presence: true @@ -55,6 +52,10 @@ module Clusters private + def set_default_email + self.email ||= self.cluster&.user&.email + end + def pre_install_script [ apply_file("https://raw.githubusercontent.com/jetstack/cert-manager/release-#{CRD_VERSION}/deploy/manifests/00-crds.yaml"), diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb index 07378b4e8dc..a7b4fb57149 100644 --- a/app/models/clusters/applications/crossplane.rb +++ b/app/models/clusters/applications/crossplane.rb @@ -14,11 +14,8 @@ module Clusters include ::Clusters::Concerns::ApplicationVersion include ::Clusters::Concerns::ApplicationData - default_value_for :version, VERSION - - default_value_for :stack do |crossplane| - '' - end + attribute :version, default: VERSION + attribute :stack, default: "" validates :stack, presence: true diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index e89cb8be1e7..9fac852ed5b 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -18,7 +18,7 @@ module Clusters include ::Clusters::Concerns::ApplicationStatus include ::Gitlab::Utils::StrongMemoize - default_value_for :version, Gitlab::Kubernetes::Helm::V2::BaseCommand::HELM_VERSION + attribute :version, default: Gitlab::Kubernetes::Helm::V2::BaseCommand::HELM_VERSION before_create :create_keys_and_certs diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 27550616002..034b178d67d 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -17,12 +17,11 @@ module Clusters include AfterCommitQueue include UsageStatistics - default_value_for :ingress_type, :nginx - default_value_for :version, VERSION + attribute :version, default: VERSION enum ingress_type: { nginx: 1 - } + }, _default: :nginx FETCH_IP_ADDRESS_DELAY = 30.seconds diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 8d7d9c20bfa..9c0e90d59ed 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -18,7 +18,7 @@ module Clusters belongs_to :oauth_application, class_name: 'Doorkeeper::Application' - default_value_for :version, VERSION + attribute :version, default: VERSION def set_initial_status return unless not_installable? diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 0e7cbb35e47..64366594583 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -43,7 +43,7 @@ module Clusters end end - default_value_for :version, VERSION + attribute :version, default: VERSION validates :hostname, presence: true, hostname: true diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index d1e169a1f78..a076c871824 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -15,7 +15,7 @@ module Clusters include ::Clusters::Concerns::ApplicationData include AfterCommitQueue - default_value_for :version, VERSION + attribute :version, default: VERSION scope :preload_cluster_platform, -> { preload(cluster: [:platform_kubernetes]) } @@ -24,7 +24,7 @@ module Clusters key: Settings.attr_encrypted_db_key_base_32, algorithm: 'aes-256-gcm' - default_value_for(:alert_manager_token) { SecureRandom.hex } + after_initialize :set_alert_manager_token, if: :new_record? after_destroy do cluster.find_or_build_integration_prometheus.destroy @@ -101,6 +101,10 @@ module Clusters private + def set_alert_manager_token + self.alert_manager_token = SecureRandom.hex + end + def install_knative_metrics return [] unless cluster.application_knative_available? diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 1ac4cbac1da..b8ed33828bc 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -15,7 +15,7 @@ module Clusters belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id delegate :project, :group, to: :cluster - default_value_for :version, VERSION + attribute :version, default: VERSION def chart "#{name}/gitlab-runner" diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index ad1e7dc305f..25d41d68b9e 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -79,7 +79,7 @@ module Clusters validates :namespace_per_environment, inclusion: { in: [true, false] } validates :helm_major_version, inclusion: { in: [2, 3] } - default_value_for :helm_major_version, 3 + attribute :helm_major_version, default: 3 validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb index 899529ff49f..935d6238dba 100644 --- a/app/models/clusters/integrations/prometheus.rb +++ b/app/models/clusters/integrations/prometheus.rb @@ -26,7 +26,7 @@ module Clusters key: Settings.attr_encrypted_db_key_base_32, algorithm: 'aes-256-gcm' - default_value_for(:alert_manager_token) { SecureRandom.hex } + after_initialize :set_alert_manager_token, if: :new_record? scope :enabled, -> { where(enabled: true) } @@ -54,6 +54,10 @@ module Clusters private + def set_alert_manager_token + self.alert_manager_token = SecureRandom.hex + end + def activate_project_integrations ::Clusters::Applications::ActivateIntegrationWorker .perform_async(cluster_id, ::Integrations::Prometheus.to_param) diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 9d4f0a89403..165285b34b2 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -10,6 +10,7 @@ module Clusters include NullifyIfBlank RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze + REQUIRED_K8S_MIN_VERSION = 23 IGNORED_CONNECTION_EXCEPTIONS = [ Gitlab::UrlBlocker::BlockedUrlError, @@ -21,6 +22,8 @@ module Clusters OpenSSL::SSL::SSLError ].freeze + FailedVersionCheckError = Class.new(StandardError) + self.table_name = 'cluster_platforms_kubernetes' self.reactive_cache_work_type = :external_dependency @@ -64,9 +67,7 @@ module Clusters unknown_authorization: nil, rbac: 1, abac: 2 - } - - default_value_for :authorization_type, :rbac + }, _default: :rbac nullify_if_blank :namespace @@ -208,6 +209,29 @@ module Clusters kubeclient.get_ingresses(namespace: namespace).as_json rescue Kubeclient::ResourceNotFoundError [] + rescue NoMethodError => e + # We get NoMethodError for Kubernetes versions < 1.19. Since we only support >= 1.23 + # we will ignore this error for previous versions. For more details read: + # https://gitlab.com/gitlab-org/gitlab/-/issues/371249#note_1079866043 + return [] if server_version < REQUIRED_K8S_MIN_VERSION + + raise e + end + + def server_version + full_url = Gitlab::UrlSanitizer.new("#{api_url}/version").full_url + + # We can't use `kubeclient` to check the cluster version because it does not support it + # https://github.com/ManageIQ/kubeclient/issues/309 + response = Gitlab::HTTP.perform_request( + Net::HTTP::Get, full_url, + headers: { "Authorization" => "Bearer #{token}" }, + cert_store: kubeclient_ssl_options[:cert_store]) + + Gitlab::ErrorTracking.track_exception(FailedVersionCheckError.new) unless response.success? + + json_response = Gitlab::Json.parse(response.body) + json_response["minor"].to_i end def build_kube_client! diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb index af2eba42721..f0f56d9ebd9 100644 --- a/app/models/clusters/providers/aws.rb +++ b/app/models/clusters/providers/aws.rb @@ -12,9 +12,9 @@ module Clusters belongs_to :cluster, inverse_of: :provider_aws, class_name: 'Clusters::Cluster' - default_value_for :region, DEFAULT_REGION - default_value_for :num_nodes, 3 - default_value_for :instance_type, 'm5.large' + attribute :region, default: DEFAULT_REGION + attribute :num_nodes, default: 3 + attribute :instance_type, default: "m5.large" attr_encrypted :secret_access_key, mode: :per_attribute_iv, diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index 2ca7d0249dc..fde5ed592cb 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -9,10 +9,10 @@ module Clusters belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster' - default_value_for :zone, 'us-central1-a' - default_value_for :num_nodes, 3 - default_value_for :machine_type, 'n1-standard-2' - default_value_for :cloud_run, false + attribute :zone, default: 'us-central1-a' + attribute :num_nodes, default: 3 + attribute :machine_type, default: 'n1-standard-2' + attribute :cloud_run, default: false scope :cloud_run, -> { where(cloud_run: true) } diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb index a3ee8e4f364..7d89ddde0cb 100644 --- a/app/models/commit_collection.rb +++ b/app/models/commit_collection.rb @@ -13,10 +13,11 @@ class CommitCollection # container - The object the commits belong to. # commits - The Commit instances to store. # ref - The name of the ref (e.g. "master"). - def initialize(container, commits, ref = nil) + def initialize(container, commits, ref = nil, page: nil, per_page: nil, count: nil) @container = container @commits = commits @ref = ref + @pagination = Gitlab::PaginationDelegate.new(page: page, per_page: per_page, count: count) end def each(&block) @@ -113,4 +114,8 @@ class CommitCollection def method_missing(message, *args, &block) commits.public_send(message, *args, &block) end + + def next_page + @pagination.next_page + end end diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb index 1ce76b53da4..2ae59853520 100644 --- a/app/models/commit_signatures/gpg_signature.rb +++ b/app/models/commit_signatures/gpg_signature.rb @@ -49,5 +49,9 @@ module CommitSignatures Gitlab::Gpg::Commit.new(commit) end + + def user + gpg_key&.user + end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 05a258e6e26..2470eada62e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -9,11 +9,9 @@ class CommitStatus < Ci::ApplicationRecord include EnumWithNil include BulkInsertableAssociations include TaggableQueries - include IgnorableColumns self.table_name = 'ci_builds' partitionable scope: :pipeline - ignore_column :trace, remove_with: '15.6', remove_after: '2022-10-22' belongs_to :user belongs_to :project @@ -23,7 +21,12 @@ class CommitStatus < Ci::ApplicationRecord has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build + attribute :retried, default: false + enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true + # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily + # extend this `Hash` with new values. + enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons delegate :commit, to: :pipeline delegate :sha, :short_sha, :before_sha, to: :pipeline @@ -98,12 +101,6 @@ class CommitStatus < Ci::ApplicationRecord merge(or_conditions) end - # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily - # extend this `Hash` with new values. - enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons - - default_value_for :retried, false - ## # We still create some CommitStatuses outside of CreatePipelineService. # diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb index 910885c833f..9a04776f1c6 100644 --- a/app/models/concerns/ci/has_status.rb +++ b/app/models/concerns/ci/has_status.rb @@ -110,6 +110,10 @@ module Ci COMPLETED_STATUSES.include?(status) end + def incomplete? + COMPLETED_STATUSES.exclude?(status) + end + def blocked? BLOCKED_STATUS.include?(status) end diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index ff884984099..d93f4a150d5 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -87,6 +87,16 @@ module Ci ensure_metadata.id_tokens = value end + def enqueue_immediately? + !!options[:enqueue_immediately] + end + + def set_enqueue_immediately! + # ensures that even if `config_options: nil` in the database we set the + # new value correctly. + self.options = options.merge(enqueue_immediately: true) + end + private def read_metadata_attribute(legacy_key, metadata_key, default_value = nil) diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb index df803180e77..68a6714c892 100644 --- a/app/models/concerns/ci/partitionable.rb +++ b/app/models/concerns/ci/partitionable.rb @@ -57,9 +57,14 @@ module Ci end class_methods do - private + def partitionable(scope:, through: nil) + if through + define_singleton_method(:routing_table_name) { through[:table] } + define_singleton_method(:routing_table_name_flag) { through[:flag] } + + include Partitionable::Switch + end - def partitionable(scope:) define_method(:partition_scope_value) do strong_memoize(:partition_scope_value) do next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing? diff --git a/app/models/concerns/ci/partitionable/switch.rb b/app/models/concerns/ci/partitionable/switch.rb new file mode 100644 index 00000000000..c1bbd107e9f --- /dev/null +++ b/app/models/concerns/ci/partitionable/switch.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Ci + module Partitionable + module Switch + extend ActiveSupport::Concern + + # These methods are cached at the class level and depend on the value + # of `table_name`, changing that value resets them. + # `cached_find_by_statement` is used to cache SQL statements which can + # include the table name. + # + SWAPABLE_METHODS = %i[table_name quoted_table_name arel_table + predicate_builder cached_find_by_statement].freeze + + included do |base| + partitioned = Class.new(base) do + self.table_name = base.routing_table_name + + def self.routing_class? + true + end + end + + base.const_set(:Partitioned, partitioned) + end + + class_methods do + def routing_class? + false + end + + def routing_table_enabled? + return false if routing_class? + + Gitlab::SafeRequestStore.fetch(routing_table_name_flag) do + ::Feature.enabled?(routing_table_name_flag) + end + end + + # We're delegating them to the `Partitioned` model. + # They do not require any check override since they come from AR core + # (are always defined) and we're using `super` to get the value. + # + SWAPABLE_METHODS.each do |name| + define_method(name) do |*args, &block| + if routing_table_enabled? + self::Partitioned.public_send(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend + else + super(*args, &block) + end + end + end + end + end + end +end diff --git a/app/models/concerns/ci/raw_variable.rb b/app/models/concerns/ci/raw_variable.rb new file mode 100644 index 00000000000..5cfc781c9f1 --- /dev/null +++ b/app/models/concerns/ci/raw_variable.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Ci + module RawVariable + extend ActiveSupport::Concern + + included do + validates :raw, inclusion: { in: [true, false] } + end + + private + + def uncached_runner_variable + super.merge(raw: raw?) + end + end +end diff --git a/app/models/concerns/ci/track_environment_usage.rb b/app/models/concerns/ci/track_environment_usage.rb index 45d9cdeeb59..fe548c77590 100644 --- a/app/models/concerns/ci/track_environment_usage.rb +++ b/app/models/concerns/ci/track_environment_usage.rb @@ -17,7 +17,7 @@ module Ci end def verifies_environment? - has_environment? && environment_action == 'verify' + has_environment_keyword? && environment_action == 'verify' end def count_user_deployment? diff --git a/app/models/concerns/encrypted_user_password.rb b/app/models/concerns/encrypted_user_password.rb new file mode 100644 index 00000000000..97e6592f442 --- /dev/null +++ b/app/models/concerns/encrypted_user_password.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Support for both BCrypt and PBKDF2+SHA512 user passwords +# Meant to be used exclusively with User model but extracted +# to a concern for isolation and clarity. +module EncryptedUserPassword + extend ActiveSupport::Concern + + BCRYPT_PREFIX = '$2a$' + PBKDF2_SHA512_PREFIX = '$pbkdf2-sha512$' + + BCRYPT_STRATEGY = :bcrypt + PBKDF2_SHA512_STRATEGY = :pbkdf2_sha512 + + # Use Devise DatabaseAuthenticatable#authenticatable_salt + # unless encrypted password is PBKDF2+SHA512. + def authenticatable_salt + return super unless pbkdf2_password? + + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.split_digest(encrypted_password)[:salt] + end + + # Called by Devise during database authentication. + # Also migrates the user password to the configured + # encryption type (BCrypt or PBKDF2+SHA512), if needed. + def valid_password?(password) + return false unless password_matches?(password) + + migrate_password!(password) + end + + def password=(new_password) + @password = new_password # rubocop:disable Gitlab/ModuleWithInstanceVariables + return unless new_password.present? + + self.encrypted_password = if Gitlab::FIPS.enabled? + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest( + new_password, + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512::STRETCHES, + Devise.friendly_token[0, 16]) + else + Devise::Encryptor.digest(self.class, new_password) + end + end + + private + + def password_strategy + return BCRYPT_STRATEGY if encrypted_password.starts_with?(BCRYPT_PREFIX) + return PBKDF2_SHA512_STRATEGY if encrypted_password.starts_with?(PBKDF2_SHA512_PREFIX) + + :unknown + end + + def pbkdf2_password? + password_strategy == PBKDF2_SHA512_STRATEGY + end + + def bcrypt_password? + password_strategy == BCRYPT_STRATEGY + end + + def password_matches?(password) + if bcrypt_password? + Devise::Encryptor.compare(self.class, encrypted_password, password) + elsif pbkdf2_password? + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.compare(encrypted_password, password) + end + end + + def migrate_password!(password) + return true if password_strategy == encryptor + + update_attribute(:password, password) + end + + def encryptor + return BCRYPT_STRATEGY unless Gitlab::FIPS.enabled? + + PBKDF2_SHA512_STRATEGY + end +end diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb index 518efa669ad..8848c0c5555 100644 --- a/app/models/concerns/enums/sbom.rb +++ b/app/models/concerns/enums/sbom.rb @@ -6,8 +6,23 @@ module Enums library: 0 }.with_indifferent_access.freeze + PURL_TYPES = { + composer: 1, # refered to as `packagist` in gemnasium-db + conan: 2, + gem: 3, + golang: 4, # refered to as `go` in gemnasium-db + maven: 5, + npm: 6, + nuget: 7, + pypi: 8 + }.with_indifferent_access.freeze + def self.component_types COMPONENT_TYPES end + + def self.purl_types + PURL_TYPES + end end end diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb index f1ac734635d..4d267dc69d0 100644 --- a/app/models/concerns/file_store_mounter.rb +++ b/app/models/concerns/file_store_mounter.rb @@ -1,31 +1,35 @@ # frozen_string_literal: true module FileStoreMounter + ALLOWED_FILE_FIELDS = %i[file signed_file].freeze + extend ActiveSupport::Concern class_methods do - # When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!` - def mount_file_store_uploader(uploader, skip_store_file: false) - mount_uploader(:file, uploader) + # When `skip_store_file: true` is used, the model MUST explicitly call `store_#{file_field}_now!` + def mount_file_store_uploader(uploader, skip_store_file: false, file_field: :file) + raise ArgumentError, "file_field not allowed: #{file_field}" unless ALLOWED_FILE_FIELDS.include?(file_field) + + mount_uploader(file_field, uploader) + + define_method("update_#{file_field}_store") do + # The file.object_store is set during `uploader.store!` and `uploader.migrate!` + update_column("#{file_field}_store", public_send(file_field).object_store) # rubocop:disable GitlabSecurity/PublicSend + end + + define_method("store_#{file_field}_now!") do + public_send("store_#{file_field}!") # rubocop:disable GitlabSecurity/PublicSend + public_send("update_#{file_field}_store") # rubocop:disable GitlabSecurity/PublicSend + end if skip_store_file - skip_callback :save, :after, :store_file! + skip_callback :save, :after, "store_#{file_field}!".to_sym return end # This hook is a no-op when the file is uploaded after_commit - after_save :update_file_store, if: :saved_change_to_file? + after_save "update_#{file_field}_store".to_sym, if: "saved_change_to_#{file_field}?".to_sym end end - - def update_file_store - # The file.object_store is set during `uploader.store!` and `uploader.migrate!` - update_column(:file_store, file.object_store) - end - - def store_file_now! - store_file! - update_file_store - end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f8389865f91..31b2a8d7cc1 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -217,6 +217,10 @@ module Issuable false end + def supports_confidentiality? + false + end + def severity return IssuableSeverity::DEFAULT unless supports_severity? @@ -236,7 +240,6 @@ module Issuable end def validate_assignee_size_length - return true unless Feature.enabled?(:limit_assignees_per_issuable) return true unless assignees.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS errors.add :assignees, @@ -460,18 +463,6 @@ module Issuable end end - def today? - Date.today == created_at.to_date - end - - def created_hours_ago - (Time.now.utc.to_i - created_at.utc.to_i) / 3600 - end - - def new? - created_hours_ago < 24 - end - def open? opened? end diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index 14c54d99ef3..a95bed7ad42 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -18,7 +18,7 @@ module Milestoneable scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) } scope :any_release, -> { joins_milestone_releases } - scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) } + scope :with_release, -> (tag, project_id) { joins_milestone_releases.where(milestones: { releases: { tag: tag, project_id: project_id } }) } scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) } scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } diff --git a/app/models/concerns/mirror_authentication.rb b/app/models/concerns/mirror_authentication.rb index 14c8be93ce0..e3bfeaf7f95 100644 --- a/app/models/concerns/mirror_authentication.rb +++ b/app/models/concerns/mirror_authentication.rb @@ -11,7 +11,7 @@ module MirrorAuthentication # We should generate a key even if there's no SSH URL present before_validation :generate_ssh_private_key!, if: -> { - regenerate_ssh_private_key || ( auth_method == 'ssh_public_key' && ssh_private_key.blank? ) + regenerate_ssh_private_key || (auth_method == 'ssh_public_key' && ssh_private_key.blank?) } credentials_field :auth_method, reader: false diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index c1aac235d33..492d55c74e2 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -210,11 +210,23 @@ module Noteable # Synthetic system notes don't have discussion IDs because these are generated dynamically # in Ruby. These are always root notes anyway so we don't need to group by discussion ID. def synthetic_note_ids_relations - [ - resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at), - resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at), - resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at) - ] + relations = [] + + # currently multiple models include Noteable concern, but not all of them support + # all resource events, so we check if given model supports given resource event. + if respond_to?(:resource_label_events) + relations << resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at) + end + + if respond_to?(:resource_state_events) + relations << resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at) + end + + if respond_to?(:resource_milestone_events) + relations << resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at) + end + + relations end end diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb index 1520ec0828e..75fd45d13a9 100644 --- a/app/models/concerns/packages/debian/distribution.rb +++ b/app/models/concerns/packages/debian/distribution.rb @@ -85,8 +85,7 @@ module Packages scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) } mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader - mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader - after_save :update_signed_file_store, if: :saved_change_to_signed_file? + mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader, file_field: :signed_file def component_names components.pluck(:name).sort @@ -119,12 +118,6 @@ module Packages self.class.with_container(container).with_codename(suite).exists? end - - def update_signed_file_store - # The signed_file.object_store is set during `uploader.store!` - # which happens after object is inserted/updated - self.update_column(:signed_file_store, signed_file.object_store) - end end end end diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb index 335fcec2611..562c8cf23f3 100644 --- a/app/models/concerns/pg_full_text_searchable.rb +++ b/app/models/concerns/pg_full_text_searchable.rb @@ -25,6 +25,7 @@ module PgFullTextSearchable TSVECTOR_MAX_LENGTH = 1.megabyte.freeze TEXT_SEARCH_DICTIONARY = 'english' URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}.freeze + TSQUERY_DISALLOWED_CHARACTERS_REGEX = %r{[^a-zA-Z0-9 .@/\-_"]}.freeze def update_search_data! tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight| @@ -102,21 +103,16 @@ module PgFullTextSearchable end end - def pg_full_text_search(search_term) + def pg_full_text_search(query, matched_columns: []) search_data_table = reflect_on_association(:search_data).klass.arel_table - # This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs - # See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920 - search_term = remove_url_scheme(search_term) - search_term = ActiveSupport::Inflector.transliterate(search_term) - joins(:search_data).where( Arel::Nodes::InfixOperation.new( '@@', search_data_table[:search_vector], Arel::Nodes::NamedFunction.new( - 'websearch_to_tsquery', - [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(search_term)] + 'to_tsquery', + [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), build_tsquery(query, matched_columns)] ) ) ) @@ -124,8 +120,39 @@ module PgFullTextSearchable private - def remove_url_scheme(search_term) - search_term.gsub(URL_SCHEME_REGEX, '') + def build_tsquery(query, matched_columns) + # URLs get broken up into separate words when : is removed below, so we just remove the whole scheme. + query = remove_url_scheme(query) + # Remove accents from search term to match indexed data + query = ActiveSupport::Inflector.transliterate(query) + # Prevent users from using tsquery operators that can cause syntax errors. + query = filter_allowed_characters(query) + + weights = matched_columns.map do |column_name| + pg_full_text_searchable_columns[column_name] + end.compact.join + prefix_search_suffix = ":*#{weights}" + + tsquery = Gitlab::SQL::Pattern.split_query_to_search_terms(query).map do |search_term| + case search_term + when /\A\d+\z/ # Handles https://gitlab.com/gitlab-org/gitlab/-/issues/375337 + "(#{search_term + prefix_search_suffix} | -#{search_term + prefix_search_suffix})" + when /\s/ + search_term.split.map { |t| "#{t}:#{weights}" }.join(' <-> ') + else + search_term + prefix_search_suffix + end + end.join(' & ') + + Arel::Nodes.build_quoted(tsquery) + end + + def remove_url_scheme(query) + query.gsub(URL_SCHEME_REGEX, '') + end + + def filter_allowed_characters(query) + query.gsub(TSQUERY_DISALLOWED_CHARACTERS_REGEX, ' ') end end end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 2976b6f02a7..d37f20e2e7c 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -110,6 +110,10 @@ module ProjectFeaturesCompatibility write_feature_attribute_string(:releases_access_level, value) end + def infrastructure_access_level=(value) + write_feature_attribute_string(:infrastructure_access_level, value) + end + # TODO: Remove this method after we drop support for project create/edit APIs to set the # container_registry_enabled attribute. They can instead set the container_registry_access_level # attribute. diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index ec56f4a32af..7e1ebd1eba3 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -7,7 +7,6 @@ module ProtectedRef belongs_to :project, touch: true validates :name, presence: true - validates :project, presence: true delegate :matching, :matches?, :wildcard?, to: :ref_matcher diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 618ad96905d..facf0808e7a 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -21,8 +21,8 @@ module ProtectedRefAccess included do scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - scope :by_user, -> (user) { where(user_id: user ) } - scope :by_group, -> (group) { where(group_id: group ) } + scope :by_user, -> (user) { where(user_id: user) } + scope :by_group, -> (group) { where(group_id: group) } scope :for_role, -> { where(user_id: nil, group_id: nil) } scope :for_user, -> { where.not(user_id: nil) } scope :for_group, -> { where.not(group_id: nil) } diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb index 2d4ed51ce3b..f1d29ad5a90 100644 --- a/app/models/concerns/redis_cacheable.rb +++ b/app/models/concerns/redis_cacheable.rb @@ -26,8 +26,8 @@ module RedisCacheable end def cache_attributes(values) - Gitlab::Redis::Cache.with do |redis| - redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME) + with_redis do |redis| + redis.set(cache_attribute_key, Gitlab::Json.dump(values), ex: CACHED_ATTRIBUTES_EXPIRY_TIME) end clear_memoization(:cached_attributes) @@ -41,13 +41,17 @@ module RedisCacheable def cached_attributes strong_memoize(:cached_attributes) do - Gitlab::Redis::Cache.with do |redis| + with_redis do |redis| data = redis.get(cache_attribute_key) Gitlab::Json.parse(data, symbolize_names: true) if data end end end + def with_redis(&block) + Gitlab::Redis::Cache.with(&block) # rubocop:disable CodeReuse/ActiveRecord + end + def cast_value_from_cache(attribute, value) self.class.type_for_attribute(attribute.to_s).cast(value) end diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb index b7fd52ab305..87ff413f2c1 100644 --- a/app/models/concerns/repository_storage_movable.rb +++ b/app/models/concerns/repository_storage_movable.rb @@ -19,9 +19,7 @@ module RepositoryStorageMovable inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } } validate :container_repository_writable, on: :create - default_value_for(:destination_storage_name, allows_nil: false) do - Repository.pick_storage_shard - end + attribute :destination_storage_name, default: -> { Repository.pick_storage_shard } state_machine initial: :initial do event :schedule do diff --git a/app/models/concerns/subquery.rb b/app/models/concerns/subquery.rb new file mode 100644 index 00000000000..ae92d2137c1 --- /dev/null +++ b/app/models/concerns/subquery.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Distinguish between a top level query and a subselect. +# +# Retrieve column values when the relation has already been loaded, otherwise reselect the relation. +# Useful for preload query patterns where the typical Rails #preload does not fit. Such as: +# +# projects = Project.where(...) +# projects.load +# ... +# options[members] = ProjectMember.where(...).where(source_id: projects.select(:id)) +module Subquery + extend ActiveSupport::Concern + + class_methods do + def subquery(*column_names, max_limit: 5_000) + if current_scope.loaded? && current_scope.size <= max_limit + current_scope.pluck(*column_names) + else + current_scope.reselect(*column_names) + end + end + end +end diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb index 1c2147beedd..d09ce4873b1 100644 --- a/app/models/concerns/ttl_expirable.rb +++ b/app/models/concerns/ttl_expirable.rb @@ -4,8 +4,8 @@ module TtlExpirable extend ActiveSupport::Concern included do + attribute :read_at, default: -> { Time.zone.now } validates :status, presence: true - default_value_for :read_at, Time.zone.now enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 } diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 14520b2da26..7da4e31b472 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -6,6 +6,7 @@ class ContainerRepository < ApplicationRecord include EachBatch include Sortable include AfterCommitQueue + include Packages::Destructible WAITING_CLEANUP_STATUSES = %i[cleanup_scheduled cleanup_unfinished].freeze REQUIRING_CLEANUP_STATUSES = %i[cleanup_unscheduled cleanup_scheduled].freeze @@ -34,7 +35,7 @@ class ContainerRepository < ApplicationRecord numericality: { greater_than_or_equal_to: 0 }, allow_nil: false - enum status: { delete_scheduled: 0, delete_failed: 1 } + enum status: { delete_scheduled: 0, delete_failed: 1, delete_ongoing: 2 } enum expiration_policy_cleanup_status: { cleanup_unscheduled: 0, cleanup_scheduled: 1, cleanup_unfinished: 2, cleanup_ongoing: 3 } enum migration_skipped_reason: { @@ -69,6 +70,7 @@ class ContainerRepository < ApplicationRecord scope :with_migration_pre_import_started_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_pre_import_started_at, '01-01-1970') < ?", timestamp) } scope :with_migration_pre_import_done_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_pre_import_done_at, '01-01-1970') < ?", timestamp) } scope :with_stale_ongoing_cleanup, ->(threshold) { cleanup_ongoing.where('expiration_policy_started_at < ?', threshold) } + scope :with_stale_delete_at, ->(threshold) { where('delete_started_at < ?', threshold) } scope :import_in_process, -> { where(migration_state: %w[pre_importing pre_import_done importing]) } scope :recently_done_migration_step, -> do @@ -224,6 +226,13 @@ class ContainerRepository < ApplicationRecord end end + # Container Repository model and the code that makes API calls + # are tied. Sometimes (mainly in Geo) we need to work with Registry + # when Container Repository record doesn't even exist. + # The ability to create a not-persisted record with a certain "path" parameter + # is very useful + attr_writer :path + def self.exists_by_path?(path) where( project: path.repository_project, @@ -278,6 +287,10 @@ class ContainerRepository < ApplicationRecord all end + class << self + alias_method :pending_destruction, :delete_scheduled # needed by Packages::Destructible + end + def skip_import(reason:) self.migration_skipped_reason = reason @@ -507,6 +520,14 @@ class ContainerRepository < ApplicationRecord end end + def set_delete_ongoing_status + update_columns(status: :delete_ongoing, delete_started_at: Time.zone.now) + end + + def set_delete_scheduled_status + update_columns(status: :delete_scheduled, delete_started_at: nil) + end + def migration_in_active_state? migration_state.in?(ACTIVE_MIGRATION_STATES) end diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb index 5538e93a39e..9b9c0822f63 100644 --- a/app/models/cycle_analytics/project_level_stage_adapter.rb +++ b/app/models/cycle_analytics/project_level_stage_adapter.rb @@ -4,7 +4,7 @@ # compatible with the old value stream controller actions. module CycleAnalytics class ProjectLevelStageAdapter - ProjectLevelStage = Struct.new(:title, :description, :legend, :name, :project_median, keyword_init: true ) + ProjectLevelStage = Struct.new(:title, :description, :legend, :name, :project_median, keyword_init: true) def initialize(stage, options) @stage = stage diff --git a/app/models/dependency_proxy/group_setting.rb b/app/models/dependency_proxy/group_setting.rb index bcf09b27129..3a7ae66a263 100644 --- a/app/models/dependency_proxy/group_setting.rb +++ b/app/models/dependency_proxy/group_setting.rb @@ -3,7 +3,7 @@ class DependencyProxy::GroupSetting < ApplicationRecord belongs_to :group - validates :group, presence: true + attribute :enabled, default: true - default_value_for :enabled, true + validates :group, presence: true end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 20d19ec9541..66d1ce01814 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -13,7 +13,7 @@ class DeployToken < ApplicationRecord GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token' REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze - default_value_for(:expires_at) { Forever.date } + attribute :expires_at, default: -> { Forever.date } # Do NOT use this `user` for the authentication/authorization of the deploy tokens. # It's for the auditing purpose on Credential Inventory, only. diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 20841bc14cd..ea92b978d3a 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -306,8 +306,8 @@ class Deployment < ApplicationRecord last_deployment_id = environment.last_deployment&.id return false unless last_deployment_id.present? - return false if self.id == last_deployment_id + return false if self.sha == environment.last_deployment&.sha self.id < last_deployment_id end @@ -439,8 +439,9 @@ class Deployment < ApplicationRecord end # default tag limit is 100, 0 means no limit + # when refs_by_oid is passed an SHA, returns refs for that commit def tags(limit: 100) - project.repository.tag_names_contains(sha, limit: limit) + project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || [] end strong_memoize_attr :tags diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index f4d665cf279..041ec98ffc9 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -37,18 +37,18 @@ class DiffDiscussion < Discussion def reply_attributes super.merge( - original_position: original_position.to_json, - position: position.to_json + original_position: Gitlab::Json.dump(original_position), + position: Gitlab::Json.dump(position) ) end def cache_key - positions_json = diff_note_positions.map { |dnp| dnp.position.to_json } + positions_json = diff_note_positions.map { |dnp| Gitlab::Json.dump(dnp.position) } positions_sha = Digest::SHA1.hexdigest(positions_json.join(':')) if positions_json.any? [ super, - Digest::SHA1.hexdigest(position.to_json), + Digest::SHA1.hexdigest(Gitlab::Json.dump(position)), positions_sha ].join(':') end diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb index a1defb2594f..fb127de2bc7 100644 --- a/app/models/diff_viewer/server_side.rb +++ b/app/models/diff_viewer/server_side.rb @@ -9,14 +9,6 @@ module DiffViewer self.size_limit = 5.megabytes end - def prepare! - return if Feature.enabled?(:disable_load_entire_blob_for_diff_viewer, diff_file.repository.project) - - # TODO: remove this after resolving #342703 - diff_file.old_blob&.load_all_data! - diff_file.new_blob&.load_all_data! - end - def render_error # Files that are not stored in the repository, like LFS files and # build artifacts, can only be rendered using a client-side viewer, diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 12d73ef0d72..1c7a8d93e6e 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -104,18 +104,9 @@ module ErrorTracking api_host end - def sentry_response_limit_enabled? - Feature.enabled?(:error_tracking_sentry_limit, project) - end - - def reactive_cache_limit_enabled? - sentry_response_limit_enabled? - end - def sentry_client strong_memoize(:sentry_client) do - ::ErrorTracking::SentryClient - .new(api_url, token, validate_size_guarded_by_feature_flag: sentry_response_limit_enabled?) + ::ErrorTracking::SentryClient.new(api_url, token) end end diff --git a/app/models/event.rb b/app/models/event.rb index 4c1793d3f13..a1417db3410 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -358,7 +358,7 @@ class Event < ApplicationRecord # hence we add the extra WHERE clause for last_activity_at. Project.unscoped.where(id: project_id) .where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago) - .touch_all(:last_activity_at, time: created_at) # rubocop: disable Rails/SkipsModelValidations + .touch_all(:last_activity_at, time: created_at) Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset end @@ -441,7 +441,7 @@ class Event < ApplicationRecord def set_last_repository_updated_at Project.unscoped.where(id: project_id) .where("last_repository_updated_at < ? OR last_repository_updated_at IS NULL", REPOSITORY_UPDATED_AT_INTERVAL.ago) - .touch_all(:last_repository_updated_at, time: created_at) # rubocop: disable Rails/SkipsModelValidations + .touch_all(:last_repository_updated_at, time: created_at) end def design_action_names diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb index 4258027aa56..72e1d28a297 100644 --- a/app/models/event_collection.rb +++ b/app/models/event_collection.rb @@ -62,21 +62,12 @@ class EventCollection end def in_operator_optimized_relation(parent_column, parents, parent_model) - query_builder_params = if Feature.enabled?(:optimized_project_and_group_activity_queries) - array_data = { - scope_ids: parents.pluck(:id), - scope_model: parent_model, - mapping_column: parent_column - } - filter.in_operator_query_builder_params(array_data) - else - { - scope: filtered_events, - array_scope: parents.select(:id), - array_mapping_scope: -> (parent_id_expression) { Event.where(Event.arel_table[parent_column].eq(parent_id_expression)).reorder(id: :desc) }, - finder_query: -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) } - } - end + array_data = { + scope_ids: parents.pluck(:id), + scope_model: parent_model, + mapping_column: parent_column + } + query_builder_params = filter.in_operator_query_builder_params(array_data) Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder .new(**query_builder_params) @@ -84,10 +75,6 @@ class EventCollection .limit(@limit + @offset) end - def filtered_events - filter.apply_filter(base_relation) - end - def paginate_events(events) events.limit(@limit).offset(@offset) end diff --git a/app/models/experiment.rb b/app/models/experiment.rb deleted file mode 100644 index 2300ec2996d..00000000000 --- a/app/models/experiment.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -class Experiment < ApplicationRecord - has_many :experiment_users - has_many :experiment_subjects, inverse_of: :experiment - - validates :name, presence: true, uniqueness: true, length: { maximum: 255 } - - def self.add_user(name, group_type, user, context = {}) - by_name(name).record_user_and_group(user, group_type, context) - end - - def self.add_group(name, variant:, group:) - add_subject(name, variant: variant, subject: group) - end - - def self.add_subject(name, variant:, subject:) - by_name(name).record_subject_and_variant!(subject, variant) - end - - def self.record_conversion_event(name, user, context = {}) - by_name(name).record_conversion_event_for_user(user, context) - end - - def self.by_name(name) - find_or_create_by!(name: name) - end - - # Create or update the recorded experiment_user row for the user in this experiment. - def record_user_and_group(user, group_type, context = {}) - experiment_user = experiment_users.find_or_initialize_by(user: user) - experiment_user.assign_attributes(group_type: group_type, context: merged_context(experiment_user, context)) - # We only call save when necessary because this causes the request to stick to the primary DB - # even when the save is a no-op - # https://gitlab.com/gitlab-org/gitlab/-/issues/324649 - experiment_user.save! if experiment_user.changed? - - experiment_user - end - - def record_conversion_event_for_user(user, context = {}) - experiment_user = experiment_users.find_by(user: user) - return unless experiment_user - - experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context)) - end - - def record_conversion_event_for_subject(subject, context = {}) - raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject) - - attr_name = subject.class.table_name.singularize.to_sym - experiment_subject = experiment_subjects.find_by(attr_name => subject) - return unless experiment_subject - - experiment_subject.update!(converted_at: Time.current, context: merged_context(experiment_subject, context)) - end - - def record_subject_and_variant!(subject, variant) - raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject) - - attr_name = subject.class.table_name.singularize.to_sym - experiment_subject = experiment_subjects.find_or_initialize_by(attr_name => subject) - experiment_subject.assign_attributes(variant: variant) - # We only call save when necessary because this causes the request to stick to the primary DB - # even when the save is a no-op - # https://gitlab.com/gitlab-org/gitlab/-/issues/324649 - experiment_subject.save! if experiment_subject.changed? - - experiment_subject - end - - private - - def merged_context(experiment_subject, new_context) - experiment_subject.context.deep_merge(new_context.deep_stringify_keys) - end -end diff --git a/app/models/experiment_subject.rb b/app/models/experiment_subject.rb deleted file mode 100644 index 2a7b9017a51..00000000000 --- a/app/models/experiment_subject.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class ExperimentSubject < ApplicationRecord - include ::Gitlab::Experimentation::GroupTypes - - belongs_to :experiment, inverse_of: :experiment_subjects - belongs_to :user - belongs_to :namespace - belongs_to :project - - validates :experiment, presence: true - validates :variant, presence: true - validate :must_have_one_subject_present - - enum variant: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 } - - def self.valid_subject?(subject) - subject.is_a?(Namespace) || subject.is_a?(User) || subject.is_a?(Project) - end - - private - - def must_have_one_subject_present - if non_nil_subjects.length != 1 - errors.add(:base, s_("ExperimentSubject|Must have exactly one of User, Namespace, or Project.")) - end - end - - def non_nil_subjects - @non_nil_subjects ||= [user, namespace, project].reject(&:blank?) - end -end diff --git a/app/models/experiment_user.rb b/app/models/experiment_user.rb deleted file mode 100644 index e447becc1bd..00000000000 --- a/app/models/experiment_user.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class ExperimentUser < ApplicationRecord - include ::Gitlab::Experimentation::GroupTypes - - belongs_to :experiment - belongs_to :user - - enum group_type: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 } - - validates :experiment_id, presence: true - validates :user_id, presence: true - validates :group_type, presence: true -end diff --git a/app/models/group.rb b/app/models/group.rb index 38623d91705..098116ed800 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -119,6 +119,8 @@ class Group < Namespace has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id + has_many :protected_branches, inverse_of: :group + has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting' delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, :setup_for_company, :jobs_to_be_done, to: :namespace_settings @@ -465,9 +467,10 @@ class Group < Namespace end # Check if user is a last owner of the group. + # Excludes non-direct owners for top-level group # Excludes project_bots def last_owner?(user) - has_owner?(user) && all_owners_excluding_project_bots.size == 1 + has_owner?(user) && member_owners_excluding_project_bots.size == 1 end def member_last_owner?(member) @@ -476,8 +479,14 @@ class Group < Namespace last_owner?(member.user) end - def all_owners_excluding_project_bots - members_with_parents.owners.merge(User.without_project_bot) + # Excludes non-direct owners for top-level group + # Excludes project_bots + def member_owners_excluding_project_bots + if root? + members + else + members_with_parents + end.owners.merge(User.without_project_bot) end def single_blocked_owner? @@ -487,7 +496,7 @@ class Group < Namespace def member_last_blocked_owner?(member) return member.last_blocked_owner unless member.last_blocked_owner.nil? - return false if members_with_parents.owners.any? + return false if member_owners_excluding_project_bots.any? single_blocked_owner? && blocked_owners.exists?(user_id: member.user) end @@ -1010,10 +1019,6 @@ class Group < Namespace Arel::Nodes::SqlLiteral.new(column_alias)) end - def self.groups_including_descendants_by(group_ids) - Group.where(id: group_ids).self_and_descendants - end - def disable_shared_runners! update!( shared_runners_enabled: false, diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb index 283e2d680f4..cdcfd3f3ff5 100644 --- a/app/models/hooks/active_hook_filter.rb +++ b/app/models/hooks/active_hook_filter.rb @@ -3,14 +3,36 @@ class ActiveHookFilter def initialize(hook) @hook = hook - @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter) end def matches?(hooks_scope, data) - return true if hooks_scope != :push_hooks + return true unless hooks_scope == :push_hooks + + matches_branch?(data) + end + + private + + def matches_branch?(data) return true if @hook.push_events_branch_filter.blank? branch_name = Gitlab::Git.branch_name(data[:ref]) - @push_events_filter_matcher.matches?(branch_name) + + if Feature.disabled?(:enhanced_webhook_support_regex) + return RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name) + end + + case @hook.branch_filter_strategy + when 'all_branches' + true + when 'wildcard' + RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name) + when 'regex' + begin + Gitlab::UntrustedRegexp.new(@hook.push_events_branch_filter) === branch_name + rescue RegexpError + false + end + end end end diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index c0073f9a9b8..3c7f0ef9ffc 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -10,9 +10,9 @@ class SystemHook < WebHook :merge_request_hooks ] - default_value_for :push_events, false - default_value_for :repository_update_events, true - default_value_for :merge_requests_events, false + attribute :push_events, default: false + attribute :repository_update_events, default: true + attribute :merge_requests_events, default: false validates :url, system_hook_url: true diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 71794964c99..05e50c17988 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -7,9 +7,11 @@ class WebHook < ApplicationRecord MAX_FAILURES = 100 FAILURE_THRESHOLD = 3 # three strikes + EXCEEDED_FAILURE_THRESHOLD = FAILURE_THRESHOLD + 1 INITIAL_BACKOFF = 1.minute MAX_BACKOFF = 1.day BACKOFF_GROWTH_FACTOR = 2.0 + SECRET_MASK = '************' attr_encrypted :token, mode: :per_attribute_iv, @@ -33,14 +35,26 @@ class WebHook < ApplicationRecord has_many :web_hook_logs validates :url, presence: true - validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) } + validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) || hook.url_variables? } validates :token, format: { without: /\n/ } - validates :push_events_branch_filter, branch_filter: true + after_initialize :initialize_url_variables + before_validation :set_branch_filter_nil, \ + if: -> { branch_filter_strategy_all_branches? && enhanced_webhook_support_regex? } + validates :push_events_branch_filter, \ + untrusted_regexp: true, if: -> { branch_filter_strategy_regex? && enhanced_webhook_support_regex? } + validates :push_events_branch_filter, \ + "web_hooks/wildcard_branch_filter": true, if: -> { branch_filter_strategy_wildcard? } + validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' } validate :no_missing_url_variables + validates :interpolated_url, public_url: true, if: ->(hook) { hook.url_variables? && hook.errors.empty? } - after_initialize :initialize_url_variables + enum branch_filter_strategy: { + wildcard: 0, + regex: 1, + all_branches: 2 + }, _prefix: true scope :executable, -> do next all unless Feature.enabled?(:web_hooks_disable_failed) @@ -108,7 +122,7 @@ class WebHook < ApplicationRecord def disable! return if permanently_disabled? - update_attribute(:recent_failures, FAILURE_THRESHOLD + 1) + update_attribute(:recent_failures, EXCEEDED_FAILURE_THRESHOLD) end def enable! @@ -123,10 +137,10 @@ class WebHook < ApplicationRecord def backoff! return if permanently_disabled? || (backoff_count >= MAX_FAILURES && temporarily_disabled?) - attrs = { recent_failures: recent_failures + 1 } + attrs = { recent_failures: next_failure_count } if recent_failures >= FAILURE_THRESHOLD - attrs[:backoff_count] = backoff_count.succ.clamp(1, MAX_FAILURES) + attrs[:backoff_count] = next_backoff_count attrs[:disabled_until] = next_backoff.from_now end @@ -137,7 +151,7 @@ class WebHook < ApplicationRecord def failed! return unless recent_failures < MAX_FAILURES - assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: recent_failures + 1) + assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count) save(validate: false) end @@ -198,8 +212,20 @@ class WebHook < ApplicationRecord # Overridden in child classes. end + def masked_token + token.present? ? SECRET_MASK : nil + end + private + def next_failure_count + recent_failures.succ.clamp(1, MAX_FAILURES) + end + + def next_backoff_count + backoff_count.succ.clamp(1, MAX_FAILURES) + end + def web_hooks_disable_failed? self.class.web_hooks_disable_failed?(self) end @@ -224,4 +250,12 @@ class WebHook < ApplicationRecord errors.add(:url, "Invalid URL template. Missing keys: #{missing}") end + + def enhanced_webhook_support_regex? + Feature.enabled?(:enhanced_webhook_support_regex) + end + + def set_branch_filter_nil + self.push_events_branch_filter = nil + end end diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index c32957fbef9..2b26147b494 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -56,7 +56,7 @@ class WebHookLog < ApplicationRecord def redact_user_emails self.request_data.deep_transform_values! do |value| - value =~ URI::MailTo::EMAIL_REGEXP ? _('[REDACTED]') : value + value.to_s =~ URI::MailTo::EMAIL_REGEXP ? _('[REDACTED]') : value end end end diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb index 735d4e4298c..e70209f1538 100644 --- a/app/models/incident_management/timeline_event.rb +++ b/app/models/incident_management/timeline_event.rb @@ -18,6 +18,8 @@ module IncidentManagement validates :project, :incident, :occurred_at, presence: true validates :action, presence: true, length: { maximum: 128 } + # `user_input` is a note filled in by a user via API. Not auto generated by GitLab + validates :note, presence: true, length: { maximum: 280 }, on: :user_input validates :note, presence: true, length: { maximum: 10_000 } validates :note_html, length: { maximum: 10_000 } diff --git a/app/models/incident_management/timeline_event_tag.rb b/app/models/incident_management/timeline_event_tag.rb index cde3afcaa16..d1e3fbc2a6a 100644 --- a/app/models/incident_management/timeline_event_tag.rb +++ b/app/models/incident_management/timeline_event_tag.rb @@ -4,6 +4,9 @@ module IncidentManagement class TimelineEventTag < ApplicationRecord self.table_name = 'incident_management_timeline_event_tags' + START_TIME_TAG_NAME = 'Start time' + END_TIME_TAG_NAME = 'End time' + belongs_to :project, inverse_of: :incident_management_timeline_event_tags has_many :timeline_event_tag_links, @@ -14,7 +17,13 @@ module IncidentManagement through: :timeline_event_tag_links validates :name, presence: true, format: { with: /\A[^,]+\z/ } - validates :name, uniqueness: { scope: :project_id } + validates :name, uniqueness: { scope: :project_id, case_sensitive: false } validates :name, length: { maximum: 255 } + + scope :by_names, -> (tag_names) { where('lower(name) in (?)', tag_names.map(&:downcase)) } + + def self.pluck_names + pluck(:name) + end end end diff --git a/app/models/instance_metadata.rb b/app/models/instance_metadata.rb index 6cac78178e0..47460c85671 100644 --- a/app/models/instance_metadata.rb +++ b/app/models/instance_metadata.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true class InstanceMetadata - attr_reader :version, :revision, :kas + attr_reader :version, :revision, :kas, :enterprise - def initialize(version: Gitlab::VERSION, revision: Gitlab.revision) + def initialize(version: Gitlab::VERSION, revision: Gitlab.revision, enterprise: Gitlab.ee?) @version = version @revision = revision @kas = ::InstanceMetadata::Kas.new + @enterprise = enterprise end end diff --git a/app/models/integration.rb b/app/models/integration.rb index 23688a87cbd..41278dce22d 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -71,20 +71,20 @@ class Integration < ApplicationRecord alias_attribute :type, :type_new - default_value_for :active, false - default_value_for :alert_events, true - default_value_for :category, 'common' - default_value_for :commit_events, true - default_value_for :confidential_issues_events, true - default_value_for :confidential_note_events, true - default_value_for :issues_events, true - default_value_for :job_events, true - default_value_for :merge_requests_events, true - default_value_for :note_events, true - default_value_for :pipeline_events, true - default_value_for :push_events, true - default_value_for :tag_push_events, true - default_value_for :wiki_page_events, true + attribute :active, default: false + attribute :alert_events, default: true + attribute :category, default: 'common' + attribute :commit_events, default: true + attribute :confidential_issues_events, default: true + attribute :confidential_note_events, default: true + attribute :issues_events, default: true + attribute :job_events, default: true + attribute :merge_requests_events, default: true + attribute :note_events, default: true + attribute :pipeline_events, default: true + attribute :push_events, default: true + attribute :tag_push_events, default: true + attribute :wiki_page_events, default: true after_initialize :initialize_properties @@ -589,6 +589,10 @@ class Integration < ApplicationRecord false end + def chat? + category == :chat + end + private # Ancestors sorted by hierarchy depth in bottom-top order. diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb index 88dbf2915ef..536d5584bf6 100644 --- a/app/models/integrations/assembla.rb +++ b/app/models/integrations/assembla.rb @@ -12,6 +12,7 @@ module Integrations required: true field :subdomain, + exposes_secrets: true, placeholder: '' def title @@ -34,7 +35,9 @@ module Integrations return unless supported_events.include?(data[:object_kind]) url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}" - Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' }) + body = { payload: data } + + Gitlab::HTTP.post(url, body: Gitlab::Json.dump(body), headers: { 'Content-Type' => 'application/json' }) end end end diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index c3a4b84bb2d..b4e97f0871e 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -9,6 +9,7 @@ module Integrations title: -> { s_('BambooService|Bamboo URL') }, placeholder: -> { s_('https://bamboo.example.com') }, help: -> { s_('BambooService|Bamboo service root URL.') }, + exposes_secrets: true, required: true field :build_key, @@ -37,14 +38,6 @@ module Integrations attr_accessor :response - before_validation :reset_password - - def reset_password - if bamboo_url_changed? && !password_touched? - self.password = nil - end - end - def title s_('BambooService|Atlassian Bamboo') end diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index c7992e4083c..750aa60b185 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -22,7 +22,9 @@ module Integrations MATCH_ALL_LABELS = 'match_all' ].freeze - default_value_for :category, 'chat' + SECRET_MASK = '************' + + attribute :category, default: 'chat' prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior @@ -71,7 +73,7 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze, + { type: 'text', name: 'webhook', help: "#{webhook_help}", required: true }.freeze, { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze, { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze, { @@ -147,7 +149,7 @@ module Integrations raise NotImplementedError end - def webhook_placeholder + def webhook_help raise NotImplementedError end diff --git a/app/models/integrations/base_ci.rb b/app/models/integrations/base_ci.rb index 4f8732da703..db29f228e60 100644 --- a/app/models/integrations/base_ci.rb +++ b/app/models/integrations/base_ci.rb @@ -5,7 +5,7 @@ # working with GitLab merge requests module Integrations class BaseCi < Integration - default_value_for :category, 'ci' + attribute :category, default: 'ci' def valid_token?(token) self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token) diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb index a4cec5f927b..e0994305e9d 100644 --- a/app/models/integrations/base_issue_tracker.rb +++ b/app/models/integrations/base_issue_tracker.rb @@ -4,7 +4,7 @@ module Integrations class BaseIssueTracker < Integration validate :one_issue_tracker, if: :activated?, on: :manual_change - default_value_for :category, 'issue_tracker' + attribute :category, default: 'issue_tracker' before_validation :handle_properties before_validation :set_default_data, on: :create diff --git a/app/models/integrations/base_monitoring.rb b/app/models/integrations/base_monitoring.rb index 280eeda7c6c..b0bebb5a859 100644 --- a/app/models/integrations/base_monitoring.rb +++ b/app/models/integrations/base_monitoring.rb @@ -6,7 +6,7 @@ # to provide additional features for environments. module Integrations class BaseMonitoring < Integration - default_value_for :category, 'monitoring' + attribute :category, default: 'monitoring' def self.supported_events %w() diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb new file mode 100644 index 00000000000..cb785afdcfe --- /dev/null +++ b/app/models/integrations/base_slack_notification.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Integrations + class BaseSlackNotification < BaseChatNotification + SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[ + push issue confidential_issue merge_request note confidential_note tag_push wiki_page deployment + ].freeze + + prop_accessor EVENT_CHANNEL['alert'] + + override :default_channel_placeholder + def default_channel_placeholder + _('#general, #development') + end + + override :get_message + def get_message(object_kind, data) + return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert' + + super + end + + override :supported_events + def supported_events + additional = ['alert'] + + super + additional + end + + override :configurable_channels? + def configurable_channels? + true + end + + override :log_usage + def log_usage(event, user_id) + return unless user_id + + return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event) + + key = "i_ecosystem_slack_service_#{event}_notification" + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id) + + return unless Feature.enabled?(:route_hll_to_snowplow_phase2) + + optional_arguments = { + project: project, + namespace: group || project&.namespace + }.compact + + Gitlab::Tracking.event( + self.class.name, + Integration::SNOWPLOW_EVENT_ACTION, + label: Integration::SNOWPLOW_EVENT_LABEL, + property: key, + user: User.find(user_id), + **optional_arguments + ) + end + end +end diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb index e51d748b562..314f0a6ee5d 100644 --- a/app/models/integrations/base_slash_commands.rb +++ b/app/models/integrations/base_slash_commands.rb @@ -4,7 +4,7 @@ # This class is not meant to be used directly, but only to inherrit from. module Integrations class BaseSlashCommands < Integration - default_value_for :category, 'chat' + attribute :category, default: 'chat' prop_accessor :token diff --git a/app/models/integrations/base_third_party_wiki.rb b/app/models/integrations/base_third_party_wiki.rb index 24f5bec93cf..8df172e9a53 100644 --- a/app/models/integrations/base_third_party_wiki.rb +++ b/app/models/integrations/base_third_party_wiki.rb @@ -2,7 +2,7 @@ module Integrations class BaseThirdPartyWiki < Integration - default_value_for :category, 'third_party_wiki' + attribute :category, default: 'third_party_wiki' validate :only_one_third_party_wiki, if: :activated?, on: :manual_change diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index f2d2aca3ffe..5c08eac8557 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -6,13 +6,13 @@ module Integrations class Buildkite < BaseCi include HasWebHook include ReactivelyCached - extend Gitlab::Utils::Override ENDPOINT = "https://buildkite.com" field :project_url, title: -> { _('Pipeline URL') }, placeholder: "#{ENDPOINT}/example-org/test-pipeline", + exposes_secrets: true, required: true field :token, diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb index b3502905bf7..88db40bea7f 100644 --- a/app/models/integrations/chat_message/pipeline_message.rb +++ b/app/models/integrations/chat_message/pipeline_message.rb @@ -126,6 +126,14 @@ module Integrations } end + def pipeline_name_field + { + title: s_("ChatMessage|Pipeline name"), + value: pipeline.name, + short: false + } + end + def attachments_fields fields = [ { @@ -143,6 +151,7 @@ module Integrations fields << failed_stages_field if failed_stages.any? fields << failed_jobs_field if failed_jobs.any? fields << yaml_error_field if pipeline.has_yaml_errors? + fields << pipeline_name_field if Feature.enabled?(:pipeline_name, project) && pipeline.name.present? fields end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index ab0fdbd777f..27bed5d3f76 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -3,7 +3,6 @@ module Integrations class Datadog < Integration include HasWebHook - extend Gitlab::Utils::Override DEFAULT_DOMAIN = 'datadoghq.com' URL_TEMPLATE = 'https://webhook-intake.%{datadog_domain}/api/v2/webhook' @@ -91,7 +90,7 @@ module Integrations with_options if: :activated? do validates :api_key, presence: true, format: { with: /\A\w+\z/ } - validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true } + validates :datadog_site, format: { with: %r{\A\w+([-.]\w+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?\z}, allow_blank: true } validates :api_url, public_url: { allow_blank: true } validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? } validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? } @@ -169,8 +168,8 @@ module Integrations result = execute(data) { - success: (200..299).cover?(result[:http_status]), - result: result[:message] + success: (200..299).cover?(result.payload[:http_status]), + result: result.message } end diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index d0389b82410..061c491034d 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -10,8 +10,7 @@ module Integrations field :webhook, section: SECTION_TYPE_CONNECTION, - placeholder: 'https://discordapp.com/api/webhooks/…', - help: 'URL to the webhook for the Discord channel.', + help: 'e.g. https://discordapp.com/api/webhooks/…', required: true field :notify_only_broken_pipelines, diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index d1a64aa96d4..781acf65c47 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -6,7 +6,6 @@ module Integrations include PushDataValidations include ReactivelyCached prepend EnableSslVerification - extend Gitlab::Utils::Override DRONE_SAAS_HOSTNAME = 'cloud.drone.io' diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index 6e7f31aa030..c903e8d9eb8 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -22,10 +22,6 @@ module Integrations def default_channel_placeholder end - def webhook_placeholder - 'https://chat.googleapis.com/v1/spaces…' - end - def self.supported_events %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] @@ -33,7 +29,7 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" }, + { type: 'text', name: 'webhook', help: 'https://chat.googleapis.com/v1/spaces…' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index 74a6449f4f9..d2e8393ef95 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -5,10 +5,10 @@ module Integrations include HasWebHook prepend EnableSslVerification - extend Gitlab::Utils::Override field :jenkins_url, title: -> { s_('ProjectService|Jenkins server URL') }, + exposes_secrets: true, required: true, placeholder: 'http://jenkins.example.com', help: -> { s_('The URL of the Jenkins server.') } @@ -27,21 +27,13 @@ module Integrations non_empty_password_title: -> { s_('ProjectService|Enter new password.') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password.') } - before_validation :reset_password - validates :jenkins_url, presence: true, addressable_url: true, if: :activated? validates :project_name, presence: true, if: :activated? validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? } + validates :password, presence: true, if: ->(service) { service.activated? && service.username.present? } - default_value_for :merge_requests_events, false - default_value_for :tag_push_events, false - - def reset_password - # don't reset the password if a new one is provided - if (jenkins_url_changed? || username.blank?) && !password_touched? - self.password = nil - end - end + attribute :merge_requests_events, default: false + attribute :tag_push_events, default: false def execute(data) return unless supported_events.include?(data[:object_kind]) @@ -52,12 +44,12 @@ module Integrations def test(data) begin result = execute(data) - return { success: false, result: result[:message] } if result[:http_status] != 200 + return { success: false, result: result.message } if result.payload[:http_status] != 200 rescue StandardError => e return { success: false, result: e } end - { success: true, result: result[:message] } + { success: true, result: result.message } end override :hook_url diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index 3ca514ab1fd..30497c0110e 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -3,7 +3,6 @@ # Accessible as Project#external_issue_tracker module Integrations class Jira < BaseIssueTracker - extend ::Gitlab::Utils::Override include Gitlab::Routing include ApplicationHelper include ActionView::Helpers::AssetUrlHelper @@ -533,13 +532,14 @@ module Integrations end def build_entity_meta(entity) - if entity.is_a?(Commit) + case entity + when Commit { id: entity.short_id, description: entity.safe_message, branch: branch_name(entity) } - elsif entity.is_a?(MergeRequest) + when MergeRequest { id: entity.to_reference, branch: entity.source_branch diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb index dae11b99bc5..dd1c98ee06b 100644 --- a/app/models/integrations/mattermost.rb +++ b/app/models/integrations/mattermost.rb @@ -3,7 +3,6 @@ module Integrations class Mattermost < BaseChatNotification include SlackMattermostNotifier - extend ::Gitlab::Utils::Override def title s_('Mattermost notifications') @@ -26,7 +25,7 @@ module Integrations 'my-channel' end - def webhook_placeholder + def webhook_help 'http://mattermost.example.com/hooks/' end diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb index 69863f164cd..d6cbe5760e8 100644 --- a/app/models/integrations/microsoft_teams.rb +++ b/app/models/integrations/microsoft_teams.rb @@ -18,10 +18,6 @@ module Integrations '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>' end - def webhook_placeholder - 'https://outlook.office.com/webhook/…' - end - def default_channel_placeholder end @@ -32,7 +28,7 @@ module Integrations def default_fields [ - { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', required: true, placeholder: "#{webhook_placeholder}" }, + { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', help: 'https://outlook.office.com/webhook/…', required: true }, { type: 'checkbox', section: SECTION_TYPE_CONFIGURATION, diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb index 7177c82a167..7148de66aee 100644 --- a/app/models/integrations/packagist.rb +++ b/app/models/integrations/packagist.rb @@ -3,7 +3,6 @@ module Integrations class Packagist < Integration include HasWebHook - extend Gitlab::Utils::Override field :username, title: -> { s_('Username') }, @@ -55,12 +54,12 @@ module Integrations def test(data) begin result = execute(data) - return { success: false, result: result[:message] } if result[:http_status] != 202 + return { success: false, result: result.message } if result.payload[:http_status] != 202 rescue StandardError => e - return { success: false, result: e } + return { success: false, result: e.message } end - { success: true, result: result[:message] } + { success: true, result: result.message } end override :hook_url diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb index d32fb974339..1acdbbbf9bc 100644 --- a/app/models/integrations/pivotaltracker.rb +++ b/app/models/integrations/pivotaltracker.rb @@ -56,7 +56,7 @@ module Integrations } Gitlab::HTTP.post( API_ENDPOINT, - body: message.to_json, + body: Gitlab::Json.dump(message), headers: { 'Content-Type' => 'application/json', 'X-TrackerToken' => token diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb index 17026410eb1..e08dc6d0f51 100644 --- a/app/models/integrations/pumble.rb +++ b/app/models/integrations/pumble.rb @@ -36,7 +36,7 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', placeholder: "https://api.pumble.com/workspaces/x/...", required: true }, + { type: 'text', name: 'webhook', help: 'https://api.pumble.com/workspaces/x/...', required: true }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', @@ -51,7 +51,7 @@ module Integrations def notify(message, opts) header = { 'Content-Type' => 'application/json' } - response = Gitlab::HTTP.post(webhook, headers: header, body: { text: message.summary }.to_json) + response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump({ text: message.summary })) response if response.success? end diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb index c254ea379bb..89326b8174f 100644 --- a/app/models/integrations/slack.rb +++ b/app/models/integrations/slack.rb @@ -1,17 +1,8 @@ # frozen_string_literal: true module Integrations - class Slack < BaseChatNotification + class Slack < BaseSlackNotification include SlackMattermostNotifier - extend ::Gitlab::Utils::Override - - SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[ - push issue confidential_issue merge_request note confidential_note - tag_push wiki_page deployment - ].freeze - SNOWPLOW_EVENT_CATEGORY = self.name - - prop_accessor EVENT_CHANNEL['alert'] def title 'Slack notifications' @@ -25,57 +16,9 @@ module Integrations 'slack' end - def default_channel_placeholder - _('#general, #development') - end - - def webhook_placeholder + override :webhook_help + def webhook_help 'https://hooks.slack.com/services/…' end - - def supported_events - additional = [] - additional << 'alert' - - super + additional - end - - def get_message(object_kind, data) - return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert' - - super - end - - override :log_usage - def log_usage(event, user_id) - return unless user_id - - return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event) - - key = "i_ecosystem_slack_service_#{event}_notification" - - Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id) - - return unless Feature.enabled?(:route_hll_to_snowplow_phase2) - - optional_arguments = { - project: project, - namespace: group || project&.namespace - }.compact - - Gitlab::Tracking.event( - SNOWPLOW_EVENT_CATEGORY, - Integration::SNOWPLOW_EVENT_ACTION, - label: Integration::SNOWPLOW_EVENT_LABEL, - property: key, - user: User.find(user_id), - **optional_arguments - ) - end - - override :configurable_channels? - def configurable_channels? - true - end end end diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index ca7a715f4b3..af629d6ef1e 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -11,6 +11,7 @@ module Integrations field :teamcity_url, title: -> { s_('ProjectService|TeamCity server URL') }, placeholder: 'https://teamcity.example.com', + exposes_secrets: true, required: true field :build_type, @@ -36,8 +37,6 @@ module Integrations attr_accessor :response - before_validation :reset_password - class << self def to_param 'teamcity' @@ -48,12 +47,6 @@ module Integrations end end - def reset_password - if teamcity_url_changed? && !password_touched? - self.password = nil - end - end - def title 'JetBrains TeamCity' end diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index f10a75fac5d..aa19133b8c2 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -29,7 +29,7 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true }, + { type: 'text', name: 'webhook', help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…', required: true }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', @@ -43,11 +43,13 @@ module Integrations private def notify(message, opts) - response = Gitlab::HTTP.post(webhook, body: { + body = { subject: message.project_name, text: message.summary, markdown: true - }.to_json) + } + + response = Gitlab::HTTP.post(webhook, body: Gitlab::Json.dump(body)) response if response.success? end diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index 75be457dcf5..8e6f5ca6d17 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -29,7 +29,7 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true }, + { type: 'text', name: 'webhook', help: 'https://api.ciscospark.com/v1/webhooks/incoming/...', required: true }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', @@ -44,7 +44,7 @@ module Integrations def notify(message, opts) header = { 'Content-Type' => 'application/json' } - response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json) + response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump({ markdown: message.summary })) response if response.success? end diff --git a/app/models/issue.rb b/app/models/issue.rb index ea7acf9a5d1..fc083002c41 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -39,13 +39,14 @@ class Issue < ApplicationRecord DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze SORTING_PREFERENCE_FIELD = :issues_sort + MAX_BRANCH_TEMPLATE = 255 # Types of issues that should be displayed on issue lists across the app # for example, project issues list, group issues list, and issues dashboard. # # This should be kept consistent with the enums used for the GraphQL issue list query in # https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158 - TYPES_FOR_LIST = %w(issue incident test_case task).freeze + TYPES_FOR_LIST = %w(issue incident test_case task objective).freeze # Types of issues that should be displayed on issue board lists TYPES_FOR_BOARD_LIST = %w(issue incident).freeze @@ -90,6 +91,7 @@ class Issue < ApplicationRecord has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus' has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany + has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue has_many :prometheus_alerts, through: :prometheus_alert_events has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues @@ -210,6 +212,7 @@ class Issue < ApplicationRecord end scope :with_null_relative_position, -> { where(relative_position: nil) } scope :with_non_null_relative_position, -> { where.not(relative_position: nil) } + scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') } before_validation :ensure_namespace_id, :ensure_work_item_type @@ -270,9 +273,14 @@ class Issue < ApplicationRecord reorder(upvotes_count: :asc) end - override :pg_full_text_search - def pg_full_text_search(search_term) - super.where('issue_search_data.project_id = issues.project_id') + override :full_search + def full_search(query, matched_columns: nil, use_minimum_char_limit: true) + return super if query.match?(IssuableFinder::FULL_TEXT_SEARCH_TERM_REGEX) + + super.where( + 'issues.title NOT SIMILAR TO :pattern OR issues.description NOT SIMILAR TO :pattern', + pattern: IssuableFinder::FULL_TEXT_SEARCH_TERM_PATTERN + ) end end @@ -393,10 +401,21 @@ class Issue < ApplicationRecord ) end - def self.to_branch_name(*args) - branch_name = args.map(&:to_s).each_with_index.map do |arg, i| - arg.parameterize(preserve_case: i == 0).presence - end.compact.join('-') + def self.to_branch_name(id, title, project: nil) + params = { + 'id' => id.to_s.parameterize(preserve_case: true), + 'title' => title.to_s.parameterize + } + template = project&.issue_branch_template + + branch_name = + if template.present? + Gitlab::StringPlaceholderReplacer.replace_string_placeholders(template, /(#{params.keys.join('|')})/) do |arg| + params[arg] + end + else + params.values.select(&:present?).join('-') + end if branch_name.length > 100 truncated_string = branch_name[0, 100] @@ -474,7 +493,7 @@ class Issue < ApplicationRecord if self.confidential? "#{iid}-confidential-issue" else - self.class.to_branch_name(iid, title) + self.class.to_branch_name(iid, title, project: project) end end @@ -653,6 +672,10 @@ class Issue < ApplicationRecord Gitlab::EtagCaching::Store.new.touch(key) end + def supports_confidentiality? + true + end + private def due_date_after_start_date diff --git a/app/models/iteration.rb b/app/models/iteration.rb index ed73793c78f..c6269313d8b 100644 --- a/app/models/iteration.rb +++ b/app/models/iteration.rb @@ -4,9 +4,8 @@ class Iteration < ApplicationRecord include IgnorableColumns - # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372125 # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372126 - ignore_column :project_id, remove_with: '15.6', remove_after: '2022-09-17' + ignore_column :project_id, remove_with: '15.7', remove_after: '2022-11-18' self.table_name = 'sprints' diff --git a/app/models/label.rb b/app/models/label.rb index 483d51099b1..aa53c0e0f3f 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -14,8 +14,7 @@ class Label < ApplicationRecord DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc') - attribute :color, ::Gitlab::Database::Type::Color.new - default_value_for :color, DEFAULT_COLOR + attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :priorities, class_name: 'LabelPriority' diff --git a/app/models/member.rb b/app/models/member.rb index ff1d8f18c25..80c5fd7e468 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -286,7 +286,7 @@ class Member < ApplicationRecord refresh_member_authorized_projects(blocking: false) end - default_value_for :notification_level, NotificationSetting.levels[:global] + attribute :notification_level, default: -> { NotificationSetting.levels[:global] } class << self def search(query) diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 2b35f7da7b4..ad1ad1e74fe 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -13,7 +13,7 @@ class GroupMember < Member delegate :update_two_factor_requirement, to: :user, allow_nil: true # Make sure group member points only to group as it source - default_value_for :source_type, SOURCE_TYPE + attribute :source_type, default: SOURCE_TYPE validates :source_type, format: { with: SOURCE_TYPE_FORMAT } default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb index e411a0ef5eb..48c9bcb9a70 100644 --- a/app/models/members/last_group_owner_assigner.rb +++ b/app/models/members/last_group_owner_assigner.rb @@ -40,6 +40,6 @@ class LastGroupOwnerAssigner end def owners - @owners ||= group.all_owners_excluding_project_bots.load + @owners ||= group.member_owners_excluding_project_bots.load end end diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb index f093619ff36..6cf6b1adb45 100644 --- a/app/models/members/member_task.rb +++ b/app/models/members/member_task.rb @@ -34,9 +34,10 @@ class MemberTask < ApplicationRecord end def project_in_member_source - if member.is_a?(GroupMember) + case member + when GroupMember errors.add(:project, _('is not in the member group')) unless project.namespace == member.source - elsif member.is_a?(ProjectMember) + when ProjectMember errors.add(:project, _('is not the member project')) unless project == member.source end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 8fd82fcb34a..1099e0f48c0 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -10,7 +10,7 @@ class ProjectMember < Member delegate :namespace_id, to: :project # Make sure project member points only to project as it source - default_value_for :source_type, SOURCE_TYPE + attribute :source_type, default: SOURCE_TYPE validates :source_type, format: { with: SOURCE_TYPE_FORMAT } default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index fb20d91fa20..735c0df1529 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -136,7 +136,7 @@ class MergeRequest < ApplicationRecord before_validation :set_draft_status - after_create :ensure_merge_request_diff + after_create :ensure_merge_request_diff, unless: :skip_ensure_merge_request_diff after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed after_commit :ensure_metrics, on: [:create, :update], unless: :importing? @@ -146,6 +146,10 @@ class MergeRequest < ApplicationRecord # It allows us to close or modify broken merge requests attr_accessor :allow_broken + # Temporary flag to skip merge_request_diff creation on create. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100390 + attr_accessor :skip_ensure_merge_request_diff + # Temporary fields to store compare vars # when creating new merge request attr_accessor :can_be_created, :compare_commits, :diff_options, :compare @@ -242,9 +246,7 @@ class MergeRequest < ApplicationRecord end after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition| - if Feature.enabled?(:trigger_mr_subscription_on_merge_status_change, merge_request.project) - GraphqlTriggers.merge_request_merge_status_updated(merge_request) - end + GraphqlTriggers.merge_request_merge_status_updated(merge_request) end # rubocop: disable CodeReuse/ServiceClass @@ -649,8 +651,8 @@ class MergeRequest < ApplicationRecord context_commits.count end - def commits(limit: nil, load_from_gitaly: false) - return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly) if merge_request_diff.persisted? + def commits(limit: nil, load_from_gitaly: false, page: nil) + return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page) if merge_request_diff.persisted? commits_arr = if compare_commits reversed_commits = compare_commits.reverse @@ -662,8 +664,8 @@ class MergeRequest < ApplicationRecord CommitCollection.new(source_project, commits_arr, source_branch) end - def recent_commits(load_from_gitaly: false) - commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: load_from_gitaly) + def recent_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: false, page: nil) + commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page) end def commits_count @@ -1130,7 +1132,7 @@ class MergeRequest < ApplicationRecord # rubocop: enable CodeReuse/ServiceClass def diffable_merge_ref? - open? && merge_head_diff.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?) + open? && merge_head_diff.present? && can_be_merged? end # Returns boolean indicating the merge_status should be rechecked in order to @@ -1673,7 +1675,7 @@ class MergeRequest < ApplicationRecord # TODO: consider renaming this as with exposed artifacts we generate reports, # not always compare # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 - def compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {} ) + def compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {}) with_reactive_cache(service_class.name, current_user&.id, report_type) do |data| unless service_class.new(project, current_user, id: id, report_type: report_type, additional_params: additional_params) .latest?(comparison_base_pipeline(service_class.name), actual_head_pipeline, data) diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb index be3a1d42eac..3e481e35deb 100644 --- a/app/models/merge_request_assignee.rb +++ b/app/models/merge_request_assignee.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class MergeRequestAssignee < ApplicationRecord - include IgnorableColumns - ignore_column %i[state updated_state_by_user_id], remove_with: '15.6', remove_after: '2022-10-22' - belongs_to :merge_request, touch: true belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 9f7e98dc04b..98a9ccc2040 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -292,9 +292,9 @@ class MergeRequestDiff < ApplicationRecord end end - def commits(limit: nil, load_from_gitaly: false) - strong_memoize(:"commits_#{limit || 'all'}_#{load_from_gitaly}") do - load_commits(limit: limit, load_from_gitaly: load_from_gitaly) + def commits(limit: nil, load_from_gitaly: false, page: nil) + strong_memoize(:"commits_#{limit || 'all'}_#{load_from_gitaly}_page_#{page}") do + load_commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page) end end @@ -725,17 +725,19 @@ class MergeRequestDiff < ApplicationRecord end end - def load_commits(limit: nil, load_from_gitaly: false) + def load_commits(limit: nil, load_from_gitaly: false, page: nil) + diff_commits = page.present? ? merge_request_diff_commits.page(page).per(limit) : merge_request_diff_commits.limit(limit) + if load_from_gitaly - commits = Gitlab::Git::Commit.batch_by_oid(repository, merge_request_diff_commits.limit(limit).map(&:sha)) + commits = Gitlab::Git::Commit.batch_by_oid(repository, diff_commits.map(&:sha)) commits = Commit.decorate(commits, project) else - commits = merge_request_diff_commits.with_users.limit(limit) + commits = diff_commits.with_users .map { |commit| Commit.from_hash(commit.to_hash, project) } end CommitCollection - .new(merge_request.target_project, commits, merge_request.target_branch) + .new(merge_request.target_project, commits, merge_request.target_branch, page: page.to_i, per_page: limit, count: commits_count) end def save_diffs diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 66f1e45fd49..152fb195c97 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -70,7 +70,7 @@ class MergeRequestDiffCommit < ApplicationRecord sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]), committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]), - trailers: commit_hash.fetch(:trailers, {}).to_json + trailers: Gitlab::Json.dump(commit_hash.fetch(:trailers, {})) ) end diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb index 04b322ef5a6..5a98131a6fd 100644 --- a/app/models/merge_request_diff_file.rb +++ b/app/models/merge_request_diff_file.rb @@ -15,12 +15,7 @@ class MergeRequestDiffFile < ApplicationRecord end def utf8_diff - fetched_diff = if Feature.enabled?(:externally_stored_diffs_caching_export) && - merge_request_diff&.stored_externally? - diff_export - else - diff - end + fetched_diff = merge_request_diff&.stored_externally? ? diff_export : diff return '' if fetched_diff.blank? diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb index 4b5b71481d3..e1e2805cd8f 100644 --- a/app/models/merge_request_reviewer.rb +++ b/app/models/merge_request_reviewer.rb @@ -2,8 +2,7 @@ class MergeRequestReviewer < ApplicationRecord include MergeRequestReviewerState - include IgnorableColumns - ignore_column :updated_state_by_user_id, remove_with: '15.6', remove_after: '2022-10-22' + include BulkInsertSafe # must be included _last_ i.e. after any other concerns belongs_to :merge_request belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index 29e1ba88528..f7da4418624 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -11,8 +11,15 @@ module Ml belongs_to :user has_many :metrics, class_name: 'Ml::CandidateMetric' has_many :params, class_name: 'Ml::CandidateParam' + has_many :latest_metrics, -> { latest }, class_name: 'Ml::CandidateMetric', inverse_of: :candidate - default_value_for(:iid) { SecureRandom.uuid } + attribute :iid, default: -> { SecureRandom.uuid } + + scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) } + + def artifact_root + "/ml_candidate_#{iid}/-/" + end class << self def with_project_id_and_iid(project_id, iid) diff --git a/app/models/ml/candidate_metric.rb b/app/models/ml/candidate_metric.rb index e03a8b83ee6..8e13a46d6b4 100644 --- a/app/models/ml/candidate_metric.rb +++ b/app/models/ml/candidate_metric.rb @@ -6,5 +6,7 @@ module Ml validates :name, length: { maximum: 250 }, presence: true belongs_to :candidate, class_name: 'Ml::Candidate' + + scope :latest, -> { select('DISTINCT ON (candidate_id, name) *').order('candidate_id, name, id DESC') } end end diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index a32099e8a0c..05b238b960d 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -23,7 +23,7 @@ module Ml end def by_project_id(project_id) - where(project_id: project_id) + where(project_id: project_id).order(id: :desc) end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 42f362876bb..51c39ad4ec3 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -40,9 +40,9 @@ class Namespace < ApplicationRecord PATH_TRAILING_VIOLATIONS = %w[.git .atom .].freeze - # The first date in https://docs.gitlab.com/ee/user/usage_quotas.html#namespace-storage-limit-enforcement-schedule - # Determines when we start enforcing namespace storage - MIN_STORAGE_ENFORCEMENT_DATE = Date.new(2022, 10, 19) + # This date is just a placeholder until namespace storage enforcement timeline is confirmed at which point + # this should be replaced, see https://about.gitlab.com/pricing/faq-efficient-free-tier/#user-limits-on-gitlab-saas-free-tier + MIN_STORAGE_ENFORCEMENT_DATE = 3.months.from_now.to_date # https://gitlab.com/gitlab-org/gitlab/-/issues/367531 MIN_STORAGE_ENFORCEMENT_USAGE = 5.gigabytes @@ -91,6 +91,7 @@ class Namespace < ApplicationRecord validates :name, presence: true, length: { maximum: 255 } + validates :name, uniqueness: { scope: [:type, :parent_id] }, if: -> { parent_id.present? } validates :description, length: { maximum: 255 } @@ -550,11 +551,12 @@ class Namespace < ApplicationRecord end def shared_runners_setting_higher_than?(other_setting) - if other_setting == SR_ENABLED + case other_setting + when SR_ENABLED false - elsif other_setting == SR_DISABLED_WITH_OVERRIDE + when SR_DISABLED_WITH_OVERRIDE shared_runners_setting == SR_ENABLED - elsif other_setting == SR_DISABLED_AND_UNOVERRIDABLE + when SR_DISABLED_AND_UNOVERRIDABLE shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE else raise ArgumentError diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 6a87fba57ac..3e6371b0c4d 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -4,11 +4,6 @@ class NamespaceSetting < ApplicationRecord include CascadingNamespaceSettingAttribute include Sanitizable include ChronicDurationAttribute - include IgnorableColumns - - ignore_columns %i[exclude_from_free_user_cap include_for_free_user_cap_preview], - remove_with: '15.5', - remove_after: '2022-09-23' cascading_attr :delayed_project_removal diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index a034d97a6bb..7ffcb8b9219 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -23,6 +23,8 @@ module Network protected def collect_notes + return {} if Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment) + h = Hash.new(0) @project diff --git a/app/models/note.rb b/app/models/note.rb index e444111119b..8e1f4979602 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -60,7 +60,7 @@ class Note < ApplicationRecord # Attribute used to determine whether keep_around_commits will be skipped for diff notes. attr_accessor :skip_keep_around_commits - default_value_for :system, false + attribute :system, default: false attr_mentionable :note, pipeline: :note participant :author @@ -361,14 +361,6 @@ class Note < ApplicationRecord super(noteable_type.to_s.classify.constantize.base_class.to_s) end - def noteable_assignee_or_author?(user) - return false unless user - return false unless noteable.respond_to?(:author_id) - return noteable.assignee_or_author?(user) if [MergeRequest, Issue].include?(noteable.class) - - noteable.author_id == user.id - end - def contributor? project&.team&.contributor?(self.author_id) end @@ -756,7 +748,8 @@ class Note < ApplicationRecord if user_visible_reference_count.present? && total_reference_count.present? # if they are not equal, then there are private/confidential references as well - user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count + total_reference_count == 0 || + user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count else refs = all_references(user) refs.all.any? && refs.all_visible? diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 2e45753c182..cde7b92e74a 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -3,9 +3,7 @@ class NotificationSetting < ApplicationRecord include FromUnion - enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 } - - default_value_for :level, NotificationSetting.levels[:global] + enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }, _default: :global belongs_to :user belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index eac99e8d441..8e79a750793 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -31,8 +31,6 @@ class OauthAccessToken < Doorkeeper::AccessToken # have `reuse_access_tokens` disabled and we also hash tokens. # This ensures we don't accidentally return a hashed token value. def self.matching_token_for(application, resource_owner, scopes) - return if Feature.enabled?(:hash_oauth_tokens) - - super + # no-op end end diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index e36c59366fe..0df8c87f73f 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -16,8 +16,8 @@ module Operations has_internal_id :iid, scope: :project - default_value_for :active, true - default_value_for :version, :new_version_flag + attribute :active, default: true + attribute :version, default: :new_version_flag # strategies exists only for the second version has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy' diff --git a/app/models/packages/go/module_version.rb b/app/models/packages/go/module_version.rb index c442b2416f1..5869a03e081 100644 --- a/app/models/packages/go/module_version.rb +++ b/app/models/packages/go/module_version.rb @@ -21,9 +21,10 @@ module Packages raise ArgumentError, "mod is required" unless mod raise ArgumentError, "commit is required" unless commit - if type == :ref + case type + when :ref raise ArgumentError, "ref is required" unless ref - elsif type == :pseudo + when :pseudo raise ArgumentError, "name is required" unless name raise ArgumentError, "semver is required" unless semver end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 16d5492a65e..328c67a0711 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -10,8 +10,8 @@ class PagesDomain < ApplicationRecord SSL_RENEWAL_THRESHOLD = 30.days.freeze enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate - enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope - enum usage: { pages: 0, serverless: 1 }, _prefix: :usage + enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project + enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages belongs_to :project has_many :acme_orders, class_name: "PagesDomainAcmeOrder" @@ -35,10 +35,8 @@ class PagesDomain < ApplicationRecord validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? } validate :validate_custom_domain_count_per_project, on: :create - default_value_for(:auto_ssl_enabled, allows_nil: false) { ::Gitlab::LetsEncrypt.enabled? } - default_value_for :scope, allows_nil: false, value: :project - default_value_for :wildcard, allows_nil: false, value: false - default_value_for :usage, allows_nil: false, value: :pages + attribute :auto_ssl_enabled, default: -> { ::Gitlab::LetsEncrypt.enabled? } + attribute :wildcard, default: false attr_encrypted :key, mode: :per_attribute_iv_and_salt, @@ -50,7 +48,7 @@ class PagesDomain < ApplicationRecord scope :for_project, ->(project) { where(project: project) } - scope :enabled, -> { where('enabled_until >= ?', Time.current ) } + scope :enabled, -> { where('enabled_until >= ?', Time.current) } scope :needs_verification, -> do verified_at = arel_table[:verified_at] enabled_until = arel_table[:enabled_until] diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index f0ed1822da6..3126dba9d6d 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -11,8 +11,6 @@ class PersonalAccessToken < ApplicationRecord add_authentication_token_field :token, digest: true - REDIS_EXPIRY_TIME = 3.minutes - # PATs are 20 characters + optional configurable settings prefix (0..20) TOKEN_LENGTH_RANGE = (20..40).freeze @@ -34,8 +32,6 @@ class PersonalAccessToken < ApplicationRecord scope :for_user, -> (user) { where(user: user) } scope :for_users, -> (users) { where(user: users) } scope :preload_users, -> { preload(:user) } - scope :order_expires_at_asc, -> { reorder(expires_at: :asc) } - scope :order_expires_at_desc, -> { reorder(expires_at: :desc) } scope :order_expires_at_asc_id_desc, -> { reorder(expires_at: :asc, id: :desc) } scope :project_access_token, -> { includes(:user).where(user: { user_type: :project_bot }) } scope :owner_is_human, -> { includes(:user).where(user: { user_type: :human }) } @@ -55,35 +51,10 @@ class PersonalAccessToken < ApplicationRecord !revoked? && !expired? end - def self.redis_getdel(user_id) - Gitlab::Redis::SharedState.with do |redis| - redis_key = redis_shared_state_key(user_id) - encrypted_token = redis.get(redis_key) - redis.del(redis_key) - - begin - Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) - rescue StandardError => e - logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{e.class}" - encrypted_token - end - end - end - - def self.redis_store!(user_id, token) - encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) - - Gitlab::Redis::SharedState.with do |redis| - redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME) - end - end - override :simple_sorts def self.simple_sorts super.merge( { - 'expires_at_asc' => -> { order_expires_at_asc }, - 'expires_at_desc' => -> { order_expires_at_desc }, 'expires_at_asc_id_desc' => -> { order_expires_at_asc_id_desc } } ) @@ -121,10 +92,6 @@ class PersonalAccessToken < ApplicationRecord self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? end - - def self.redis_shared_state_key(user_id) - "gitlab:personal_access_token:#{user_id}" - end end PersonalAccessToken.prepend_mod_with('PersonalAccessToken') diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb index 12b48895e0c..b0dd52c9657 100644 --- a/app/models/postgresql/detached_partition.rb +++ b/app/models/postgresql/detached_partition.rb @@ -3,5 +3,9 @@ module Postgresql class DetachedPartition < ::Gitlab::Database::SharedModel scope :ready_to_drop, -> { where('drop_after < ?', Time.current) } + + def fully_qualified_table_name + "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}" + end end end diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb index 1e935249407..6192f79ce2c 100644 --- a/app/models/preloaders/project_root_ancestor_preloader.rb +++ b/app/models/preloaders/project_root_ancestor_preloader.rb @@ -9,7 +9,7 @@ module Preloaders end def execute - return if @projects.is_a?(ActiveRecord::NullRelation) + return unless @projects.is_a?(ActiveRecord::Relation) return unless ::Feature.enabled?(:use_traversal_ids) root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id") diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb index 2e2272a2ef5..c9fd5e7718a 100644 --- a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb +++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb @@ -7,9 +7,11 @@ module Preloaders def initialize(projects, user) @projects = if projects.is_a?(Array) Project.where(id: projects) - else + elsif Feature.enabled?(:projects_preloader_fix) # Push projects base query in to a sub-select to avoid # table name clashes. Performs better than aliasing. + Project.where(id: projects.subquery(:id)) + else Project.where(id: projects.reselect(:id)) end @@ -17,6 +19,8 @@ module Preloaders end def execute + return unless @user + project_authorizations = ProjectAuthorization.arel_table auths = @projects diff --git a/app/models/project.rb b/app/models/project.rb index 7b61010ab01..a07d4147228 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -39,6 +39,7 @@ class Project < ApplicationRecord include BulkUsersByEmailLoad include RunnerTokenExpirationInterval include BlocksUnsafeSerialization + include Subquery extend Gitlab::Cache::RequestCache extend Gitlab::Utils::Override @@ -222,6 +223,7 @@ class Project < ApplicationRecord has_one :youtrack_integration, class_name: 'Integrations::Youtrack' has_one :zentao_integration, class_name: 'Integrations::Zentao' + has_one :wiki_repository, class_name: 'Projects::WikiRepository', inverse_of: :project has_one :root_of_fork_network, foreign_key: 'root_project_id', inverse_of: :root_project, @@ -451,7 +453,7 @@ class Project < ApplicationRecord :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, - :monitor_access_level, :releases_access_level, + :monitor_access_level, :releases_access_level, :infrastructure_access_level, to: :project_feature, allow_nil: true delegate :show_default_award_emojis, :show_default_award_emojis=, @@ -491,6 +493,7 @@ class Project < ApplicationRecord to: :project_setting delegate :merge_commit_template, :merge_commit_template=, to: :project_setting, allow_nil: true delegate :squash_commit_template, :squash_commit_template=, to: :project_setting, allow_nil: true + delegate :issue_branch_template, :issue_branch_template=, to: :project_setting, allow_nil: true delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage @@ -1616,7 +1619,7 @@ class Project < ApplicationRecord end def all_clusters - group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } ) + group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto }) instance_clusters = Clusters::Cluster.instance_type Clusters::Cluster.from_union([clusters, group_clusters, instance_clusters]) @@ -1705,7 +1708,11 @@ class Project < ApplicationRecord end def has_active_integrations?(hooks_scope = :push_hooks) - integrations.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend + @has_active_integrations ||= {} # rubocop: disable Gitlab/PredicateMemoization + + return @has_active_integrations[hooks_scope] if @has_active_integrations.key?(hooks_scope) + + @has_active_integrations[hooks_scope] = integrations.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend end def feature_usage @@ -2729,11 +2736,6 @@ class Project < ApplicationRecord ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] end - # DO NOT USE. This method will be deprecated soon - def uses_external_project_ci_config? - !!(ci_config_path =~ %r{@.+/.+}) - end - def limited_protected_branches(limit) protected_branches.limit(limit) end @@ -2784,7 +2786,7 @@ class Project < ApplicationRecord return unless service_desk_enabled? config = Gitlab.config.incoming_email - wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER + wildcard = Gitlab::Email::Common::WILDCARD_PLACEHOLDER config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}") end @@ -2854,11 +2856,6 @@ class Project < ApplicationRecord repository.gitlab_ci_yml_for(sha, ci_config_path_or_default) end - # DO NOT USE. This method will be deprecated soon - def ci_config_external_project - Project.find_by_full_path(ci_config_path.split('@', 2).last) - end - def enabled_group_deploy_keys return GroupDeployKey.none unless group @@ -2927,10 +2924,6 @@ class Project < ApplicationRecord ci_cd_settings.keep_latest_artifact? end - def runner_token_expiration_interval - ci_cd_settings&.runner_token_expiration_interval - end - def group_runners_enabled? return false unless ci_cd_settings @@ -3006,7 +2999,7 @@ class Project < ApplicationRecord end def work_items_create_from_markdown_feature_flag_enabled? - work_items_feature_flag_enabled? && (group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown)) + group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown) end def enqueue_record_project_target_platforms diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index 8b43e5e5d63..3623b3be20d 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -31,6 +31,7 @@ class ProjectAuthorization < ApplicationRecord def self.insert_all_in_batches(attributes, per_batch = BATCH_SIZE) add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: per_batch) + log_details(entire_size: attributes.size) if add_delay attributes.each_slice(per_batch) do |attributes_batch| insert_all(attributes_batch) @@ -40,6 +41,7 @@ class ProjectAuthorization < ApplicationRecord def self.delete_all_in_batches_for_project(project:, user_ids:, per_batch: BATCH_SIZE) add_delay = add_delay_between_batches?(entire_size: user_ids.size, batch_size: per_batch) + log_details(entire_size: user_ids.size) if add_delay user_ids.each_slice(per_batch) do |user_ids_batch| project.project_authorizations.where(user_id: user_ids_batch).delete_all @@ -49,6 +51,7 @@ class ProjectAuthorization < ApplicationRecord def self.delete_all_in_batches_for_user(user:, project_ids:, per_batch: BATCH_SIZE) add_delay = add_delay_between_batches?(entire_size: project_ids.size, batch_size: per_batch) + log_details(entire_size: project_ids.size) if add_delay project_ids.each_slice(per_batch) do |project_ids_batch| user.project_authorizations.where(project_id: project_ids_batch).delete_all @@ -65,6 +68,13 @@ class ProjectAuthorization < ApplicationRecord Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh) end + private_class_method def self.log_details(entire_size:) + Gitlab::AppLogger.info( + entire_size: entire_size, + message: 'Project authorizations refresh performed with delay' + ) + end + private_class_method def self.perform_delay sleep(SLEEP_DELAY) end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index d7a5d0d9d84..cc9003423be 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -17,8 +17,8 @@ class ProjectCiCdSetting < ApplicationRecord }, allow_nil: true - default_value_for :forward_deployment_enabled, true - default_value_for :separated_caches, true + attribute :forward_deployment_enabled, default: true + attribute :separated_caches, default: true chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index dad8aaf0625..11f4a3f3b6f 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -25,6 +25,7 @@ class ProjectFeature < ApplicationRecord environments feature_flags releases + infrastructure ].freeze EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 6d40544fad4..7116ccd9824 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -2,6 +2,7 @@ class ProjectSetting < ApplicationRecord include ::Gitlab::Utils::StrongMemoize + include EachBatch ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze @@ -20,12 +21,13 @@ class ProjectSetting < ApplicationRecord validates :merge_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH } validates :squash_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH } + validates :issue_branch_template, length: { maximum: Issue::MAX_BRANCH_TEMPLATE } validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS } validates :suggested_reviewers_enabled, inclusion: { in: [true, false] } validate :validates_mr_default_target_self - default_value_for(:legacy_open_source_license_available) do + attribute :legacy_open_source_license_available, default: -> do Feature.enabled?(:legacy_open_source_license_available, type: :ops) end @@ -57,7 +59,7 @@ class ProjectSetting < ApplicationRecord !!super end end - strong_memoize_attr :show_diff_preview_in_email + strong_memoize_attr :show_diff_preview_in_email?, :show_diff_preview_in_email private diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index f108e43015e..0570be85ad1 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -7,8 +7,8 @@ class ProjectStatistics < ApplicationRecord belongs_to :project belongs_to :namespace - default_value_for :wiki_size, 0 - default_value_for :snippets_size, 0 + attribute :wiki_size, default: 0 + attribute :snippets_size, default: 0 counter_attribute :build_artifacts_size @@ -95,8 +95,7 @@ class ProjectStatistics < ApplicationRecord # and the column can be nil. # This means that, when the columns were added, all rows had nil # values on them. - # Therefore, any call to any of those methods will return nil instead - # of 0, because `default_value_for` works with new records, not existing ones. + # Therefore, any call to any of those methods will return nil instead of 0. # # These two methods provide consistency and avoid returning nil. def wiki_size diff --git a/app/models/projects/import_export/relation_export.rb b/app/models/projects/import_export/relation_export.rb index 15198049f87..9bdf10d7c0e 100644 --- a/app/models/projects/import_export/relation_export.rb +++ b/app/models/projects/import_export/relation_export.rb @@ -34,11 +34,18 @@ module Projects scope :by_relation, -> (relation) { where(relation: relation) } + STATUS = { + queued: 0, + started: 1, + finished: 2, + failed: 3 + }.freeze + state_machine :status, initial: :queued do - state :queued, value: 0 - state :started, value: 1 - state :finished, value: 2 - state :failed, value: 3 + state :queued, value: STATUS[:queued] + state :started, value: STATUS[:started] + state :finished, value: STATUS[:finished] + state :failed, value: STATUS[:failed] event :start do transition queued: :started diff --git a/app/models/projects/wiki_repository.rb b/app/models/projects/wiki_repository.rb new file mode 100644 index 00000000000..88382adbcb7 --- /dev/null +++ b/app/models/projects/wiki_repository.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Projects + class WikiRepository < ApplicationRecord + self.table_name = :project_wiki_repositories + + belongs_to :project, inverse_of: :wiki_repository + + validates :project, presence: true, uniqueness: true + end +end + +Projects::WikiRepository.prepend_mod_with('Projects::WikiRepository') diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index dfd5c315f6e..80967c1b072 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -4,6 +4,10 @@ class ProtectedBranch < ApplicationRecord include ProtectedRef include Gitlab::SQL::Pattern + belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches + + validate :validate_either_project_or_top_group + scope :requiring_code_owner_approval, -> { where(code_owner_approval_required: true) } @@ -99,6 +103,18 @@ class ProtectedBranch < ApplicationRecord def default_branch? name == project.default_branch end + + private + + def validate_either_project_or_top_group + if !project && !group + errors.add(:base, _('must be associated with a Group or a Project')) + elsif project && group + errors.add(:base, _('cannot be associated with both a Group and a Project')) + elsif group && group.root_ancestor != group + errors.add(:base, _('cannot be associated with a subgroup')) + end + end end ProtectedBranch.prepend_mod_with('ProtectedBranch') diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index 5b2467daddc..e89cb3aabc7 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -4,6 +4,7 @@ class ProtectedTag < ApplicationRecord include ProtectedRef validates :name, uniqueness: { scope: :project_id } + validates :project, presence: true protected_ref_access_levels :create diff --git a/app/models/repository.rb b/app/models/repository.rb index 3413b3e3424..95d1b815e74 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -98,6 +98,10 @@ class Repository alias_method :raw, :raw_repository + def flipper_id + raw_repository.flipper_id + end + # Don't use this! It's going away. Use Gitaly to read or write from repos. def path_to_repo @path_to_repo ||= @@ -1232,7 +1236,8 @@ class Repository Gitlab::Git::Repository.new(shard, disk_path + '.git', repo_type.identifier_for_container(container), - container.full_path) + container.full_path, + container: container) end end diff --git a/app/models/serverless/domain_cluster.rb b/app/models/serverless/domain_cluster.rb index 1effabf1c22..561bfc65b2b 100644 --- a/app/models/serverless/domain_cluster.rb +++ b/app/models/serverless/domain_cluster.rb @@ -19,7 +19,7 @@ module Serverless validates :uuid, presence: true, uniqueness: true, length: { is: ::Serverless::Domain::UUID_LENGTH }, format: { with: HEX_REGEXP, message: 'only allows hex characters' } - default_value_for(:uuid, allows_nil: false) { ::Serverless::Domain.generate_uuid } + after_initialize :set_uuid, if: :new_record? delegate :domain, to: :pages_domain delegate :cluster, to: :knative @@ -29,5 +29,11 @@ module Serverless .includes(:pages_domain, :knative) .find_by(uuid: uuid) end + + private + + def set_uuid + self.uuid = ::Serverless::Domain.generate_uuid + end end end diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index e5c8f4ab32a..8a207c891e2 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -28,7 +28,7 @@ module Terraform validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, format: { with: HEX_REGEXP, message: 'only allows hex characters' } - default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) } + attribute :uuid, default: -> { SecureRandom.hex(UUID_LENGTH / 2) } def latest_file latest_version&.file diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb index c50eaa66860..d6a16ad5b99 100644 --- a/app/models/terraform/state_version.rb +++ b/app/models/terraform/state_version.rb @@ -13,7 +13,7 @@ module Terraform scope :with_files_stored_locally, -> { where(file_store: Terraform::StateUploader::Store::LOCAL) } scope :preload_state, -> { includes(:terraform_state) } - default_value_for(:file_store) { StateUploader.default_store } + attribute :file_store, default: -> { StateUploader.default_store } mount_file_store_uploader StateUploader diff --git a/app/models/time_tracking/timelog_category.rb b/app/models/time_tracking/timelog_category.rb index 26614f6fc44..246e78f31cb 100644 --- a/app/models/time_tracking/timelog_category.rb +++ b/app/models/time_tracking/timelog_category.rb @@ -24,8 +24,7 @@ module TimeTracking DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc') - attribute :color, ::Gitlab::Database::Type::Color.new - default_value_for :color, DEFAULT_COLOR + attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR def self.find_by_name(namespace_id, name) where(namespace: namespace_id) diff --git a/app/models/todo.rb b/app/models/todo.rb index 634fa9e7eda..f2fa0df852a 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -94,7 +94,7 @@ class Todo < ApplicationRecord # # Returns an `ActiveRecord::Relation`. def for_group_ids_and_descendants(group_ids) - groups = Group.groups_including_descendants_by(group_ids) + groups = Group.where(id: group_ids).self_and_descendants from_union( [ diff --git a/app/models/user.rb b/app/models/user.rb index 6d198fc755b..24f947183a2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -83,7 +83,10 @@ class User < ApplicationRecord serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize devise :lockable, :recoverable, :rememberable, :trackable, - :validatable, :omniauthable, :confirmable, :registerable, :pbkdf2_encryptable + :validatable, :omniauthable, :confirmable, :registerable + + # Must be included after `devise` + include EncryptedUserPassword include AdminChangedPasswordNotifier @@ -185,7 +188,7 @@ class User < ApplicationRecord has_many :personal_projects, through: :namespace, source: :projects has_many :project_members, -> { where(requested_at: nil) } has_many :projects, through: :project_members - has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' + has_many :created_projects, foreign_key: :creator_id, class_name: 'Project', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :projects_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :project_members, source: :project has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :starred_projects, through: :users_star_projects, source: :project @@ -258,6 +261,8 @@ class User < ApplicationRecord has_many :resource_state_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :authored_events, class_name: 'Event', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent + has_many :namespace_commit_emails + # # Validations # @@ -420,10 +425,6 @@ class User < ApplicationRecord end # rubocop: disable CodeReuse/ServiceClass - # Ideally we should not call a service object here but user.block - # is also called by Users::MigrateToGhostUserService which references - # this state transition object in order to do a rollback. - # For this reason the tradeoff is to disable this cop. after_transition any => :blocked do |user| user.run_after_commit do Ci::DropPipelineService.new.execute_async_for_all(user.pipelines, :user_blocked, user) @@ -447,6 +448,14 @@ class User < ApplicationRecord after_transition banned: :active do |user| user.banned_user&.destroy end + + after_transition any => :active do |user| + user.starred_projects.update_counters(star_count: 1) + end + + after_transition active: any do |user| + user.starred_projects.update_counters(star_count: -1) + end end # Scopes @@ -693,6 +702,8 @@ class User < ApplicationRecord # # Returns an ActiveRecord::Relation. def search(query, **options) + return none unless query.is_a?(String) + query = query&.delete_prefix('@') return none if query.blank? @@ -937,26 +948,14 @@ class User < ApplicationRecord reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end - def authenticatable_salt - return encrypted_password[0, 29] unless Feature.enabled?(:pbkdf2_password_encryption) - return super if password_strategy == :pbkdf2_sha512 - - encrypted_password[0, 29] - end - # Overwrites valid_password? from Devise::Models::DatabaseAuthenticatable # In constant-time, check both that the password isn't on a denylist AND # that the password is the user's password def valid_password?(password) return false unless password_allowed?(password) return false if password_automatically_set? - return super if Feature.enabled?(:pbkdf2_password_encryption) - Devise::Encryptor.compare(self.class, encrypted_password, password) - rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash - validate_and_migrate_bcrypt_password(password) - rescue ::BCrypt::Errors::InvalidHash - false + super end def generate_otp_backup_codes! @@ -975,27 +974,6 @@ class User < ApplicationRecord end end - # This method should be removed once the :pbkdf2_password_encryption feature flag is removed. - def password=(new_password) - if Feature.enabled?(:pbkdf2_password_encryption) && Feature.enabled?(:pbkdf2_password_encryption_write, self) - super - else - # Copied from Devise DatabaseAuthenticatable. - @password = new_password - self.encrypted_password = Devise::Encryptor.digest(self.class, new_password) if new_password.present? - end - end - - def password_strategy - super - rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash - begin - return :bcrypt if BCrypt::Password.new(encrypted_password) - rescue BCrypt::Errors::InvalidHash - :unknown - end - end - # See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638 DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze @@ -1224,6 +1202,10 @@ class User < ApplicationRecord authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled end + def preloaded_member_roles_for_projects(projects) + # overridden in EE + end + # rubocop: disable CodeReuse/ServiceClass def require_ssh_key? count = Users::KeysCountService.new(self).count @@ -1786,7 +1768,7 @@ class User < ApplicationRecord end def owns_runner?(runner) - ci_owned_runners.exists?(runner.id) + ci_owned_runners.include?(runner) end def notification_email_for(notification_group) @@ -2440,15 +2422,6 @@ class User < ApplicationRecord Ci::NamespaceMirror.contains_traversal_ids(traversal_ids) end - - def validate_and_migrate_bcrypt_password(password) - return false unless Devise::Encryptor.compare(self.class, encrypted_password, password) - return true unless Feature.enabled?(:pbkdf2_password_encryption_write, self) - - update_attribute(:password, password) - rescue ::BCrypt::Errors::InvalidHash - false - end end User.prepend_mod_with('User') diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index ae6950d800c..b037d07658d 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -62,7 +62,8 @@ module Users namespace_storage_limit_banner_error_threshold: 58, # EE-only project_quality_summary_feedback: 59, # EE-only merge_request_settings_moved_callout: 60, - new_top_level_group_alert: 61 + new_top_level_group_alert: 61, + artifacts_management_page_feedback_banner: 62 } validates :feature_name, diff --git a/app/models/users/ghost_user_migration.rb b/app/models/users/ghost_user_migration.rb index 1d93498e88b..4578e0503c3 100644 --- a/app/models/users/ghost_user_migration.rb +++ b/app/models/users/ghost_user_migration.rb @@ -8,5 +8,7 @@ module Users belongs_to :initiator_user, class_name: 'User' validates :user_id, presence: true + + scope :consume_order, -> { order(:consume_after, :id) } end end diff --git a/app/models/users/namespace_commit_email.rb b/app/models/users/namespace_commit_email.rb new file mode 100644 index 00000000000..4ec02f12717 --- /dev/null +++ b/app/models/users/namespace_commit_email.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Users + class NamespaceCommitEmail < ApplicationRecord + belongs_to :user + belongs_to :namespace + belongs_to :email + + validates :user, presence: true + validates :namespace, presence: true + validates :email, presence: true + validates :user_id, uniqueness: { scope: [:namespace_id] } + end +end diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 9a514b82506..6cffc97822d 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -3,7 +3,7 @@ class UsersStarProject < ApplicationRecord include Sortable - belongs_to :project, counter_cache: :star_count + belongs_to :project belongs_to :user validates :user, presence: true @@ -12,6 +12,10 @@ class UsersStarProject < ApplicationRecord alias_attribute :starred_since, :created_at + after_create :increment_project_star_count + after_destroy :decrement_project_star_count + + scope :with_active_user, -> { joins(:user).merge(User.with_state(:active)) } scope :order_user_name_asc, -> { joins(:user).merge(User.order_name_asc) } scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) } scope :by_project, -> (project) { where(project_id: project.id) } @@ -35,4 +39,14 @@ class UsersStarProject < ApplicationRecord joins(:user).merge(User.search(query, use_minimum_char_limit: false)) end end + + private + + def increment_project_star_count + Project.update_counters(project, star_count: 1) if user.active? + end + + def decrement_project_star_count + Project.update_counters(project, star_count: -1) if user.active? + end end diff --git a/app/models/wiki.rb b/app/models/wiki.rb index b718c3a096f..57488749b76 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -190,7 +190,7 @@ class Wiki end def empty? - !repository_exists? || list_page_paths.empty? + !repository_exists? || list_page_paths(limit: 1).empty? end def exists? @@ -207,9 +207,29 @@ class Wiki # # Returns an Array of GitLab WikiPage instances or an # empty Array if this Wiki has no pages. - def list_pages(limit: 0, direction: DIRECTION_ASC, load_content: false) + def list_pages(direction: DIRECTION_ASC, load_content: false, limit: 0, offset: 0) create_wiki_repository unless repository_exists? - list_pages_with_repository_rpcs(limit: limit, direction: direction, load_content: load_content) + + paths = list_page_paths(limit: limit, offset: offset) + return [] if paths.empty? + + pages = paths.map do |path| + page = Gitlab::Git::WikiPage.new( + url_path: sluggified_title(strip_extension(path)), + title: canonicalize_filename(path), + format: find_page_format(path), + path: sluggified_title(path), + raw_data: '', + name: canonicalize_filename(path), + historical: false + ) + WikiPage.new(self, page) + end + sort_pages!(pages, direction) + pages = pages.take(limit) if limit > 0 + fetch_pages_content!(pages) if load_content + + pages end def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options) @@ -229,7 +249,27 @@ class Wiki # Returns an initialized WikiPage instance or nil def find_page(title, version = nil, load_content: true) create_wiki_repository unless repository_exists? - find_page_with_repository_rpcs(title, version, load_content: load_content) + + version = version.presence || default_branch + path = find_matched_file(title, version) + return if path.blank? + + blob_options = load_content ? {} : { limit: 0 } + blob = repository.blob_at(version, path, **blob_options) + commit = repository.commit(blob.commit_id) + format = find_page_format(path) + + page = Gitlab::Git::WikiPage.new( + url_path: sluggified_title(strip_extension(path)), + title: canonicalize_filename(path), + format: format, + path: sluggified_title(path), + raw_data: blob.data, + name: canonicalize_filename(path), + historical: version == default_branch ? false : check_page_historical(path, commit), + version: Gitlab::Git::WikiPageVersion.new(commit, format) + ) + WikiPage.new(self, page) end def find_sidebar(version = nil) @@ -315,12 +355,6 @@ class Wiki [title, title_array.join("/")] end - # TODO: This method is redundant. Should be replaced by create_wiki_repository - def ensure_repository - create_wiki_repository - raise CouldNotCreateWikiError unless repository_exists? - end - def hook_attrs { web_url: web_url, @@ -457,7 +491,7 @@ class Wiki escaped_path = RE2::Regexp.escape(sluggified_title(title)) path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{file_extension_regexp})$") - matched_files = repository.search_files_by_regexp(path_regexp, version) + matched_files = repository.search_files_by_regexp(path_regexp, version, limit: 1) return if matched_files.blank? Gitlab::EncodingHelper.encode_utf8_no_detect(matched_files.first) @@ -472,29 +506,6 @@ class Wiki repository.last_commit_for_path(default_branch, path)&.id != commit&.id end - def find_page_with_repository_rpcs(title, version, load_content: true) - version = version.presence || default_branch - path = find_matched_file(title, version) - return if path.blank? - - blob_options = load_content ? {} : { limit: 0 } - blob = repository.blob_at(version, path, **blob_options) - commit = repository.commit(blob.commit_id) - format = find_page_format(path) - - page = Gitlab::Git::WikiPage.new( - url_path: sluggified_title(strip_extension(path)), - title: canonicalize_filename(path), - format: format, - path: sluggified_title(path), - raw_data: blob.data, - name: canonicalize_filename(path), - historical: version == default_branch ? false : check_page_historical(path, commit), - version: Gitlab::Git::WikiPageVersion.new(commit, format) - ) - WikiPage.new(self, page) - end - def file_extension_regexp # We could not use ALLOWED_EXTENSIONS_REGEX constant or similar regexp with # Regexp.union. The result combination complicated modifiers: @@ -509,34 +520,11 @@ class Wiki path.sub(/\.[^.]+\z/, "") end - def list_page_paths + def list_page_paths(limit: 0, offset: 0) return [] if repository.empty? path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)\\.(#{file_extension_regexp})$") - repository.search_files_by_regexp(path_regexp, default_branch) - end - - def list_pages_with_repository_rpcs(limit:, direction:, load_content:) - paths = list_page_paths - return [] if paths.empty? - - pages = paths.map do |path| - page = Gitlab::Git::WikiPage.new( - url_path: sluggified_title(strip_extension(path)), - title: canonicalize_filename(path), - format: find_page_format(path), - path: sluggified_title(path), - raw_data: '', - name: canonicalize_filename(path), - historical: false - ) - WikiPage.new(self, page) - end - sort_pages!(pages, direction) - pages = pages.take(limit) if limit > 0 - fetch_pages_content!(pages) if load_content - - pages + repository.search_files_by_regexp(path_regexp, default_branch, limit: limit, offset: offset) end # After migrating to normal repository RPCs, it's very expensive to sort the @@ -552,7 +540,7 @@ class Wiki def fetch_pages_content!(pages) blobs = repository - .blobs_at(pages.map { |page| [default_branch, page.path] } ) + .blobs_at(pages.map { |page| [default_branch, page.path] }) .to_h { |blob| [blob.path, blob.data] } pages.each do |page| diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 05e45fa5b29..ed6f9d161a6 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -16,8 +16,14 @@ class WorkItem < Issue scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) } - def self.assignee_association_name - 'issue' + class << self + def assignee_association_name + 'issue' + end + + def test_reports_join_column + 'issues.id' + end end def noteable_target_type_name diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index 753fcbcb8f9..dc30899d24f 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -12,20 +12,27 @@ module WorkItems # Base types need to exist on the DB on app startup # This constant is used by the DB seeder + # TODO - where to add new icon names created? BASE_TYPES = { issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 }, incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 }, test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only - task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 } + task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }, + objective: { name: 'Objective', icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only + key_result: { name: 'Key Result', icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only }.freeze WIDGETS_FOR_TYPE = { - issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate], + issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate, + Widgets::Milestone], incident: [Widgets::Description, Widgets::Hierarchy], test_case: [Widgets::Description], requirement: [Widgets::Description], - task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate] + task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate, + Widgets::Milestone], + objective: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::Milestone], + key_result: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::StartAndDueDate] }.freeze WI_TYPES_WITH_CREATED_HEADER = %w[issue incident].freeze @@ -67,7 +74,7 @@ module WorkItems end def self.allowed_types_for_issues - base_types.keys.excluding('task') + base_types.keys.excluding('task', 'objective', 'key_result') end def default? diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb index 930aced8ace..d0819076efd 100644 --- a/app/models/work_items/widgets/hierarchy.rb +++ b/app/models/work_items/widgets/hierarchy.rb @@ -4,14 +4,10 @@ module WorkItems module Widgets class Hierarchy < Base def parent - return unless work_item.project.work_items_feature_flag_enabled? - work_item.work_item_parent end def children - return WorkItem.none unless work_item.project.work_items_feature_flag_enabled? - work_item.work_item_children end end diff --git a/app/models/work_items/widgets/milestone.rb b/app/models/work_items/widgets/milestone.rb new file mode 100644 index 00000000000..a3e610110f1 --- /dev/null +++ b/app/models/work_items/widgets/milestone.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module WorkItems + module Widgets + class Milestone < Base + delegate :milestone, to: :work_item + end + end +end |