diff options
Diffstat (limited to 'app/models')
225 files changed, 1352 insertions, 1062 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb index 4da4d113a7f..d8510524c1f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -46,6 +46,7 @@ class Ability issues.select { |issue| issue.visible_to_user?(user) } end end + alias_method :work_items_readable_by_user, :issues_readable_by_user # Returns an Array of MergeRequests that can be read by the given user. # diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 75c90d370c3..bf25c539830 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -61,10 +61,11 @@ class AbuseReport < ApplicationRecord validates :screenshot, file_size: { maximum: MAX_FILE_SIZE } validate :validate_screenshot_is_image - scope :by_user_id, ->(id) { where(user_id: id) } - scope :by_reporter_id, ->(id) { where(reporter_id: id) } + scope :by_user_id, ->(user_id) { where(user_id: user_id) } + scope :by_reporter_id, ->(reporter_id) { where(reporter_id: reporter_id) } scope :by_category, ->(category) { where(category: category) } scope :with_users, -> { includes(:reporter, :user) } + scope :with_labels, -> { includes(:labels) } enum category: { spam: 1, @@ -141,8 +142,14 @@ class AbuseReport < ApplicationRecord end end - def other_reports_for_user - user.abuse_reports.id_not_in(id) + def past_closed_reports_for_user + user.abuse_reports.closed.id_not_in(id) + end + + def similar_open_reports_for_user + return AbuseReport.none unless open? + + user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter) end private diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 7d025fb7738..e42f9eeef23 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -102,17 +102,16 @@ class ActiveSession # set marketing cookie when user has active session def self.set_active_user_cookie(auth) - auth.cookies[:about_gitlab_active_user] = + expiration_time = 2.weeks.from_now + + auth.cookies[:gitlab_user] = { value: true, - domain: Gitlab.config.gitlab.host + domain: Gitlab.config.gitlab.host, + expires: expiration_time } end - def self.unset_active_user_cookie(auth) - auth.cookies.delete :about_gitlab_active_user - end - def self.list(user) Gitlab::Redis::Sessions.with do |redis| cleaned_up_lookup_entries(redis, user).map do |raw_session| diff --git a/app/models/alerting/project_alerting_setting.rb b/app/models/alerting/project_alerting_setting.rb index 34fa27eb29b..7e94d41137f 100644 --- a/app/models/alerting/project_alerting_setting.rb +++ b/app/models/alerting/project_alerting_setting.rb @@ -14,6 +14,8 @@ module Alerting algorithm: 'aes-256-gcm' before_validation :ensure_token + after_create :create_http_integration + after_update :sync_http_integration private @@ -24,5 +26,31 @@ module Alerting def generate_token SecureRandom.hex end + + # Remove in next required stop after %16.4 + # https://gitlab.com/gitlab-org/gitlab/-/issues/338838 + def sync_http_integration + project.alert_management_http_integrations + .for_endpoint_identifier('legacy-prometheus') + .take + &.update_columns( + encrypted_token: encrypted_token, + encrypted_token_iv: encrypted_token_iv + ) + end + + # Remove in next required stop after %16.4 + # https://gitlab.com/gitlab-org/gitlab/-/issues/338838 + def create_http_integration + AlertManagement::HttpIntegration.insert({ + project_id: project_id, + encrypted_token: encrypted_token, + encrypted_token_iv: encrypted_token_iv, + active: true, + name: 'Prometheus', + endpoint_identifier: 'legacy-prometheus', + type_identifier: :prometheus + }) + end end end diff --git a/app/models/analytics/cycle_analytics/runtime_limiter.rb b/app/models/analytics/cycle_analytics/runtime_limiter.rb new file mode 100644 index 00000000000..063377c3ddb --- /dev/null +++ b/app/models/analytics/cycle_analytics/runtime_limiter.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class RuntimeLimiter + delegate :monotonic_time, to: :'Gitlab::Metrics::System' + + DEFAULT_MAX_RUNTIME = 200.seconds + + attr_reader :max_runtime, :start_time + + def initialize(max_runtime = DEFAULT_MAX_RUNTIME) + @start_time = monotonic_time + @max_runtime = max_runtime + end + + def elapsed_time + monotonic_time - start_time + end + + def over_time? + @last_check = elapsed_time >= max_runtime + end + + def was_over_time? + !!@last_check + end + end + end +end diff --git a/app/models/analytics/cycle_analytics/stage_event_hash.rb b/app/models/analytics/cycle_analytics/stage_event_hash.rb index 6443a970945..7dcabd01ebf 100644 --- a/app/models/analytics/cycle_analytics/stage_event_hash.rb +++ b/app/models/analytics/cycle_analytics/stage_event_hash.rb @@ -13,7 +13,7 @@ module Analytics # Atomic, safe insert without retrying query = <<~SQL - WITH insert_cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} ( + WITH insert_cte AS MATERIALIZED ( INSERT INTO #{quoted_table_name} (hash_sha256) VALUES (#{casted_hash_code}) ON CONFLICT DO NOTHING RETURNING ID ) SELECT ids.id FROM ( diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f67efaf4f58..153257636ba 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -14,38 +14,30 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord ignore_column :send_user_confirmation_email, remove_with: '15.8', remove_after: '2022-12-18' ignore_column :web_ide_clientside_preview_enabled, remove_with: '15.11', remove_after: '2023-04-22' ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22' - ignore_columns %i[ - encrypted_tofa_access_token_expires_in - encrypted_tofa_access_token_expires_in_iv - encrypted_tofa_client_library_args - encrypted_tofa_client_library_args_iv - encrypted_tofa_client_library_class - encrypted_tofa_client_library_class_iv - encrypted_tofa_client_library_create_credentials_method - encrypted_tofa_client_library_create_credentials_method_iv - encrypted_tofa_client_library_fetch_access_token_method - encrypted_tofa_client_library_fetch_access_token_method_iv - encrypted_tofa_credentials - encrypted_tofa_credentials_iv - encrypted_tofa_host - encrypted_tofa_host_iv - encrypted_tofa_request_json_keys - encrypted_tofa_request_json_keys_iv - encrypted_tofa_request_payload - encrypted_tofa_request_payload_iv - encrypted_tofa_response_json_keys - encrypted_tofa_response_json_keys_iv - encrypted_tofa_url - encrypted_tofa_url_iv - vertex_project - ], remove_with: '16.3', remove_after: '2023-07-22' ignore_column :database_apdex_settings, remove_with: '16.4', remove_after: '2023-08-22' + ignore_columns %i[ dashboard_notification_limit dashboard_enforcement_limit dashboard_limit_new_namespace_creation_enforcement_date ], remove_with: '16.5', remove_after: '2023-08-22' + ignore_column %i[ + relay_state_domain_allowlist + in_product_marketing_emails_enabled + ], remove_with: '16.6', remove_after: '2023-10-22' + + ignore_columns %i[ + encrypted_product_analytics_clickhouse_connection_string + encrypted_product_analytics_clickhouse_connection_string_iv + encrypted_jitsu_administrator_password + encrypted_jitsu_administrator_password_iv + jitsu_host + jitsu_project_xid + jitsu_administrator_email + ], remove_with: '16.5', remove_after: '2023-09-22' + ignore_columns %i[ai_access_token ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22' + INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ 'Admin Area > Settings > Metrics and profiling > Metrics - Grafana' @@ -244,6 +236,11 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord hostname: true, if: :snowplow_enabled + validates :snowplow_database_collector_hostname, + allow_blank: true, + hostname: true, + length: { maximum: 255 } + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -300,6 +297,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :decompress_archive_file_timeout, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :repository_storages, presence: true validate :check_repository_storages validate :check_repository_storages_weighted @@ -310,7 +311,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord if: :auto_devops_enabled? validates :enabled_git_access_protocol, - inclusion: { in: %w(ssh http), allow_blank: true } + inclusion: { in: %w[ssh http], allow_blank: true } validates :domain_denylist, presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' }, @@ -551,7 +552,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord if: :external_authorization_service_enabled validates :spam_check_endpoint_url, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ schemes: %w(tls grpc) }), allow_blank: true + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ schemes: %w[tls grpc] }), allow_blank: true validates :spam_check_endpoint_url, presence: true, @@ -666,6 +667,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :gitlab_shell_operation_limit end + validates :search_rate_limit_allowlist, + length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, + allow_nil: false + validates :notes_create_limit_allowlist, length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, allow_nil: false @@ -794,18 +799,20 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord attr_encrypted :arkose_labs_public_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) 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) - attr_encrypted :product_analytics_clickhouse_connection_string, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :product_analytics_configurator_connection_string, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :openai_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :anthropic_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) - attr_encrypted :ai_access_token, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :vertex_ai_credentials, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) + # Restricting the validation to `on: :update` only to avoid cyclical dependencies with + # License <--> ApplicationSetting. This method calls a license check when we create + # ApplicationSetting from defaults which in turn depends on ApplicationSetting record. + # The currect default is defined in the `defaults` method so we don't need to validate + # it here. validates :disable_feed_token, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') }, on: :update validates :disable_admin_oauth_scopes, inclusion: { in: [true, false], message: N_('must be a boolean value') } @@ -962,7 +969,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord end def parsed_kroki_url - @parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w(http https), enforce_sanitization: true)[0] + @parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w[http https], enforce_sanitization: true)[0] rescue Gitlab::UrlBlocker::BlockedUrlError => e self.errors.add( :kroki_url, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index f6bf535158a..5a90e246499 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -9,12 +9,12 @@ module ApplicationSettingImplementation \s # any whitespace character | # or [\r\n] # any number of newline characters - }x.freeze + }x # Setting a key restriction to `-1` means that all keys of this type are # forbidden. FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN - VALID_RUNNER_REGISTRAR_TYPES = %w(project group).freeze + VALID_RUNNER_REGISTRAR_TYPES = %w[project group].freeze DEFAULT_PROTECTED_PATHS = [ '/users/password', @@ -37,7 +37,6 @@ module ApplicationSettingImplementation { admin_mode: false, after_sign_up_text: nil, - ai_access_token: nil, akismet_enabled: false, akismet_api_key: nil, allow_local_requests_from_system_hooks: true, @@ -53,6 +52,7 @@ module ApplicationSettingImplementation container_registry_vendor: '', container_registry_version: '', custom_http_clone_url_root: nil, + decompress_archive_file_timeout: 210, default_artifacts_expire_in: '30 days', default_branch_name: nil, default_branch_protection: Settings.gitlab['default_branch_protection'], @@ -171,6 +171,7 @@ module ApplicationSettingImplementation snowplow_app_id: nil, snowplow_collector_hostname: nil, snowplow_cookie_domain: nil, + snowplow_database_collector_hostname: nil, snowplow_enabled: false, sourcegraph_enabled: false, sourcegraph_public_only: true, @@ -254,6 +255,7 @@ module ApplicationSettingImplementation user_deactivation_emails_enabled: true, search_rate_limit: 30, search_rate_limit_unauthenticated: 10, + search_rate_limit_allowlist: [], users_get_by_id_limit: 300, users_get_by_id_limit_allowlist: [], can_create_group: true, @@ -380,6 +382,14 @@ module ApplicationSettingImplementation self.protected_paths = strings_to_array(values) end + def protected_paths_for_get_request_raw + array_to_string(protected_paths_for_get_request) + end + + def protected_paths_for_get_request_raw=(values) + self.protected_paths_for_get_request = strings_to_array(values) + end + def notes_create_limit_allowlist_raw array_to_string(notes_create_limit_allowlist) end @@ -396,6 +406,14 @@ module ApplicationSettingImplementation self.users_get_by_id_limit_allowlist = strings_to_array(values).map(&:downcase) end + def search_rate_limit_allowlist_raw + array_to_string(search_rate_limit_allowlist) + end + + def search_rate_limit_allowlist_raw=(values) + self.search_rate_limit_allowlist = strings_to_array(values).map(&:downcase) + end + def asset_proxy_whitelist=(values) values = strings_to_array(values) if values.is_a?(String) diff --git a/app/models/approval.rb b/app/models/approval.rb index 9ded44fe425..ecc15077c8d 100644 --- a/app/models/approval.rb +++ b/app/models/approval.rb @@ -3,10 +3,13 @@ class Approval < ApplicationRecord include CreatedAtFilterable include Importable + include ShaAttribute belongs_to :user belongs_to :merge_request + sha_attribute :patch_id_sha + validates :merge_request_id, presence: true, unless: :importing? validates :user_id, presence: true, uniqueness: { scope: [:merge_request_id] } diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index ebc43b04b1b..73e3fa709b0 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -78,7 +78,7 @@ class AwardEmoji < ApplicationRecord end def broadcast_note_update - awardable.expire_etag_cache + awardable.broadcast_noteable_notes_changed awardable.trigger_note_subscription_update end diff --git a/app/models/badge.rb b/app/models/badge.rb index 23e6f305c32..f4e719887ba 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -18,7 +18,7 @@ class Badge < ApplicationRecord # This regex is built dynamically using the keys from the PLACEHOLDER struct. # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash. # This regex will build the new PLACEHOLDER_REGEX with the new information - PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze + PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/ default_scope { order_created_at_asc } # rubocop:disable Cop/DefaultScope diff --git a/app/models/blob_viewer/binary_stl.rb b/app/models/blob_viewer/binary_stl.rb index 425f72decae..6ccf75200e5 100644 --- a/app/models/blob_viewer/binary_stl.rb +++ b/app/models/blob_viewer/binary_stl.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'stl' - self.extensions = %w(stl) + self.extensions = %w[stl] self.binary = true end end diff --git a/app/models/blob_viewer/cargo_toml.rb b/app/models/blob_viewer/cargo_toml.rb index 2f1ebd25b4f..eb2a6f4433d 100644 --- a/app/models/blob_viewer/cargo_toml.rb +++ b/app/models/blob_viewer/cargo_toml.rb @@ -4,7 +4,7 @@ module BlobViewer class CargoToml < DependencyManager include Static - self.file_types = %i(cargo_toml) + self.file_types = %i[cargo_toml] def manager_name 'Cargo' diff --git a/app/models/blob_viewer/cartfile.rb b/app/models/blob_viewer/cartfile.rb index ea0494033bf..58fc97a9ffc 100644 --- a/app/models/blob_viewer/cartfile.rb +++ b/app/models/blob_viewer/cartfile.rb @@ -4,7 +4,7 @@ module BlobViewer class Cartfile < DependencyManager include Static - self.file_types = %i(cartfile) + self.file_types = %i[cartfile] def manager_name 'Carthage' diff --git a/app/models/blob_viewer/changelog.rb b/app/models/blob_viewer/changelog.rb index 8810bd25809..7992fbf542c 100644 --- a/app/models/blob_viewer/changelog.rb +++ b/app/models/blob_viewer/changelog.rb @@ -6,7 +6,7 @@ module BlobViewer include Static self.partial_name = 'changelog' - self.file_types = %i(changelog) + self.file_types = %i[changelog] self.binary = false def render_error diff --git a/app/models/blob_viewer/composer_json.rb b/app/models/blob_viewer/composer_json.rb index aac7271242e..3449780f50f 100644 --- a/app/models/blob_viewer/composer_json.rb +++ b/app/models/blob_viewer/composer_json.rb @@ -4,7 +4,7 @@ module BlobViewer class ComposerJson < DependencyManager include ServerSide - self.file_types = %i(composer_json) + self.file_types = %i[composer_json] def manager_name 'Composer' diff --git a/app/models/blob_viewer/contributing.rb b/app/models/blob_viewer/contributing.rb index fa224309e31..524104f176a 100644 --- a/app/models/blob_viewer/contributing.rb +++ b/app/models/blob_viewer/contributing.rb @@ -6,7 +6,7 @@ module BlobViewer include Static self.partial_name = 'contributing' - self.file_types = %i(contributing) + self.file_types = %i[contributing] self.binary = false end end diff --git a/app/models/blob_viewer/csv.rb b/app/models/blob_viewer/csv.rb index 633e3bd63d8..97fa890653d 100644 --- a/app/models/blob_viewer/csv.rb +++ b/app/models/blob_viewer/csv.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.binary = false - self.extensions = %w(csv) + self.extensions = %w[csv] self.partial_name = 'csv' self.switcher_icon = 'table' end diff --git a/app/models/blob_viewer/gemfile.rb b/app/models/blob_viewer/gemfile.rb index 77220cdbd08..84edacb32bd 100644 --- a/app/models/blob_viewer/gemfile.rb +++ b/app/models/blob_viewer/gemfile.rb @@ -4,7 +4,7 @@ module BlobViewer class Gemfile < DependencyManager include Static - self.file_types = %i(gemfile gemfile_lock) + self.file_types = %i[gemfile gemfile_lock] def manager_name 'Bundler' diff --git a/app/models/blob_viewer/gemspec.rb b/app/models/blob_viewer/gemspec.rb index 274859a7710..645458467f4 100644 --- a/app/models/blob_viewer/gemspec.rb +++ b/app/models/blob_viewer/gemspec.rb @@ -4,7 +4,7 @@ module BlobViewer class Gemspec < DependencyManager include ServerSide - self.file_types = %i(gemspec) + self.file_types = %i[gemspec] def manager_name 'RubyGems' diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index e255b6d15d2..9cee536d15b 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -7,7 +7,7 @@ module BlobViewer self.partial_name = 'gitlab_ci_yml' self.loading_partial_name = 'gitlab_ci_yml_loading' - self.file_types = %i(gitlab_ci) + self.file_types = %i[gitlab_ci] self.binary = false def validation_message(opts) diff --git a/app/models/blob_viewer/go_mod.rb b/app/models/blob_viewer/go_mod.rb index d4d117f899c..eebf057c6dc 100644 --- a/app/models/blob_viewer/go_mod.rb +++ b/app/models/blob_viewer/go_mod.rb @@ -11,9 +11,9 @@ module BlobViewer (?<name>.*?) (?# module name) \s*(?://.*)? (?# comment) (?:\n|\z) (?# newline or end of file) - }x.freeze + }x - self.file_types = %i(go_mod go_sum) + self.file_types = %i[go_mod go_sum] def manager_name 'Go Modules' diff --git a/app/models/blob_viewer/godeps_json.rb b/app/models/blob_viewer/godeps_json.rb index 743c759aea5..37a133848a0 100644 --- a/app/models/blob_viewer/godeps_json.rb +++ b/app/models/blob_viewer/godeps_json.rb @@ -4,7 +4,7 @@ module BlobViewer class GodepsJson < DependencyManager include Static - self.file_types = %i(godeps_json) + self.file_types = %i[godeps_json] def manager_name 'godep' diff --git a/app/models/blob_viewer/license.rb b/app/models/blob_viewer/license.rb index 3427227ad26..489b29380d0 100644 --- a/app/models/blob_viewer/license.rb +++ b/app/models/blob_viewer/license.rb @@ -6,7 +6,7 @@ module BlobViewer include Static self.partial_name = 'license' - self.file_types = %i(license) + self.file_types = %i[license] self.binary = false def license diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb index 6f002a6b224..4b04d8425fd 100644 --- a/app/models/blob_viewer/markup.rb +++ b/app/models/blob_viewer/markup.rb @@ -7,7 +7,7 @@ module BlobViewer self.partial_name = 'markup' self.extensions = Gitlab::MarkupHelper::EXTENSIONS - self.file_types = %i(readme) + self.file_types = %i[readme] self.binary = false def banzai_render_context diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb index 351502d451f..e6f1988d7a6 100644 --- a/app/models/blob_viewer/notebook.rb +++ b/app/models/blob_viewer/notebook.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'notebook' - self.extensions = %w(ipynb) + self.extensions = %w[ipynb] self.binary = false self.switcher_icon = 'doc-text' self.switcher_title = 'notebook' diff --git a/app/models/blob_viewer/open_api.rb b/app/models/blob_viewer/open_api.rb index 0551f3bb1e3..5d9c5bea8dc 100644 --- a/app/models/blob_viewer/open_api.rb +++ b/app/models/blob_viewer/open_api.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'openapi' - self.file_types = %i(openapi) + self.file_types = %i[openapi] self.binary = false self.switcher_icon = 'api' end diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb index 5350b6b0626..c205c10b536 100644 --- a/app/models/blob_viewer/package_json.rb +++ b/app/models/blob_viewer/package_json.rb @@ -4,7 +4,7 @@ module BlobViewer class PackageJson < DependencyManager include ServerSide - self.file_types = %i(package_json) + self.file_types = %i[package_json] def manager_name yarn? ? 'yarn' : 'npm' diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb index e3542b91d5c..61957ef4228 100644 --- a/app/models/blob_viewer/pdf.rb +++ b/app/models/blob_viewer/pdf.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'pdf' - self.extensions = %w(pdf) + self.extensions = %w[pdf] self.binary = true self.switcher_icon = 'document' self.switcher_title = 'PDF' diff --git a/app/models/blob_viewer/podfile.rb b/app/models/blob_viewer/podfile.rb index 73d714f48ca..dcabcfc4d57 100644 --- a/app/models/blob_viewer/podfile.rb +++ b/app/models/blob_viewer/podfile.rb @@ -4,7 +4,7 @@ module BlobViewer class Podfile < DependencyManager include Static - self.file_types = %i(podfile) + self.file_types = %i[podfile] def manager_name 'CocoaPods' diff --git a/app/models/blob_viewer/podspec.rb b/app/models/blob_viewer/podspec.rb index 2303471583d..50ca3f5bd16 100644 --- a/app/models/blob_viewer/podspec.rb +++ b/app/models/blob_viewer/podspec.rb @@ -4,7 +4,7 @@ module BlobViewer class Podspec < DependencyManager include ServerSide - self.file_types = %i(podspec) + self.file_types = %i[podspec] def manager_name 'CocoaPods' diff --git a/app/models/blob_viewer/podspec_json.rb b/app/models/blob_viewer/podspec_json.rb index d606f72376d..03e680e2a8b 100644 --- a/app/models/blob_viewer/podspec_json.rb +++ b/app/models/blob_viewer/podspec_json.rb @@ -2,7 +2,7 @@ module BlobViewer class PodspecJson < Podspec - self.file_types = %i(podspec_json) + self.file_types = %i[podspec_json] def package_name @package_name ||= fetch_from_json('name') diff --git a/app/models/blob_viewer/readme.rb b/app/models/blob_viewer/readme.rb index f1a5c6a6acc..ec84977d8c5 100644 --- a/app/models/blob_viewer/readme.rb +++ b/app/models/blob_viewer/readme.rb @@ -6,7 +6,7 @@ module BlobViewer include Static self.partial_name = 'readme' - self.file_types = %i(readme) + self.file_types = %i[readme] self.binary = false def visible_to?(current_user) diff --git a/app/models/blob_viewer/requirements_txt.rb b/app/models/blob_viewer/requirements_txt.rb index 58161e83493..7322e416c4c 100644 --- a/app/models/blob_viewer/requirements_txt.rb +++ b/app/models/blob_viewer/requirements_txt.rb @@ -4,7 +4,7 @@ module BlobViewer class RequirementsTxt < DependencyManager include Static - self.file_types = %i(requirements_txt) + self.file_types = %i[requirements_txt] def manager_name 'pip' diff --git a/app/models/blob_viewer/route_map.rb b/app/models/blob_viewer/route_map.rb index 6731536dfe1..a8c64bd5e6a 100644 --- a/app/models/blob_viewer/route_map.rb +++ b/app/models/blob_viewer/route_map.rb @@ -7,7 +7,7 @@ module BlobViewer self.partial_name = 'route_map' self.loading_partial_name = 'route_map_loading' - self.file_types = %i(route_map) + self.file_types = %i[route_map] self.binary = false def validation_message diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb index 90bc9be29f4..b7b1d412eff 100644 --- a/app/models/blob_viewer/sketch.rb +++ b/app/models/blob_viewer/sketch.rb @@ -6,7 +6,7 @@ module BlobViewer include ClientSide self.partial_name = 'sketch' - self.extensions = %w(sketch) + self.extensions = %w[sketch] self.binary = true self.switcher_icon = 'doc-image' self.switcher_title = 'preview' diff --git a/app/models/blob_viewer/svg.rb b/app/models/blob_viewer/svg.rb index 60a11fbd97e..afcd3a7c735 100644 --- a/app/models/blob_viewer/svg.rb +++ b/app/models/blob_viewer/svg.rb @@ -6,7 +6,7 @@ module BlobViewer include ServerSide self.partial_name = 'svg' - self.extensions = %w(svg) + self.extensions = %w[svg] self.binary = false self.switcher_icon = 'doc-image' self.switcher_title = 'image' diff --git a/app/models/blob_viewer/yarn_lock.rb b/app/models/blob_viewer/yarn_lock.rb index 196d9f96f23..75369370602 100644 --- a/app/models/blob_viewer/yarn_lock.rb +++ b/app/models/blob_viewer/yarn_lock.rb @@ -4,7 +4,7 @@ module BlobViewer class YarnLock < DependencyManager include Static - self.file_types = %i(yarn_lock) + self.file_types = %i[yarn_lock] def manager_name 'Yarn' diff --git a/app/models/bulk_imports/batch_tracker.rb b/app/models/bulk_imports/batch_tracker.rb index 2e79d41d46e..eb7fe9f9913 100644 --- a/app/models/bulk_imports/batch_tracker.rb +++ b/app/models/bulk_imports/batch_tracker.rb @@ -18,6 +18,8 @@ module BulkImports event :start do transition created: :started + # To avoid errors when re-starting a pipeline in case of network errors + transition started: :started end event :retry do diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index 644673e249e..437118c36e8 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -196,6 +196,10 @@ class BulkImports::Entity < ApplicationRecord update!(has_failures: true) end + def source_version + @source_version ||= bulk_import.source_version_info + end + private def validate_parent_is_a_group @@ -240,7 +244,9 @@ class BulkImports::Entity < ApplicationRecord errors.add( :source_full_path, - Gitlab::Regex.bulk_import_source_full_path_regex_message + s_('BulkImport|must have a relative path structure with no HTTP ' \ + 'protocol characters, or leading or trailing forward slashes. Path segments must not start or ' \ + 'end with a special character, and must not contain consecutive special characters') ) end end diff --git a/app/models/bulk_imports/file_transfer/group_config.rb b/app/models/bulk_imports/file_transfer/group_config.rb index 6766c00246b..67d53056444 100644 --- a/app/models/bulk_imports/file_transfer/group_config.rb +++ b/app/models/bulk_imports/file_transfer/group_config.rb @@ -3,7 +3,7 @@ module BulkImports module FileTransfer class GroupConfig < BaseConfig - SKIPPED_RELATIONS = %w(members).freeze + SKIPPED_RELATIONS = %w[members].freeze def import_export_yaml ::Gitlab::ImportExport.group_config_file diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb index 8d4c68f7b5a..890a0fb6ee4 100644 --- a/app/models/bulk_imports/file_transfer/project_config.rb +++ b/app/models/bulk_imports/file_transfer/project_config.rb @@ -3,10 +3,10 @@ module BulkImports module FileTransfer class ProjectConfig < BaseConfig - SKIPPED_RELATIONS = %w( + SKIPPED_RELATIONS = %w[ project_members group_members - ).freeze + ].freeze LFS_OBJECTS_RELATION = 'lfs_objects' REPOSITORY_BUNDLE_RELATION = 'repository' diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb index cda19273f52..d3fbfe3aa55 100644 --- a/app/models/chat_name.rb +++ b/app/models/chat_name.rb @@ -3,9 +3,6 @@ class ChatName < ApplicationRecord LAST_USED_AT_INTERVAL = 1.hour - include IgnorableColumns - ignore_column :integration_id, remove_with: '16.0', remove_after: '2023-04-22' - belongs_to :user validates :user, presence: true diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7a623b0cefb..2abb8e4be48 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -165,7 +165,10 @@ module Ci scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where("#{quoted_table_name}.id = #{Ci::BuildTraceChunk.quoted_table_name}.build_id").select(1)) } scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) } scope :finished_before, -> (date) { finished.where('finished_at < ?', date) } - scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911 + scope :license_management_jobs, -> { where(name: %i[license_management license_scanning]) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911 + # WARNING: This scope could lead to performance implications for large size of tables `ci_builds` and ci_runners`. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123131 + scope :with_runner_type, -> (runner_type) { joins(:runner).where(runner: { runner_type: runner_type }) } scope :with_secure_reports_from_config_options, -> (job_types) do joins(:metadata).where("#{Ci::BuildMetadata.quoted_table_name}.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types) @@ -388,6 +391,9 @@ module Ci name == 'pages' end + # overridden on EE + def pages_path_prefix; end + def runnable? true end @@ -408,7 +414,7 @@ module Ci end def options_scheduled_at - ChronicDuration.parse(options[:start_in])&.seconds&.from_now + ChronicDuration.parse(options[:start_in], use_complete_matcher: true)&.seconds&.from_now end def action? @@ -487,10 +493,7 @@ module Ci Gitlab::Ci::Variables::Collection.new.tap do |variables| break variables unless persisted? && persisted_environment.present? - variables.concat(persisted_environment.predefined_variables) - - variables.append(key: 'CI_ENVIRONMENT_ACTION', value: environment_action) - variables.append(key: 'CI_ENVIRONMENT_TIER', value: environment_tier) + variables.append(key: 'CI_ENVIRONMENT_SLUG', value: environment_slug) # Here we're passing unexpanded environment_url for runner to expand, # and we need to make sure that CI_ENVIRONMENT_NAME and @@ -735,7 +738,7 @@ module Ci def artifacts_expire_in=(value) self.artifacts_expire_at = if value - ChronicDuration.parse(value)&.seconds&.from_now + ChronicDuration.parse(value, use_complete_matcher: true)&.seconds&.from_now end end @@ -1039,6 +1042,13 @@ module Ci end end + def time_in_queue_seconds + return if queued_at.nil? + + (::Time.current - queued_at).seconds.to_i + end + strong_memoize_attr :time_in_queue_seconds + protected def run_status_commit_hooks! diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 317f2523f69..00241908644 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -7,15 +7,16 @@ module Ci include SafelyChangeColumnDefault include BulkInsertSafe + MAX_JOB_NAME_LENGTH = 128 + columns_changing_default :partition_id - ignore_column :id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22' belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs partitionable scope: :build validates :build, presence: true - validates :name, presence: true, length: { maximum: 128 } + validates :name, presence: true, length: { maximum: MAX_JOB_NAME_LENGTH } validates :optional, inclusion: { in: [true, false] } scope :scoped_build, -> { where("#{Ci::Build.quoted_table_name}.id = #{quoted_table_name}.build_id") } diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index eaa2e1c428e..e197217bb70 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -20,7 +20,7 @@ module Ci partitionable scope: :build validates :build, presence: true - validates :url, public_url: { schemes: %w(https) } + validates :url, public_url: { schemes: %w[https] } def terminal_specification wss_url = Gitlab::UrlHelpers.as_wss(Addressable::URI.escape(url)) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3a5db04a687..5bf4e846304 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -162,7 +162,7 @@ module Ci validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? - validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create + validates :source, exclusion: { in: %w[unknown], unless: :importing? }, on: :create after_create :keep_around_commits, unless: :importing? after_find :observe_age_in_minutes, unless: :importing? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 8d93429fd24..91c919dc662 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -52,7 +52,7 @@ module Ci RUNNER_QUEUE_EXPIRY_TIME = 1.hour # The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated - UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze + UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes) # The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner will be considered stale STALE_TIMEOUT = 3.months @@ -532,7 +532,9 @@ module Ci 'virtualbox' => :virtualbox, 'docker+machine' => :docker_machine, 'docker-ssh+machine' => :docker_ssh_machine, - 'kubernetes' => :kubernetes + 'kubernetes' => :kubernetes, + 'docker-autoscaler' => :docker_autoscaler, + 'instance' => :instance }.freeze EXECUTOR_TYPE_TO_NAMES = EXECUTOR_NAME_TO_TYPES.invert.freeze @@ -552,9 +554,7 @@ module Ci end def cleanup_runner_queue - Gitlab::Redis::SharedState.with do |redis| - redis.del(runner_queue_key) - end + ::Gitlab::Workhorse.cleanup_key(runner_queue_key) end def runner_queue_key diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index 8dc866929f3..cbea7efc70e 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -50,7 +50,7 @@ module Clusters end def connected? - agent_tokens.active.where("last_used_at > ?", INACTIVE_AFTER.ago).exists? + agent_tokens.connected.exists? end def activity_event_deletion_cutoff diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb index b2b13f6cef7..f4c497a42cc 100644 --- a/app/models/clusters/agent_token.rb +++ b/app/models/clusters/agent_token.rb @@ -2,10 +2,15 @@ module Clusters class AgentToken < ApplicationRecord + TOKEN_PREFIX = "glagent-" + include RedisCacheable include TokenAuthenticatable - add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) } + add_authentication_token_field :token, + encrypted: :required, + token_generator: -> { Devise.friendly_token(50) }, + format_with_prefix: :glagent_prefix cached_attr_reader :last_used_at self.table_name = 'cluster_agent_tokens' @@ -21,6 +26,7 @@ module Clusters scope :order_last_used_at_desc, -> { order(arel_table[:last_used_at].desc.nulls_last) } scope :with_status, -> (status) { where(status: status) } scope :active, -> { where(status: :active) } + scope :connected, -> { active.where("last_used_at > ?", Clusters::Agent::INACTIVE_AFTER.ago) } enum status: { active: 0, @@ -30,5 +36,9 @@ module Clusters def to_ability_name :cluster end + + def glagent_prefix + TOKEN_PREFIX + end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 123ad0ebfaf..5efbec45561 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -8,7 +8,7 @@ module Clusters include ReactiveCaching include NullifyIfBlank - RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze + RESERVED_NAMESPACES = %w[gitlab-managed-apps].freeze REQUIRED_K8S_MIN_VERSION = 23 IGNORED_CONNECTION_EXCEPTIONS = [ diff --git a/app/models/commit.rb b/app/models/commit.rb index d7aa66588d3..39e12b53f21 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -30,10 +30,10 @@ class Commit MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH MAX_SHA_LENGTH = Gitlab::Git::Commit::MAX_SHA_LENGTH - COMMIT_SHA_PATTERN = Gitlab::Git::Commit::SHA_PATTERN.freeze - EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze + COMMIT_SHA_PATTERN = Gitlab::Git::Commit::SHA_PATTERN + EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/ # Used by GFM to match and present link extensions on node texts and hrefs. - LINK_EXTENSION_PATTERN = /(patch)/.freeze + LINK_EXTENSION_PATTERN = /(patch)/ DEFAULT_MAX_DIFF_LINES_SETTING = 50_000 DEFAULT_MAX_DIFF_FILES_SETTING = 1_000 @@ -432,7 +432,7 @@ class Commit end def cherry_pick_message(user) - %{#{message}\n\n#{cherry_pick_description(user)}} + %(#{message}\n\n#{cherry_pick_description(user)}) end def revert_description(user) @@ -444,7 +444,7 @@ class Commit end def revert_message(user) - %{Revert "#{title.strip}"\n\n#{revert_description(user)}} + %(Revert "#{title.strip}"\n\n#{revert_description(user)}) end def reverts_commit?(commit, user) @@ -539,7 +539,7 @@ class Commit # added by `git commit --fixup` which is used by some community members. # https://gitlab.com/gitlab-org/gitlab/-/issues/342937#note_892065311 # - DRAFT_REGEX = /\A\s*#{Gitlab::Regex.merge_request_draft}|(fixup!|squash!)\s/.freeze + DRAFT_REGEX = /\A\s*#{Gitlab::Regex.merge_request_draft}|(fixup!|squash!)\s/ def draft? !!(title =~ DRAFT_REGEX) @@ -554,10 +554,10 @@ class Commit "commit:#{sha}" end - def expire_note_etag_cache + def broadcast_notes_changed super - expire_note_etag_cache_for_related_mrs + broadcast_notes_changed_for_related_mrs end def readable_by?(user) @@ -614,8 +614,8 @@ class Commit end end - def expire_note_etag_cache_for_related_mrs - MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:expire_note_etag_cache) + def broadcast_notes_changed_for_related_mrs + MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:broadcast_notes_changed) end def commit_reference(from, referable_commit_id, full: false) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index d882a185464..cb24297f2c8 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -28,11 +28,11 @@ class CommitRange # The beginning and ending refs can be named or SHAs, and # the range notation can be double- or triple-dot. - REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/.freeze - PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/.freeze + REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/ + PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ # In text references, the beginning and ending refs can only be valid SHAs. - STRICT_PATTERN = /#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\.{2,3}#{Gitlab::Git::Commit::RAW_SHA_PATTERN}/.freeze + STRICT_PATTERN = /#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\.{2,3}#{Gitlab::Git::Commit::RAW_SHA_PATTERN}/ def self.reference_prefix '@' diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb index a9e8ca2dd33..45937b68691 100644 --- a/app/models/commit_signatures/gpg_signature.rb +++ b/app/models/commit_signatures/gpg_signature.rb @@ -3,6 +3,7 @@ module CommitSignatures class GpgSignature < ApplicationRecord include CommitSignature include SignatureType + include EachBatch sha_attribute :gpg_key_primary_keyid diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c2425e9460a..3761aa81bf7 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -9,16 +9,19 @@ class CommitStatus < Ci::ApplicationRecord include BulkInsertableAssociations include TaggableQueries - ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_routing_table + def self.switch_table_names + if Gitlab::Utils.to_boolean(ENV['USE_CI_BUILDS_ROUTING_TABLE']) + :p_ci_builds + else + :ci_builds + end + end - self.table_name = 'ci_builds' - self.sequence_name = 'ci_builds_id_seq' + self.table_name = self.switch_table_names + self.sequence_name = :ci_builds_id_seq self.primary_key = :id - partitionable scope: :pipeline, through: { - table: :p_ci_builds, - flag: ROUTING_FEATURE_FLAG - } + partitionable scope: :pipeline belongs_to :user belongs_to :project diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index f419fa8518e..e342939b3d6 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -48,12 +48,6 @@ module Avatarable end end - class_methods do - def bot_avatar(image:) - Rails.root.join('lib', 'assets', 'images', 'bot_avatars', image).open - end - end - def avatar_type unless self.avatar.image? errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::SAFE_IMAGE_EXT.join(', ')}" diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb index af4905115b1..7b7b61fdf06 100644 --- a/app/models/concerns/chronic_duration_attribute.rb +++ b/app/models/concerns/chronic_duration_attribute.rb @@ -17,7 +17,12 @@ module ChronicDurationAttribute chronic_duration_attributes[virtual_attribute] = value.presence || parameters[:default].presence.to_s begin - new_value = value.present? ? ChronicDuration.parse(value).to_i : parameters[:default].presence + new_value = if value.present? + ChronicDuration.parse(value, use_complete_matcher: true).to_i + else + parameters[:default].presence + end + assign_attributes(source_attribute => new_value) rescue ChronicDuration::DurationParseError # ignore error as it will be caught by validation diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb index b3b80989410..d25151f9a34 100644 --- a/app/models/concerns/ci/deployable.rb +++ b/app/models/concerns/ci/deployable.rb @@ -138,7 +138,11 @@ module Ci end def environment_url - options&.dig(:environment, :url) || persisted_environment&.external_url + options&.dig(:environment, :url) || persisted_environment.try(:external_url) + end + + def environment_slug + persisted_environment.try(:slug) end def environment_status diff --git a/app/models/concerns/ci/has_runner_executor.rb b/app/models/concerns/ci/has_runner_executor.rb index dc70cdb2018..6d4622945fe 100644 --- a/app/models/concerns/ci/has_runner_executor.rb +++ b/app/models/concerns/ci/has_runner_executor.rb @@ -17,7 +17,9 @@ module Ci virtualbox: 8, docker_machine: 9, docker_ssh_machine: 10, - kubernetes: 11 + kubernetes: 11, + docker_autoscaler: 12, + instance: 13 }, _suffix: true end end diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb index e2cef0981d1..15240385dd8 100644 --- a/app/models/concerns/ci/maskable.rb +++ b/app/models/concerns/ci/maskable.rb @@ -11,12 +11,12 @@ module Ci # * Minimal length of 8 characters # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~' # * Absolutely no fun is allowed - REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z}.freeze + REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z} # * Single line # * No spaces # * Minimal length of 8 characters # * Some fun is allowed - MASK_AND_RAW_REGEX = %r{\A\S{8,}\z}.freeze + MASK_AND_RAW_REGEX = %r{\A\S{8,}\z} included do validates :masked, inclusion: { in: [true, false] } diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb index ec6c85d888d..c4b1281fa72 100644 --- a/app/models/concerns/ci/partitionable.rb +++ b/app/models/concerns/ci/partitionable.rb @@ -107,7 +107,10 @@ module Ci partitioned_by :partition_id, strategy: :ci_sliding_list, next_partition_if: proc { false }, - detach_partition_if: proc { false } + detach_partition_if: proc { false }, + # Most of the db tasks are run in a weekly basis, e.g. execute_batched_migrations. + # Therefore, let's start with 1.week and see how it'd go. + analyze_interval: 1.week end end end diff --git a/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb b/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb index eef68bfd349..9528a708ee1 100644 --- a/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb +++ b/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb @@ -17,7 +17,7 @@ module Clusters class_methods do def available_ci_access_fields(_project) - %w(agent) + %w[agent] end end end diff --git a/app/models/concerns/cross_database_ignored_tables.rb b/app/models/concerns/cross_database_ignored_tables.rb index c97e405cce4..14a9703a734 100644 --- a/app/models/concerns/cross_database_ignored_tables.rb +++ b/app/models/concerns/cross_database_ignored_tables.rb @@ -4,6 +4,12 @@ module CrossDatabaseIgnoredTables extend ActiveSupport::Concern class_methods do + def temporary_ignore_cross_database_tables(tables, url:, &blk) + Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction( + tables, url: url, &blk + ) + end + def cross_database_ignore_tables(tables, options = {}) raise "missing issue url" if options[:url].blank? @@ -40,8 +46,7 @@ module CrossDatabaseIgnoredTables return yield unless options[:if].nil? || instance_eval(&options[:if]) url = options[:url] - Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction( - tables, url: url, &blk - ) + + self.class.temporary_ignore_cross_database_tables(tables, url: url, &blk) end end diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb index b10b318fb7c..2f64129b65f 100644 --- a/app/models/concerns/diff_positionable_note.rb +++ b/app/models/concerns/diff_positionable_note.rb @@ -14,7 +14,7 @@ module DiffPositionableNote validates :position, json_schema: { filename: "position", hash_conversion: true } end - %i(original_position position change_position).each do |meth| + %i[original_position position change_position].each do |meth| define_method "#{meth}=" do |new_position| if new_position.is_a?(String) new_position = begin diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index 945d286a2fd..0c8cf861c38 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -54,7 +54,7 @@ module EachBatch 'the column: argument must be set to a column name to use for ordering rows' end - start = except(:select) + start = except(:select, :includes, :preload) .select(column) .reorder(column => order) @@ -69,7 +69,7 @@ module EachBatch 1.step do |index| start_cond = arel_table[column].gteq(start_id) start_cond = arel_table[column].lteq(start_id) if order == :desc - stop = except(:select) + stop = except(:select, :includes, :preload) .select(column) .where(start_cond) .reorder(column => order) diff --git a/app/models/concerns/editable.rb b/app/models/concerns/editable.rb index 2e49e720ac9..be9858bf49b 100644 --- a/app/models/concerns/editable.rb +++ b/app/models/concerns/editable.rb @@ -8,6 +8,6 @@ module Editable end def last_edited_by - super || User.ghost + super || Users::Internal.ghost end end diff --git a/app/models/concerns/enums/prometheus_metric.rb b/app/models/concerns/enums/prometheus_metric.rb index e65a01990a3..2cc765b7a3c 100644 --- a/app/models/concerns/enums/prometheus_metric.rb +++ b/app/models/concerns/enums/prometheus_metric.rb @@ -30,37 +30,37 @@ module Enums # built-in groups nginx_ingress_vts: { group_title: _('Response metrics (NGINX Ingress VTS)'), - required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg), + required_metrics: %w[nginx_upstream_responses_total nginx_upstream_response_msecs_avg], priority: 10 }.freeze, nginx_ingress: { group_title: _('Response metrics (NGINX Ingress)'), - required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum), + required_metrics: %w[nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum], priority: 10 }.freeze, ha_proxy: { group_title: _('Response metrics (HA Proxy)'), - required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total), + required_metrics: %w[haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total], priority: 10 }.freeze, aws_elb: { group_title: _('Response metrics (AWS ELB)'), - required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum), + required_metrics: %w[aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum], priority: 10 }.freeze, nginx: { group_title: _('Response metrics (NGINX)'), - required_metrics: %w(nginx_server_requests nginx_server_requestMsec), + required_metrics: %w[nginx_server_requests nginx_server_requestMsec], priority: 10 }.freeze, kubernetes: { group_title: _('System metrics (Kubernetes)'), - required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total), + required_metrics: %w[container_memory_usage_bytes container_cpu_usage_seconds_total], priority: 5 }.freeze, cluster_health: { group_title: _('Cluster Health'), - required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total), + required_metrics: %w[container_memory_usage_bytes container_cpu_usage_seconds_total], priority: 10 }.freeze }.merge(custom_group_details).freeze diff --git a/app/models/concerns/has_unique_internal_users.rb b/app/models/concerns/has_unique_internal_users.rb deleted file mode 100644 index 25b56f6d70f..00000000000 --- a/app/models/concerns/has_unique_internal_users.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module HasUniqueInternalUsers - extend ActiveSupport::Concern - - class_methods do - private - - def unique_internal(scope, username, email_pattern, &block) - scope.first || create_unique_internal(scope, username, email_pattern, &block) - end - - def create_unique_internal(scope, username, email_pattern, &creation_block) - # Since we only want a single one of these in an instance, we use an - # exclusive lease to ensure than this block is never run concurrently. - lease_key = "user:unique_internal:#{username}" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) - - until uuid = lease.try_obtain - # Keep trying until we obtain the lease. To prevent hammering Redis too - # much we'll wait for a bit between retries. - sleep(1) - end - - # Recheck if the user is already present. One might have been - # added between the time we last checked (first line of this method) - # and the time we acquired the lock. - existing_user = uncached { scope.first } - return existing_user if existing_user.present? - - uniquify = Gitlab::Utils::Uniquify.new - - username = uniquify.string(username) { |s| User.find_by_username(s) } - - email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s| - User.find_by_email(s) - end - - user = scope.build( - username: username, - email: email, - &creation_block - ) - - Users::UpdateService.new(user, user: user).execute(validate: false) - user - ensure - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - end - end -end diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index 2d0ff82e624..c3f702a4e69 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -74,4 +74,21 @@ module HasUserType # https://gitlab.com/gitlab-org/gitlab/-/issues/346058 '****' end + + def resource_bot_resource + return unless project_bot? + + projects&.first || groups&.first + end + + def resource_bot_owners + return [] unless project_bot? + + resource = resource_bot_resource + return [] unless resource + + return resource.maintainers if resource.is_a?(Project) + + resource.owners + end end diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb index 11dc8a76a2b..9735a9bf5f6 100644 --- a/app/models/concerns/integrations/enable_ssl_verification.rb +++ b/app/models/concerns/integrations/enable_ssl_verification.rb @@ -19,13 +19,16 @@ module Integrations url_index = fields.index { |field| field[:name].ends_with?('_url') } insert_index = url_index ? url_index + 1 : -1 - fields.insert(insert_index, { - type: 'checkbox', - name: 'enable_ssl_verification', - title: s_('Integrations|SSL verification'), - checkbox_label: s_('Integrations|Enable SSL verification'), - help: s_('Integrations|Clear if using a self-signed certificate.') - }) + fields.insert(insert_index, + Field.new( + name: 'enable_ssl_verification', + integration_class: self, + type: :checkbox, + title: s_('Integrations|SSL verification'), + checkbox_label: s_('Integrations|Enable SSL verification'), + help: s_('Integrations|Clear if using a self-signed certificate.') + ) + ) end end end diff --git a/app/models/concerns/integrations/reset_secret_fields.rb b/app/models/concerns/integrations/reset_secret_fields.rb index f79c4392f19..24d716fe5dd 100644 --- a/app/models/concerns/integrations/reset_secret_fields.rb +++ b/app/models/concerns/integrations/reset_secret_fields.rb @@ -12,9 +12,7 @@ module Integrations end def exposing_secrets_fields - # TODO: Once all integrations use `Integrations::Field` we can remove the `.try` here. - # See: https://gitlab.com/groups/gitlab-org/-/epics/7652 - fields.select { _1.try(:exposes_secrets) }.pluck(:name) + fields.select(&:exposes_secrets).pluck(:name) end private diff --git a/app/models/concerns/integrations/slack_mattermost_fields.rb b/app/models/concerns/integrations/slack_mattermost_fields.rb new file mode 100644 index 00000000000..a8e63c4e405 --- /dev/null +++ b/app/models/concerns/integrations/slack_mattermost_fields.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Integrations + module SlackMattermostFields + extend ActiveSupport::Concern + + included do + field :webhook, + help: -> { webhook_help }, + required: true, + if: -> { requires_webhook? } + + field :username, + placeholder: 'GitLab-integration', + if: -> { requires_webhook? } + + field :notify_only_broken_pipelines, + type: :checkbox, + section: Integration::SECTION_TYPE_CONFIGURATION, + help: 'Do not send notifications for successful pipelines.' + + field :branches_to_be_notified, + type: :select, + section: Integration::SECTION_TYPE_CONFIGURATION, + title: -> { s_('Integration|Branches for which notifications are to be sent') }, + choices: -> { branch_choices } + + field :labels_to_be_notified, + section: Integration::SECTION_TYPE_CONFIGURATION, + placeholder: '~backend,~frontend', + help: 'Send notifications for issue, merge request, and comment events with the listed labels only. ' \ + 'Leave blank to receive notifications for all events.' + + field :labels_to_be_notified_behavior, + type: :select, + section: Integration::SECTION_TYPE_CONFIGURATION, + choices: [ + ['Match any of the labels', Integrations::BaseChatNotification::MATCH_ANY_LABEL], + ['Match all of the labels', Integrations::BaseChatNotification::MATCH_ALL_LABELS] + ] + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 9a513ea0e5b..a9a00ab1c44 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable include Awardable include Taskable include Importable + include Transitionable include Editable include AfterCommitQueue include Sortable @@ -33,7 +34,7 @@ module Issuable TITLE_HTML_LENGTH_MAX = 800 DESCRIPTION_LENGTH_MAX = 1.megabyte DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes - SEARCHABLE_FIELDS = %w(title description).freeze + SEARCHABLE_FIELDS = %w[title description].freeze MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS = 200 STATE_ID_MAP = { @@ -225,6 +226,10 @@ module Issuable false end + def supports_lock_on_merge? + false + end + def severity return IssuableSeverity::DEFAULT unless supports_severity? @@ -235,6 +240,10 @@ module Issuable super + [:notes] end + def importing_or_transitioning? + importing? || transitioning? + end + private def validate_description_length? @@ -408,14 +417,14 @@ module Issuable sort = sort.to_s grouping_columns = [arel_table[:id]] - if %w(milestone_due_desc milestone_due_asc milestone).include?(sort) + if %w[milestone_due_desc milestone_due_asc milestone].include?(sort) milestone_table = Milestone.arel_table grouping_columns << milestone_table[:id] grouping_columns << milestone_table[:due_date] - elsif %w(merged_at_desc merged_at_asc merged_at).include?(sort) + elsif %w[merged_at_desc merged_at_asc merged_at].include?(sort) grouping_columns << MergeRequest::Metrics.arel_table[:id] grouping_columns << MergeRequest::Metrics.arel_table[:merged_at] - elsif %w(closed_at_desc closed_at_asc closed_at).include?(sort) + elsif %w[closed_at_desc closed_at_asc closed_at].include?(sort) grouping_columns << MergeRequest::Metrics.arel_table[:id] grouping_columns << MergeRequest::Metrics.arel_table[:latest_closed_at] end diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb index 3f65e701da7..2969f1e1928 100644 --- a/app/models/concerns/issue_available_features.rb +++ b/app/models/concerns/issue_available_features.rb @@ -10,10 +10,10 @@ module IssueAvailableFeatures # EE only features are listed on EE::IssueAvailableFeatures def available_features_for_issue_types { - assignee: %w(issue incident), - confidentiality: %w(issue incident), - time_tracking: %w(issue incident), - move_and_clone: %w(issue incident) + assignee: %w[issue incident], + confidentiality: %w[issue incident objective key_result], + time_tracking: %w[issue incident], + move_and_clone: %w[issue incident] }.with_indifferent_access end end diff --git a/app/models/concerns/linkable_item.rb b/app/models/concerns/linkable_item.rb index 135252727ab..c91e3615ba7 100644 --- a/app/models/concerns/linkable_item.rb +++ b/app/models/concerns/linkable_item.rb @@ -16,6 +16,7 @@ module LinkableItem scope :for_source, ->(item) { where(source_id: item.id) } scope :for_target, ->(item) { where(target_id: item.id) } + scope :for_source_and_target, ->(source, target) { where(source: source, target: target) } scope :for_items, ->(source, target) do where(source: source, target: target).or(where(source: target, target: source)) end diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb index 0b6075fbeb8..b5634ba3b6d 100644 --- a/app/models/concerns/mentionable/reference_regexes.rb +++ b/app/models/concerns/mentionable/reference_regexes.rb @@ -28,7 +28,7 @@ module Mentionable def self.external_pattern strong_memoize(:external_pattern) do issue_pattern = Integrations::BaseIssueTracker.base_reference_pattern - link_patterns = URI::DEFAULT_PARSER.make_regexp(%w(http https)) + link_patterns = URI::DEFAULT_PARSER.make_regexp(%w[http https]) reference_pattern(link_patterns, issue_pattern) end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 40a91c8ac94..06cee46645b 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -12,17 +12,17 @@ module Noteable class_methods do # `Noteable` class names that support replying to individual notes. def replyable_types - %w(Issue MergeRequest) + %w[Issue MergeRequest] end # `Noteable` class names that support resolvable notes. def resolvable_types - %w(Issue MergeRequest DesignManagement::Design) + %w[Issue MergeRequest DesignManagement::Design] end # `Noteable` class names that support creating/forwarding individual notes. def email_creatable_types - %w(Issue) + %w[Issue] end end @@ -164,28 +164,15 @@ module Noteable [MergeRequest, Issue].include?(self.class) end - def etag_caching_enabled? + def real_time_notes_enabled? false end - def expire_note_etag_cache + def broadcast_notes_changed return unless discussions_rendered_on_frontend? - return unless etag_caching_enabled? + return unless real_time_notes_enabled? - # TODO: We need to figure out a way to make ETag caching work for group-level work items - Gitlab::EtagCaching::Store.new.touch(note_etag_key) unless is_a?(Issue) && project.nil? - - Noteable::NotesChannel.broadcast_to(self, event: 'updated') if Feature.enabled?(:action_cable_notes, project || try(:group)) - end - - def note_etag_key - return Gitlab::Routing.url_helpers.designs_project_issue_path(project, issue, { vueroute: filename }) if self.is_a?(DesignManagement::Design) - - Gitlab::Routing.url_helpers.project_noteable_notes_path( - project, - target_type: noteable_target_type_name, - target_id: id - ) + Noteable::NotesChannel.broadcast_to(self, event: 'updated') end def after_note_created(_note) diff --git a/app/models/concerns/packages/nuget/version_normalizable.rb b/app/models/concerns/packages/nuget/version_normalizable.rb index 473e5f07811..4bcfec89570 100644 --- a/app/models/concerns/packages/nuget/version_normalizable.rb +++ b/app/models/concerns/packages/nuget/version_normalizable.rb @@ -13,7 +13,7 @@ module Packages private def set_normalized_version - return unless package && Feature.enabled?(:nuget_normalized_version, package.project) + return unless package self.normalized_version = normalize end diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb index 562c8cf23f3..b7ca6f61573 100644 --- a/app/models/concerns/pg_full_text_searchable.rb +++ b/app/models/concerns/pg_full_text_searchable.rb @@ -21,11 +21,11 @@ module PgFullTextSearchable extend ActiveSupport::Concern - LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,}).freeze + LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,}) 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 + URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)} + TSQUERY_DISALLOWED_CHARACTERS_REGEX = %r{[^a-zA-Z0-9 .@/\-_"]} def update_search_data! tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight| diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index a87eadb9332..ea8a1640bea 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -3,6 +3,8 @@ module ProtectedRef extend ActiveSupport::Concern + include Importable + included do belongs_to :project, touch: true @@ -32,12 +34,13 @@ module ProtectedRef # to fail. has_many :"#{type}_access_levels", inverse_of: self.model_name.singular + # Overridden in EE with `if: -> { false }` so this validation does not apply on an EE instance. validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }, - unless: -> { allow_multiple?(type) } + unless: -> { allow_multiple?(type) || importing? } accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true end diff --git a/app/models/concerns/redactable.rb b/app/models/concerns/redactable.rb index 53ae300ee2d..5ad96d6cc46 100644 --- a/app/models/concerns/redactable.rb +++ b/app/models/concerns/redactable.rb @@ -10,7 +10,7 @@ module Redactable extend ActiveSupport::Concern - UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}.freeze + UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe} class_methods do def redact_field(field) diff --git a/app/models/concerns/require_email_verification.rb b/app/models/concerns/require_email_verification.rb index d7182778b36..6581928f637 100644 --- a/app/models/concerns/require_email_verification.rb +++ b/app/models/concerns/require_email_verification.rb @@ -7,10 +7,7 @@ module RequireEmailVerification extend ActiveSupport::Concern include Gitlab::Utils::StrongMemoize - # This value is twice the amount we want it to be, because due to a bug in the devise-two-factor - # gem every failed login attempt increments the value of failed_attempts by 2 instead of 1. - # See: https://github.com/tinfoil/devise-two-factor/issues/127 - MAXIMUM_ATTEMPTS = 3 * 2 + MAXIMUM_ATTEMPTS = 3 UNLOCK_IN = 24.hours included do diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index e967c78154d..5c2f0aa04ac 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -116,7 +116,7 @@ module ResolvableDiscussion # Set the notes array to the updated notes @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables - noteable.expire_note_etag_cache + noteable.broadcast_notes_changed clear_memoized_values end diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 7f9a7faa3f5..23abc5d5c22 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -4,7 +4,7 @@ module ResolvableNote extend ActiveSupport::Concern # Names of all subclasses of `Note` that can be resolvable. - RESOLVABLE_TYPES = %w(DiffNote DiscussionNote).freeze + RESOLVABLE_TYPES = %w[DiffNote DiscussionNote].freeze included do belongs_to :resolved_by, class_name: "User" diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb index cf97be21165..6af9ede5e8b 100644 --- a/app/models/concerns/restricted_signup.rb +++ b/app/models/concerns/restricted_signup.rb @@ -84,3 +84,5 @@ module RestrictedSignup end end end + +::RestrictedSignup.prepend_mod diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index f2badfe48dd..ef14ff5fbe2 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -14,7 +14,17 @@ module Routable # Routable.find_by_full_path('groupname/projectname') # -> Project # # Returns a single object, or nil. - def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute) + + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def self.find_by_full_path( + path, + follow_redirects: false, + route_scope: Route, + redirect_route_scope: RedirectRoute, + optimize_routable: Routable.optimize_routable_enabled? + ) + return unless path.present? # Convert path to string to prevent DB error: function lower(integer) does not exist @@ -25,20 +35,50 @@ module Routable # # We need to qualify the columns with the table name, to support both direct lookups on # Route/RedirectRoute, and scoped lookups through the Routable classes. - Gitlab::Database.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") do + if optimize_routable + path_condition = { path: path } + + source_type_condition = if route_scope == Route + {} + else + { source_type: route_scope.klass.base_class } + end + route = - route_scope.find_by(routes: { path: path }) || - route_scope.iwhere(Route.arel_table[:path] => path).take + Route.where(source_type_condition).find_by(path_condition) || + Route.where(source_type_condition).iwhere(path_condition).take if follow_redirects - route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take + route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take end - next unless route + return unless route + return route.source if route_scope == Route + + route_scope.find_by(id: route.source_id) + else + Gitlab::Database.allow_cross_joins_across_databases(url: + "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") do + route = + route_scope.find_by(routes: { path: path }) || + route_scope.iwhere(Route.arel_table[:path] => path).take + + if follow_redirects + route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take + end - route.is_a?(Routable) ? route : route.source + next unless route + + route.is_a?(Routable) ? route : route.source + end end end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity + + def self.optimize_routable_enabled? + Feature.enabled?(:optimize_routable) + end included do # Remove `inverse_of: source` when upgraded to rails 5.2 @@ -67,13 +107,22 @@ module Routable # # Returns a single object, or nil. def find_by_full_path(path, follow_redirects: false) - # TODO: Optimize these queries by avoiding joins - # https://gitlab.com/gitlab-org/gitlab/-/issues/292252 + optimize_routable = Routable.optimize_routable_enabled? + + if optimize_routable + route_scope = all + redirect_route_scope = RedirectRoute + else + route_scope = includes(:route).references(:routes) + redirect_route_scope = joins(:redirect_routes) + end + Routable.find_by_full_path( path, follow_redirects: follow_redirects, - route_scope: includes(:route).references(:routes), - redirect_route_scope: joins(:redirect_routes) + route_scope: route_scope, + redirect_route_scope: redirect_route_scope, + optimize_routable: optimize_routable ) end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index b73ed937b5d..5455a2159cd 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -17,8 +17,6 @@ module Storage Namespace.find(parent_id_before_last_save) # raise NotFound early if needed end - move_repositories - if saved_change_to_parent? former_parent_full_path = parent_was&.full_path parent_full_path = parent&.full_path diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index bf645e99b5e..96f684522d2 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -11,8 +11,8 @@ require 'task_list/filter' module Taskable COMPLETED = 'completed' INCOMPLETE = 'incomplete' - COMPLETE_PATTERN = /\[[xX]\]/.freeze - INCOMPLETE_PATTERN = /\[[[:space:]]\]/.freeze + COMPLETE_PATTERN = /\[[xX]\]/ + INCOMPLETE_PATTERN = /\[[[:space:]]\]/ ITEM_PATTERN = %r{ ^ (?:(?:>\s{0,4})*) # optional blockquote characters @@ -22,7 +22,7 @@ module Taskable #{COMPLETE_PATTERN}|#{INCOMPLETE_PATTERN} ) (\s.+) # followed by whitespace and some text. - }x.freeze + }x ITEM_PATTERN_UNTRUSTED = '^' \ diff --git a/app/models/concerns/transitionable.rb b/app/models/concerns/transitionable.rb new file mode 100644 index 00000000000..70e1fc8b78a --- /dev/null +++ b/app/models/concerns/transitionable.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Transitionable + extend ActiveSupport::Concern + + attr_accessor :transitioning + + def transitioning? + return false unless transitioning && Feature.enabled?(:skip_validations_during_transitions, project) + + true + end + + def enable_transitioning + self.transitioning = true + end + + def disable_transitioning + self.transitioning = false + end +end diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb new file mode 100644 index 00000000000..cb8e5fdc682 --- /dev/null +++ b/app/models/concerns/users/visitable.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Users + module Visitable + extend ActiveSupport::Concern + + included do + def self.visited_around?(entity_id:, user_id:, time:) + visits_around(entity_id: entity_id, user_id: user_id, time: time).any? + end + + def self.visits_around(entity_id:, user_id:, time:) + time = time.to_datetime + where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes)) + end + end + end +end diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb index caaf2b33ef0..319509ea69a 100644 --- a/app/models/concerns/with_uploads.rb +++ b/app/models/concerns/with_uploads.rb @@ -22,7 +22,7 @@ module WithUploads # Currently there is no simple way how to select only not-mounted # uploads, it should be all FileUploaders so we select them by # `uploader` class - FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze + FILE_UPLOADERS = %w[PersonalFileUploader NamespaceFileUploader FileUploader].freeze included do around_destroy :ignore_uploads_table_in_transaction diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb index aecb47f7a03..f643fa7730b 100644 --- a/app/models/container_expiration_policy.rb +++ b/app/models/container_expiration_policy.rb @@ -80,7 +80,9 @@ class ContainerExpirationPolicy < ApplicationRecord end def set_next_run_at - self.next_run_at = Time.zone.now + ChronicDuration.parse(cadence).seconds + cadence_seconds = ChronicDuration.parse(cadence, use_complete_matcher: true).seconds + + self.next_run_at = Time.zone.now + cadence_seconds end def disable! diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb index dd2675e17d8..9f7724c052c 100644 --- a/app/models/container_registry/event.rb +++ b/app/models/container_registry/event.rb @@ -4,25 +4,25 @@ module ContainerRegistry class Event include Gitlab::Utils::StrongMemoize - ALLOWED_ACTIONS = %w(push delete).freeze + ALLOWED_ACTIONS = %w[push delete].freeze PUSH_ACTION = 'push' DELETE_ACTION = 'delete' EVENT_TRACKING_CATEGORY = 'container_registry:notification' EVENT_PREFIX = 'i_container_registry' - ALLOWED_ACTOR_TYPES = %w( + ALLOWED_ACTOR_TYPES = %w[ personal_access_token build gitlab_or_ldap - ).freeze + ].freeze - TRACKABLE_ACTOR_EVENTS = %w( + TRACKABLE_ACTOR_EVENTS = %w[ push_tag delete_tag push_repository delete_repository create_repository - ).freeze + ].freeze attr_reader :event @@ -60,7 +60,7 @@ module ContainerRegistry def target_tag? # There is no clear indication in the event structure when we delete a top-level manifest - # except existance of "tag" key + # except existence of "tag" key event['target'].has_key?('tag') end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 625d68925c6..c704795130b 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class CustomEmoji < ApplicationRecord - NAME_REGEXP = /[a-z0-9_-]+/.freeze + NAME_REGEXP = /[a-z0-9_-]+/ belongs_to :namespace, inverse_of: :custom_emoji diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index f9fa4bd212c..de777b8ae53 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -43,7 +43,7 @@ class DeployKey < Key end def user - super || User.ghost + super || Users::Internal.ghost end def audit_details diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 498ca9c4f30..920321a1699 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -8,8 +8,8 @@ class DeployToken < ApplicationRecord add_authentication_token_field :token, encrypted: :required - AVAILABLE_SCOPES = %i(read_repository read_registry write_registry - read_package_registry write_package_registry).freeze + AVAILABLE_SCOPES = %i[read_repository read_registry write_registry + read_package_registry write_package_registry].freeze GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token' REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze diff --git a/app/models/description_version.rb b/app/models/description_version.rb index fb61b7f5fde..05cca9f931f 100644 --- a/app/models/description_version.rb +++ b/app/models/description_version.rb @@ -9,7 +9,7 @@ class DescriptionVersion < ApplicationRecord delegate :resource_parent, to: :issuable def self.issuable_attrs - %i(issue merge_request).freeze + %i[issue merge_request].freeze end def issuable diff --git a/app/models/design_management.rb b/app/models/design_management.rb index 81e170f7e59..20ada71755b 100644 --- a/app/models/design_management.rb +++ b/app/models/design_management.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module DesignManagement - DESIGN_IMAGE_SIZES = %w(v432x230).freeze + DESIGN_IMAGE_SIZES = %w[v432x230].freeze def self.designs_directory 'designs' diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 02979d5f804..d680d0e334f 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -9,7 +9,7 @@ class DiffNote < Note include Gitlab::Utils::StrongMemoize def self.noteable_types - %w(MergeRequest Commit DesignManagement::Design) + %w[MergeRequest Commit DesignManagement::Design] end validates :original_position, presence: true diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb index 6621b30b645..a1dfa0e72ec 100644 --- a/app/models/discussion_note.rb +++ b/app/models/discussion_note.rb @@ -9,7 +9,7 @@ class DiscussionNote < Note # Names of all implementers of `Noteable` that support discussions. def self.noteable_types - %w(MergeRequest Issue Commit Snippet) + %w[MergeRequest Issue Commit Snippet] end validates :noteable_type, inclusion: { in: noteable_types } diff --git a/app/models/draft_note.rb b/app/models/draft_note.rb index ffc04f9bf90..f95eec742d8 100644 --- a/app/models/draft_note.rb +++ b/app/models/draft_note.rb @@ -5,8 +5,8 @@ class DraftNote < ApplicationRecord include Sortable include ShaAttribute - PUBLISH_ATTRS = %i(noteable_id noteable_type type note).freeze - DIFF_ATTRS = %i(position original_position change_position commit_id).freeze + PUBLISH_ATTRS = %i[noteable_id noteable_type type note].freeze + DIFF_ATTRS = %i[position original_position change_position commit_id].freeze sha_attribute :commit_id diff --git a/app/models/environment.rb b/app/models/environment.rb index 36445279b86..29394c37e2c 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -25,7 +25,6 @@ class Environment < ApplicationRecord has_many :successful_deployments, -> { success }, class_name: 'Deployment' has_many :active_deployments, -> { active }, class_name: 'Deployment' has_many :prometheus_alerts, inverse_of: :environment - has_many :self_managed_prometheus_alert_events, inverse_of: :environment has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment # NOTE: If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader. @@ -108,11 +107,11 @@ class Environment < ApplicationRecord scope :deployed_and_updated_before, -> (project_id, before) do # this query joins deployments and filters out any environment that has recent deployments - joins = %{ + joins = %( LEFT JOIN "deployments" on "deployments".environment_id = "environments".id AND "deployments".project_id = #{project_id} AND "deployments".updated_at >= #{connection.quote(before)} - } + ) Environment.joins(joins) .where(project_id: project_id, updated_at: ...before) .group('id', 'deployments.id') @@ -193,7 +192,7 @@ class Environment < ApplicationRecord end event :stop_complete do - transition %i(available stopping) => :stopped + transition %i[available stopping] => :stopped end state :available diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 7687bc2be60..f31615f2b3b 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -79,7 +79,7 @@ class EnvironmentStatus private - PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze + PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i def deployment_metrics @deployment_metrics ||= DeploymentMetrics.new(project, deployment) @@ -102,7 +102,6 @@ class EnvironmentStatus return [] unless pipeline environments = pipeline.environments_in_self_and_project_descendants.includes(:project) - environments = environments.available if Feature.disabled?(:review_apps_redeploy_mr_widget, mr.project) environments.map do |environment| next unless Ability.allowed?(user, :read_environment, environment) diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index c52f8a58c00..318538be645 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -19,7 +19,7 @@ module ErrorTracking (?<project>[^/]+)/* )? \z - }x.freeze + }x self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } self.reactive_cache_work_type = :external_dependency diff --git a/app/models/event.rb b/app/models/event.rb index 4547d7b9e60..9e4a662aaa5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -10,6 +10,7 @@ class Event < ApplicationRecord include UsageStatistics include ShaAttribute include IgnorableColumns + include EachBatch ignore_column :target_id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22' @@ -69,7 +70,7 @@ class Event < ApplicationRecord # If the association for "target" defines an "author" association we want to # eager-load this so Banzai & friends don't end up performing N+1 queries to # get the authors of notes, issues, etc. (likewise for "noteable"). - incs = %i(author noteable work_item_type).select do |a| + incs = %i[author noteable work_item_type].select do |a| reflections['events'].active_record.reflect_on_association(a) end @@ -137,7 +138,7 @@ class Event < ApplicationRecord where( 'action IN (?) OR (target_type IN (?) AND action IN (?))', [actions[:pushed], actions[:commented]], - %w(MergeRequest Issue WorkItem), [actions[:created], actions[:closed], actions[:merged]] + %w[MergeRequest Issue WorkItem], [actions[:created], actions[:closed], actions[:merged]] ) end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 1bf35179393..f0cae9c88ca 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -10,7 +10,7 @@ class GpgKey < ApplicationRecord sha_attribute :fingerprint belongs_to :user - has_many :gpg_signatures + has_many :gpg_signatures, class_name: 'CommitSignatures::GpgSignature' has_many :subkeys, class_name: 'GpgKeySubkey' scope :with_subkeys, -> { includes(:subkeys) } diff --git a/app/models/group.rb b/app/models/group.rb index 9df3c143e0c..9330ffef156 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -35,11 +35,12 @@ class Group < Namespace foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember' alias_method :members, :group_members - has_many :users, through: :group_members - has_many :owners, - -> { where(members: { access_level: Gitlab::Access::OWNER }) }, - through: :all_group_members, - source: :user + has_many :users, -> { allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405") }, + through: :group_members + has_many :owners, -> { + where(members: { access_level: Gitlab::Access::OWNER }) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405") + }, through: :all_group_members, source: :user has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent has_many :namespace_requesters, -> { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) }, @@ -785,8 +786,6 @@ class Group < Namespace end def execute_integrations(data, hooks_scope) - return unless Feature.enabled?(:group_mentions, self) - integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend integration.async_execute(data) end @@ -800,7 +799,9 @@ class Group < Namespace end def first_owner - owners.first || parent&.first_owner || owner + first_owner_member = all_group_members.all_owners.order(:user_id).first + + first_owner_member&.user || parent&.first_owner || owner end def default_branch_name @@ -898,6 +899,10 @@ class Group < Namespace feature_flag_enabled_for_self_or_ancestor?(:linked_work_items) end + def supports_lock_on_merge? + feature_flag_enabled_for_self_or_ancestor?(:enforce_locked_labels_on_merge, type: :ops) + end + def usage_quotas_enabled? ::Feature.enabled?(:usage_quotas_for_all_editions, self) && root? end @@ -939,12 +944,12 @@ class Group < Namespace private - def feature_flag_enabled_for_self_or_ancestor?(feature_flag) + def feature_flag_enabled_for_self_or_ancestor?(feature_flag, type: :development) actors = [root_ancestor] actors << self if root_ancestor != self actors.any? do |actor| - ::Feature.enabled?(feature_flag, actor) + ::Feature.enabled?(feature_flag, actor, type: type) end end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index d7a95363337..c0bfe31fb38 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -103,7 +103,7 @@ class WebHook < ApplicationRecord end # See app/validators/json_schemas/web_hooks_url_variables.json - VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze + VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/ def interpolated_url(url = self.url, url_variables = self.url_variables) return url unless url.include?('{') diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index 4c35f699468..3e0c8e7c472 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class WebHookLog < ApplicationRecord - include SafeUrl include Presentable include DeleteWithLimit include CreatedAtFilterable @@ -58,10 +57,18 @@ class WebHookLog < ApplicationRecord self[:request_headers].merge('X-Gitlab-Token' => _('[REDACTED]')) end + def url_current? + # URL hash hasn't been set, so we must assume there's no prior value to + # compare to. + return true if url_hash.nil? + + Gitlab::CryptoHelper.sha256(web_hook.interpolated_url) == url_hash + end + private def obfuscate_basic_auth - self.url = safe_url + self.url = Gitlab::UrlSanitizer.sanitize_masked_url(url) end def redact_user_emails diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb index 57638356362..7b2036a9def 100644 --- a/app/models/instance_configuration.rb +++ b/app/models/instance_configuration.rb @@ -3,7 +3,7 @@ require 'resolv' class InstanceConfiguration - SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze + SSH_ALGORITHMS = %w[DSA ECDSA ED25519 RSA].freeze SSH_ALGORITHMS_PATH = '/etc/ssh/' CACHE_KEY = 'instance_configuration' EXPIRATION_TIME = 24.hours diff --git a/app/models/integration.rb b/app/models/integration.rb index bc86b08018f..d4c76f743a3 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -63,6 +63,7 @@ class Integration < ApplicationRecord encode: false, encode_iv: false + alias_attribute :name, :title # Handle assignment of props with symbol keys. # To do this correctly, we need to call the method generated by attr_encrypted. alias_method :attr_encrypted_props=, :properties= @@ -468,11 +469,8 @@ class Integration < ApplicationRecord [] end - # TODO: Once all integrations use `Integrations::Field` we can - # use `#secret?` here. - # See: https://gitlab.com/groups/gitlab-org/-/epics/7652 def secret_fields - fields.select { |f| f[:type] == :password }.pluck(:name) + fields.select(&:secret?).pluck(:name) end # Expose a list of fields in the JSON endpoint. diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index 6f96626718f..ef12fc6bf6f 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -4,8 +4,8 @@ require 'app_store_connect' module Integrations class AppleAppStore < Integration - ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze - KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze + ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/ + KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/ IS_KEY_CONTENT_BASE64 = "true" SECTION_TYPE_APPLE_APP_STORE = 'apple_app_store' diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb index 7436c08aa38..859522670ef 100644 --- a/app/models/integrations/asana.rb +++ b/app/models/integrations/asana.rb @@ -12,8 +12,7 @@ module Integrations help: -> { s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.') }, non_empty_password_title: -> { s_('ProjectService|Enter new API key') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key.') }, - # Example Personal Access Token from Asana docs - placeholder: '0/68a9e79b868c6789e79a124c30b0', + placeholder: '0/68a9e79b868c6789e79a124c30b0', # Example Personal Access Token from Asana docs required: true field :restrict_to_branch, @@ -38,7 +37,7 @@ module Integrations end def self.supported_events - %w(push) + %w[push] end def client diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb index 6831fac32e6..1d3616b4c3b 100644 --- a/app/models/integrations/assembla.rb +++ b/app/models/integrations/assembla.rb @@ -28,7 +28,7 @@ module Integrations end def self.supported_events - %w(push) + %w[push] end def execute(data) diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index 4d207574ca7..2c929dc2cb3 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -31,12 +31,12 @@ module Integrations # Custom serialized properties initialization prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) - boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch + boolean_accessor :notify_only_default_branch validates :webhook, presence: true, public_url: true, - if: -> (integration) { integration.activated? && integration.requires_webhook? } + if: -> (integration) { integration.activated? && integration.class.requires_webhook? } validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true, if: :activated? validate :validate_channel_limit, if: :activated? @@ -44,7 +44,7 @@ module Integrations super if properties.empty? - self.notify_only_broken_pipelines = true + self.notify_only_broken_pipelines = true if self.respond_to?(:notify_only_broken_pipelines) self.branches_to_be_notified = "default" self.labels_to_be_notified_behavior = MATCH_ANY_LABEL elsif !self.notify_only_default_branch.nil? @@ -72,48 +72,7 @@ module Integrations end def fields - default_fields + build_event_channels - end - - def default_fields - [ - { - type: :checkbox, - section: SECTION_TYPE_CONFIGURATION, - name: 'notify_only_broken_pipelines', - help: 'Do not send notifications for successful pipelines.' - }.freeze, - { - type: :select, - section: SECTION_TYPE_CONFIGURATION, - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - }.freeze, - { - type: :text, - section: SECTION_TYPE_CONFIGURATION, - name: 'labels_to_be_notified', - placeholder: '~backend,~frontend', - help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' - }.freeze, - { - type: :select, - section: SECTION_TYPE_CONFIGURATION, - name: 'labels_to_be_notified_behavior', - choices: [ - ['Match any of the labels', MATCH_ANY_LABEL], - ['Match all of the labels', MATCH_ALL_LABELS] - ] - }.freeze - ].tap do |fields| - next unless requires_webhook? - - fields.unshift( - { type: :text, name: 'webhook', help: webhook_help, required: true }.freeze, - { type: :text, name: 'username', placeholder: 'GitLab-integration' }.freeze - ) - end.freeze + self.class.fields + build_event_channels end def execute(data) @@ -154,6 +113,15 @@ module Integrations supported_events.map { |event| event_channel_name(event) } end + override :api_field_names + def api_field_names + if mask_configurable_channels? + super - event_channel_names + else + super + end + end + def form_fields super.reject { |field| field[:name].end_with?('channel') } end @@ -166,6 +134,10 @@ module Integrations raise NotImplementedError end + def help + raise NotImplementedError + end + # With some integrations the webhook is already tied to a specific channel, # for others the channels are configurable for each event. def configurable_channels? @@ -181,7 +153,7 @@ module Integrations self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend end - def requires_webhook? + def self.requires_webhook? true end @@ -193,11 +165,32 @@ module Integrations false end + override :sections + def sections + [ + { + type: SECTION_TYPE_CONNECTION, + title: s_('Integrations|Connection details'), + description: help + }, + { + type: SECTION_TYPE_TRIGGER, + title: s_('Integrations|Trigger'), + description: s_('Integrations|An event will be triggered when one of the following items happen.') + }, + { + type: SECTION_TYPE_CONFIGURATION, + title: s_('Integrations|Notification settings'), + description: s_('Integrations|Configure the scope of notifications.') + } + ] + end + private def should_execute?(object_kind) supported_events.include?(object_kind) && - (!requires_webhook? || webhook.present?) + (!self.class.requires_webhook? || webhook.present?) end def log_usage(_, _) @@ -264,7 +257,7 @@ module Integrations def build_event_channels event_channel_names.map do |channel_field| - { type: :text, name: channel_field, placeholder: default_channel_placeholder } + Field.new(name: channel_field, type: :text, placeholder: default_channel_placeholder, integration_class: self) end end diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb index 7a54d354007..b59aee6743d 100644 --- a/app/models/integrations/base_issue_tracker.rb +++ b/app/models/integrations/base_issue_tracker.rb @@ -88,7 +88,7 @@ module Integrations end def self.supported_events - %w(push) + %w[push] end def execute(data) diff --git a/app/models/integrations/base_monitoring.rb b/app/models/integrations/base_monitoring.rb index b0bebb5a859..12ea57f59a3 100644 --- a/app/models/integrations/base_monitoring.rb +++ b/app/models/integrations/base_monitoring.rb @@ -9,7 +9,7 @@ module Integrations attribute :category, default: 'monitoring' def self.supported_events - %w() + %w[] end def can_query? diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index 29a20419809..65aec8b278f 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -25,20 +25,24 @@ module Integrations override :supported_events def supported_events - additional = %w[alert] - - if group_level? && Feature.enabled?(:group_mentions, group) - additional += %w[group_mention group_confidential_mention] - end + additional = group_level? ? %w[group_mention group_confidential_mention] : [] (super + additional).freeze end + def self.supported_events + super + %w[alert] + end + override :configurable_channels? def configurable_channels? true end + def help + # noop + end + private override :log_usage diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb index 7662da933ba..58821e5fb4e 100644 --- a/app/models/integrations/base_slash_commands.rb +++ b/app/models/integrations/base_slash_commands.rb @@ -13,7 +13,7 @@ module Integrations end def self.supported_events - %w() + %w[] end def testable? diff --git a/app/models/integrations/base_third_party_wiki.rb b/app/models/integrations/base_third_party_wiki.rb index 8df172e9a53..dee3706c518 100644 --- a/app/models/integrations/base_third_party_wiki.rb +++ b/app/models/integrations/base_third_party_wiki.rb @@ -9,7 +9,7 @@ module Integrations after_commit :cache_project_has_integration def self.supported_events - %w() + %w[] end private diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index 6cd36e545a5..82a5142e8c2 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -29,7 +29,7 @@ module Integrations validates :token, presence: true, if: :activated? def self.supported_events - %w(push merge_request tag_push) + %w[push merge_request tag_push] end # This is a stub method to work with deprecated API response diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb index 007578e5830..8b5797a9d24 100644 --- a/app/models/integrations/campfire.rb +++ b/app/models/integrations/campfire.rb @@ -2,7 +2,7 @@ module Integrations class Campfire < Integration - SUBDOMAIN_REGEXP = %r{\A[a-z](?:[a-z0-9-]*[a-z0-9])?\z}i.freeze + SUBDOMAIN_REGEXP = %r{\A[a-z](?:[a-z0-9-]*[a-z0-9])?\z}i validates :token, presence: true, if: :activated? validates :room, @@ -26,12 +26,9 @@ module Integrations placeholder: '', exposes_secrets: true, help: -> do - ERB::Util.html_escape( + format(ERB::Util.html_escape( s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.') - ) % { - code_open: '<code>'.html_safe, - code_close: '</code>'.html_safe - } + ), code_open: '<code>'.html_safe, code_close: '</code>'.html_safe) end field :room, @@ -48,13 +45,16 @@ module Integrations end def help - docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'campfire'), target: '_blank', rel: 'noopener noreferrer' - - ERB::Util.html_escape( + docs_link = ActionController::Base.helpers.link_to( + _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'), + target: '_blank', + rel: 'noopener noreferrer' + ) + + format(ERB::Util.html_escape( s_('CampfireService|Send notifications about push events to Campfire chat rooms. %{docs_link}') - ) % { - docs_link: docs_link.html_safe - } + ), docs_link: docs_link.html_safe) end def self.to_param @@ -62,14 +62,14 @@ module Integrations end def self.supported_events - %w(push) + %w[push] end def execute(data) return unless supported_events.include?(data[:object_kind]) message = create_message(data) - speak(self.room, message, auth) + speak(room, message, auth) end private @@ -96,7 +96,7 @@ module Integrations room = rooms(auth).find { |r| r["name"] == room_name } return unless room - path = "/room/#{room["id"]}/speak.json" + path = "/room/#{room['id']}/speak.json" body = { body: { message: { diff --git a/app/models/integrations/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb index 501b214a769..600f07b97f1 100644 --- a/app/models/integrations/chat_message/base_message.rb +++ b/app/models/integrations/chat_message/base_message.rb @@ -3,7 +3,7 @@ module Integrations module ChatMessage class BaseMessage - RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze + RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)} attr_reader :markdown attr_reader :user_full_name diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb index b28edeecb4d..0367459dfcb 100644 --- a/app/models/integrations/chat_message/deployment_message.rb +++ b/app/models/integrations/chat_message/deployment_message.rb @@ -26,8 +26,10 @@ module Integrations end def attachments + return description_message if markdown + [{ - text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{strip_markup(commit_title)}", + text: format(description_message), color: color }] end @@ -82,6 +84,10 @@ module Integrations def running? status == 'running' end + + def description_message + "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{strip_markup(commit_title)}" + end end end end diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb index 31e9a171d1b..eda8c37fc72 100644 --- a/app/models/integrations/confluence.rb +++ b/app/models/integrations/confluence.rb @@ -2,9 +2,9 @@ module Integrations class Confluence < BaseThirdPartyWiki - VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze - VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze - VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze + VALID_SCHEME_MATCH = %r{\Ahttps?\Z} + VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z} + VALID_PATH_MATCH = %r{\A/wiki(/|\Z)} validates :confluence_url, presence: true, if: :activated? validate :validate_confluence_url_is_cloud, if: :activated? @@ -14,6 +14,10 @@ module Integrations placeholder: 'https://example.atlassian.net/wiki', required: true + def avatar_url + ActionController::Base.helpers.image_path('confluence.svg') + end + def self.to_param 'confluence' end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index 1a56763fe57..b1f1361afcd 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -12,7 +12,7 @@ module Integrations pipeline build archive_trace ].freeze - TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze + TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x field :datadog_site, exposes_secrets: true, @@ -40,7 +40,7 @@ module Integrations ERB::Util.html_escape( s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.') ) % { - linkOpen: %{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe, + linkOpen: %(<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">).html_safe, linkClose: '</a>'.html_safe } end, diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index 7cae3ca20f9..815e3669d78 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -4,9 +4,7 @@ require "discordrb/webhooks" module Integrations class Discord < BaseChatNotification - ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze - - undef :notify_only_broken_pipelines + ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/ field :webhook, section: SECTION_TYPE_CONNECTION, @@ -35,10 +33,6 @@ module Integrations "discord" end - def fields - self.class.fields + build_event_channels - end - def help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer' s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } @@ -52,26 +46,6 @@ module Integrations %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] end - def sections - [ - { - type: SECTION_TYPE_CONNECTION, - title: s_('Integrations|Connection details'), - description: help - }, - { - type: SECTION_TYPE_TRIGGER, - title: s_('Integrations|Trigger'), - description: s_('Integrations|An event will be triggered when one of the following items happen.') - }, - { - type: SECTION_TYPE_CONFIGURATION, - title: s_('Integrations|Notification settings'), - description: s_('Integrations|Configure the scope of notifications.') - } - ] - end - def configurable_channels? true end diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index ac464c020dd..f6a12c4bb1a 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -43,7 +43,7 @@ module Integrations end def self.supported_events - %w(push merge_request tag_push) + %w[push merge_request tag_push] end def commit_status_path(sha, ref) diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb index eb893ae45d0..144d1a07b04 100644 --- a/app/models/integrations/emails_on_push.rb +++ b/app/models/integrations/emails_on_push.rb @@ -52,7 +52,7 @@ module Integrations end def self.supported_events - %w(push tag_push) + %w[push tag_push] end def initialize_properties diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb index 75fe6b6f164..acacab2528e 100644 --- a/app/models/integrations/external_wiki.rb +++ b/app/models/integrations/external_wiki.rb @@ -47,7 +47,7 @@ module Integrations end def self.supported_events - %w() + %w[] end end end diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb index b0f54f39e8c..2d520eaf7e7 100644 --- a/app/models/integrations/gitlab_slack_application.rb +++ b/app/models/integrations/gitlab_slack_application.rb @@ -20,6 +20,8 @@ module Integrations has_one :slack_integration, foreign_key: :integration_id, inverse_of: :integration delegate :bot_access_token, :bot_user_id, to: :slack_integration, allow_nil: true + include SlackMattermostFields + def update_active_status update(active: !!slack_integration) end @@ -66,18 +68,7 @@ module Integrations def sections return [] unless editable? - [ - { - type: SECTION_TYPE_TRIGGER, - title: s_('Integrations|Trigger'), - description: s_('Integrations|An event will be triggered when one of the following items happen.') - }, - { - type: SECTION_TYPE_CONFIGURATION, - title: s_('Integrations|Notification settings'), - description: s_('Integrations|Configure the scope of notifications.') - } - ] + super.drop(1) end override :configurable_events @@ -88,7 +79,7 @@ module Integrations end override :requires_webhook? - def requires_webhook? + def self.requires_webhook? false end diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index 037c689c75e..680752c3d56 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -2,8 +2,6 @@ module Integrations class HangoutsChat < BaseChatNotification - undef :notify_only_broken_pipelines - field :webhook, section: SECTION_TYPE_CONNECTION, help: 'https://chat.googleapis.com/v1/spaces…', @@ -36,10 +34,6 @@ module Integrations s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end - def fields - self.class.fields + build_event_channels - end - def default_channel_placeholder end diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index 7769ea7d2dd..0683c8408bc 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -66,7 +66,7 @@ module Integrations end def self.supported_events - %w(push merge_request tag_push) + %w[push merge_request tag_push] end def title diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index faf0a378a17..d8d1f860e9a 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -126,7 +126,7 @@ module Integrations # When these are false GitLab does not create cross reference # comments on Jira except when an issue gets transitioned. def self.supported_events - %w(commit merge_request) + %w[commit merge_request] end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb index e3c5c22ad3a..7e391b11d82 100644 --- a/app/models/integrations/mattermost.rb +++ b/app/models/integrations/mattermost.rb @@ -3,6 +3,7 @@ module Integrations class Mattermost < BaseChatNotification include SlackMattermostNotifier + include SlackMattermostFields def title _('Mattermost notifications') @@ -25,7 +26,7 @@ module Integrations 'my-channel' end - def webhook_help + def self.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 25308948d51..208172d6303 100644 --- a/app/models/integrations/microsoft_teams.rb +++ b/app/models/integrations/microsoft_teams.rb @@ -2,8 +2,6 @@ module Integrations class MicrosoftTeams < BaseChatNotification - undef :notify_only_broken_pipelines - field :webhook, section: SECTION_TYPE_CONNECTION, help: 'https://outlook.office.com/webhook/…', @@ -44,30 +42,6 @@ module Integrations pipeline wiki_page] end - def fields - self.class.fields + build_event_channels - end - - def sections - [ - { - type: SECTION_TYPE_CONNECTION, - title: s_('Integrations|Connection details'), - description: help - }, - { - type: SECTION_TYPE_TRIGGER, - title: s_('Integrations|Trigger'), - description: s_('Integrations|An event will be triggered when one of the following items happen.') - }, - { - type: SECTION_TYPE_CONFIGURATION, - title: s_('Integrations|Notification settings'), - description: s_('Integrations|Configure the scope of notifications.') - } - ] - end - private def notify(message, opts) diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb index c9c08ec9771..c0acb6c87b4 100644 --- a/app/models/integrations/packagist.rb +++ b/app/models/integrations/packagist.rb @@ -42,7 +42,7 @@ module Integrations end def self.supported_events - %w(push merge_request tag_push) + %w[push merge_request tag_push] end def execute(data) diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb index 0d9a3f05a86..f42a872c49e 100644 --- a/app/models/integrations/pivotaltracker.rb +++ b/app/models/integrations/pivotaltracker.rb @@ -38,7 +38,7 @@ module Integrations end def self.supported_events - %w(push) + %w[push] end def execute(data) diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index 736318ed707..8474a5b7adf 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -41,6 +41,7 @@ module Integrations before_save :synchronize_service_state after_save :clear_reactive_cache! + after_commit :sync_http_integration! after_commit :track_events @@ -180,5 +181,16 @@ module Integrations nil end strong_memoize_attr :iap_client + + # Remove in next required stop after %16.4 + # https://gitlab.com/gitlab-org/gitlab/-/issues/338838 + def sync_http_integration! + return unless manual_configuration_changed? + + project.alert_management_http_integrations + .for_endpoint_identifier('legacy-prometheus') + .take + &.update_columns(active: manual_configuration) + end end end diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb index 8f0dddcc5c5..09e011023ed 100644 --- a/app/models/integrations/pumble.rb +++ b/app/models/integrations/pumble.rb @@ -2,8 +2,6 @@ module Integrations class Pumble < BaseChatNotification - undef :notify_only_broken_pipelines - field :webhook, section: SECTION_TYPE_CONNECTION, help: 'https://api.pumble.com/workspaces/x/...', @@ -52,10 +50,6 @@ module Integrations pipeline wiki_page] end - def fields - self.class.fields + build_event_channels - end - private def notify(message, opts) diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index 006b731c6c2..e97c7e5e738 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -47,19 +47,19 @@ module Integrations [ ['Device default sound', nil], ['Pushover (default)', 'pushover'], - %w(Bike bike), - %w(Bugle bugle), + %w[Bike bike], + %w[Bugle bugle], ['Cash Register', 'cashregister'], - %w(Classical classical), - %w(Cosmic cosmic), - %w(Falling falling), - %w(Gamelan gamelan), - %w(Incoming incoming), - %w(Intermission intermission), - %w(Magic magic), - %w(Mechanical mechanical), + %w[Classical classical], + %w[Cosmic cosmic], + %w[Falling falling], + %w[Gamelan gamelan], + %w[Incoming incoming], + %w[Intermission intermission], + %w[Magic magic], + %w[Mechanical mechanical], ['Piano Bar', 'pianobar'], - %w(Siren siren), + %w[Siren siren], ['Space Alarm', 'spacealarm'], ['Tug Boat', 'tugboat'], ['Alien Alarm (long)', 'alien'], @@ -84,7 +84,7 @@ module Integrations end def self.supported_events - %w(push) + %w[push] end def execute(data) diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb index f5b6595fff2..227fdca5c91 100644 --- a/app/models/integrations/shimo.rb +++ b/app/models/integrations/shimo.rb @@ -8,6 +8,10 @@ module Integrations title: -> { s_('Shimo|Shimo Workspace URL') }, required: true + def avatar_url + ActionController::Base.helpers.image_path('logos/shimo.svg') + end + def render? valid? && activated? end diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb index 07d2d802915..f70376e2f0d 100644 --- a/app/models/integrations/slack.rb +++ b/app/models/integrations/slack.rb @@ -3,6 +3,7 @@ module Integrations class Slack < BaseSlackNotification include SlackMattermostNotifier + include SlackMattermostFields def title 'Slack notifications' @@ -16,8 +17,7 @@ module Integrations 'slack' end - override :webhook_help - def webhook_help + def self.webhook_help 'https://hooks.slack.com/services/…' end diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index c74e0aab030..575c3b8a334 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -6,7 +6,7 @@ module Integrations include ReactivelyCached prepend EnableSslVerification - TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze + TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i field :teamcity_url, title: -> { s_('ProjectService|TeamCity server URL') }, @@ -43,7 +43,7 @@ module Integrations end def supported_events - %w(push merge_request) + %w[push merge_request] end end diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb index 9af12c712c6..7c196720386 100644 --- a/app/models/integrations/telegram.rb +++ b/app/models/integrations/telegram.rb @@ -21,6 +21,11 @@ module Integrations placeholder: '@channelusername', required: true + field :notify_only_broken_pipelines, + type: :checkbox, + section: SECTION_TYPE_CONFIGURATION, + help: 'If selected, successful pipelines do not trigger a notification event.' + with_options if: :activated? do validates :token, :room, presence: true end @@ -51,34 +56,10 @@ module Integrations ) end - def fields - self.class.fields + build_event_channels - end - def self.supported_events super - ['deployment'] end - def sections - [ - { - type: SECTION_TYPE_CONNECTION, - title: s_('Integrations|Connection details'), - description: help - }, - { - type: SECTION_TYPE_TRIGGER, - title: s_('Integrations|Trigger'), - description: s_('Integrations|An event will be triggered when one of the following items happen.') - }, - { - type: SECTION_TYPE_CONFIGURATION, - title: s_('Integrations|Notification settings'), - description: s_('Integrations|Configure the scope of notifications.') - } - ] - end - private def set_webhook diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index 6de693b5278..3b4bcfa28d3 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -2,8 +2,6 @@ module Integrations class UnifyCircuit < BaseChatNotification - undef :notify_only_broken_pipelines - field :webhook, section: SECTION_TYPE_CONNECTION, help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…', @@ -31,10 +29,6 @@ module Integrations 'unify_circuit' end - def fields - self.class.fields + build_event_channels - end - def help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer' s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index 21c65cc2b32..3ef8ab39352 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -2,8 +2,6 @@ module Integrations class WebexTeams < BaseChatNotification - undef :notify_only_broken_pipelines - field :webhook, section: SECTION_TYPE_CONNECTION, help: 'https://api.ciscospark.com/v1/webhooks/incoming/...', @@ -31,10 +29,6 @@ module Integrations 'webex_teams' end - def fields - self.class.fields + build_event_channels - end - def help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer' s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe } diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb index fd2c741bd6b..58ec4abf30c 100644 --- a/app/models/integrations/zentao.rb +++ b/app/models/integrations/zentao.rb @@ -34,6 +34,10 @@ module Integrations validates :api_token, presence: true, if: :activated? validates :zentao_product_xid, presence: true, if: :activated? + def avatar_url + ActionController::Base.helpers.image_path('logos/zentao.svg') + end + def self.issues_license_available?(project) project&.licensed_feature_available?(:zentao_issues_integration) end @@ -82,7 +86,7 @@ module Integrations end def self.supported_events - %w() + %w[] end private diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb index cd7e5fafb60..08984bbb723 100644 --- a/app/models/issuable_severity.rb +++ b/app/models/issuable_severity.rb @@ -11,11 +11,11 @@ class IssuableSeverity < ApplicationRecord }.freeze SEVERITY_QUICK_ACTION_PARAMS = { - unknown: %w(Unknown 0), - low: %w(Low S4 4), - medium: %w(Medium S3 3), - high: %w(High S2 2), - critical: %w(Critical S1 1) + unknown: %w[Unknown 0], + low: %w[Low S4 4], + medium: %w[Medium S3 3], + high: %w[High S2 2], + critical: %w[Critical S1 1] }.freeze belongs_to :issue diff --git a/app/models/issue.rb b/app/models/issue.rb index d227448961a..58383a6a329 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -14,7 +14,6 @@ class Issue < ApplicationRecord include TimeTrackable include ThrottledTouch include LabelEventable - include IgnorableColumns include MilestoneEventable include WhereComposite include StateEventable @@ -48,16 +47,14 @@ class Issue < ApplicationRecord # # 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 objective key_result).freeze + TYPES_FOR_LIST = %w[issue incident test_case task objective key_result].freeze # Types of issues that should be displayed on issue board lists - TYPES_FOR_BOARD_LIST = %w(issue incident).freeze + TYPES_FOR_BOARD_LIST = %w[issue incident].freeze # This default came from the enum `issue_type` column. Defined as default in the DB DEFAULT_ISSUE_TYPE = :issue - ignore_column :issue_type, remove_with: '16.4', remove_after: '2023-08-22' - belongs_to :project belongs_to :namespace, inverse_of: :issues @@ -112,7 +109,6 @@ class Issue < ApplicationRecord has_one :sentry_issue has_one :alert_management_alert, class_name: 'AlertManagement::Alert' 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, validate: false has_many :prometheus_alerts, through: :prometheus_alert_events @@ -190,7 +186,6 @@ class Issue < ApplicationRecord scope :preload_awardable, -> { preload(:award_emoji) } scope :with_alert_management_alerts, -> { joins(:alert_management_alert) } scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) } - scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) } scope :with_api_entity_associations, -> { preload(:work_item_type, :timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity, namespace: [{ parent: :route }, :route], milestone: { project: [:route, { namespace: :route }] }, @@ -223,8 +218,11 @@ class Issue < ApplicationRecord scope :counts_by_state, -> { reorder(nil).group(:state_id).count } - scope :service_desk, -> { where(author: ::User.support_bot) } - scope :inc_relations_for_view, -> { includes(author: :status, assignees: :status) } + scope :service_desk, -> { where(author: ::Users::Internal.support_bot) } + scope :inc_relations_for_view, -> do + includes(author: :status, assignees: :status) + .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422155') + end # An issue can be uniquely identified by project_id and iid # Takes one or more sets of composite IDs, expressed as hash-like records of @@ -546,18 +544,14 @@ class Issue < ApplicationRecord end def related_issues(current_user, preload: nil) - related_issues = self.class - .select(['issues.*', 'issue_links.id AS issue_link_id', - 'issue_links.link_type as issue_link_type_value', - 'issue_links.target_id as issue_link_source_id', - 'issue_links.created_at as issue_link_created_at', - 'issue_links.updated_at as issue_link_updated_at']) - .joins("INNER JOIN issue_links ON - (issue_links.source_id = issues.id AND issue_links.target_id = #{id}) - OR - (issue_links.target_id = issues.id AND issue_links.source_id = #{id})") - .preload(preload) - .reorder('issue_link_id') + related_issues = + linked_issues_select + .joins("INNER JOIN issue_links ON + (issue_links.source_id = issues.id AND issue_links.target_id = #{id}) + OR + (issue_links.target_id = issues.id AND issue_links.source_id = #{id})") + .preload(preload) + .reorder('issue_link_id') related_issues = yield related_issues if block_given? @@ -607,7 +601,7 @@ class Issue < ApplicationRecord end end - def etag_caching_enabled? + def real_time_notes_enabled? true end @@ -642,7 +636,7 @@ class Issue < ApplicationRecord end def from_service_desk? - author.id == User.support_bot.id + author.id == Users::Internal.support_bot.id end def issue_link_type @@ -716,8 +710,8 @@ class Issue < ApplicationRecord end def expire_etag_cache - # TODO: Fix this for the case when issues is created at group level - # TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/395814 + # We don't expire the cache for issues that don't have a project, since they are created at the group level + # and they are only displayed in the new work item view that uses GraphQL subscriptions for real-time updates return unless project key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self) @@ -789,7 +783,7 @@ class Issue < ApplicationRecord # TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/393126 return unless project - Issues::SearchData.upsert({ namespace_id: namespace_id, project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id)) + Issues::SearchData.upsert({ namespace_id: namespace_id, project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i[project_id issue_id]) end def ensure_metrics! @@ -833,6 +827,14 @@ class Issue < ApplicationRecord errors.add(:work_item_type_id, format(_('can not be changed to %{new_type}'), new_type: work_item_type&.name)) end + + def linked_issues_select + self.class.select(['issues.*', 'issue_links.id AS issue_link_id', + 'issue_links.link_type as issue_link_type_value', + 'issue_links.target_id as issue_link_source_id', + 'issue_links.created_at as issue_link_created_at', + 'issue_links.updated_at as issue_link_updated_at']) + end end Issue.prepend_mod_with('Issue') diff --git a/app/models/label_link.rb b/app/models/label_link.rb index d326b07ad31..0c2d205c641 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -24,3 +24,5 @@ class LabelLink < ApplicationRecord relation end end + +LabelLink.prepend_mod_with('LabelLink') diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb index 3df6742fbc9..046e47262dd 100644 --- a/app/models/lfs_download_object.rb +++ b/app/models/lfs_download_object.rb @@ -9,7 +9,7 @@ class LfsDownloadObject validates :oid, format: { with: /\A\h{64}\z/ } validates :size, numericality: { greater_than_or_equal_to: 0 } - validates :link, public_url: { protocols: %w(http https) } + validates :link, public_url: { protocols: %w[http https] } validate :headers_must_be_hash def initialize(oid:, size:, link:, headers: {}) diff --git a/app/models/license_template.rb b/app/models/license_template.rb index 548066107c1..bfe2a8d379e 100644 --- a/app/models/license_template.rb +++ b/app/models/license_template.rb @@ -5,12 +5,12 @@ class LicenseTemplate %r{[\<\{\[] (project|description| one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]}xi.freeze - YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze + [\>\}\]]}xi + YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i FULLNAME_TEMPLATE_REGEX = %r{[\<\{\[] (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]}xi.freeze + [\>\}\]]}xi attr_reader :key, :name, :project, :category, :nickname, :url, :meta diff --git a/app/models/loose_foreign_keys/modification_tracker.rb b/app/models/loose_foreign_keys/modification_tracker.rb index 72a596d2114..eec9b8ad285 100644 --- a/app/models/loose_foreign_keys/modification_tracker.rb +++ b/app/models/loose_foreign_keys/modification_tracker.rb @@ -2,10 +2,6 @@ module LooseForeignKeys class ModificationTracker - MAX_DELETES = 100_000 - MAX_UPDATES = 50_000 - MAX_RUNTIME = 30.seconds # must be less than the scheduling frequency of the LooseForeignKeys::CleanupWorker cron worker - delegate :monotonic_time, to: :'Gitlab::Metrics::System' def initialize @@ -22,6 +18,18 @@ module LooseForeignKeys ) end + def max_runtime + 30.seconds + end + + def max_deletes + 100_000 + end + + def max_updates + 50_000 + end + def add_deletions(table, count) @delete_count_by_table[table] += count @deletes_counter.increment({ table: table }, count) @@ -33,9 +41,9 @@ module LooseForeignKeys end def over_limit? - @delete_count_by_table.values.sum >= MAX_DELETES || - @update_count_by_table.values.sum >= MAX_UPDATES || - monotonic_time - @start_time >= MAX_RUNTIME + @delete_count_by_table.values.sum >= max_deletes || + @update_count_by_table.values.sum >= max_updates || + monotonic_time - @start_time >= max_runtime end def stats diff --git a/app/models/loose_foreign_keys/turbo_modification_tracker.rb b/app/models/loose_foreign_keys/turbo_modification_tracker.rb new file mode 100644 index 00000000000..5229b17e971 --- /dev/null +++ b/app/models/loose_foreign_keys/turbo_modification_tracker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module LooseForeignKeys + # This is a modification tracker with the additional limits that can be enabled + # for some database via an OPS Feature Flag. + + class TurboModificationTracker < ModificationTracker + extend ::Gitlab::Utils::Override + + override :max_runtime + def max_runtime + 45.seconds + end + + override :max_deletes + def max_deletes + 200_000 + end + + override :max_updates + def max_updates + 150_000 + end + end +end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index ada89345a7f..52b9c3a80e3 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -5,11 +5,10 @@ class GroupMember < Member include CreatedAtFilterable SOURCE_TYPE = 'Namespace' - SOURCE_TYPE_FORMAT = /\ANamespace\z/.freeze + SOURCE_TYPE_FORMAT = /\ANamespace\z/ belongs_to :group, foreign_key: 'source_id' alias_attribute :namespace_id, :source_id - delegate :update_two_factor_requirement, to: :user, allow_nil: true # Make sure group member points only to group as it source attribute :source_type, default: SOURCE_TYPE @@ -26,6 +25,16 @@ class GroupMember < Member attr_accessor :last_owner + def update_two_factor_requirement + return unless user + + Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction( + %w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288' + ) do + user.update_two_factor_requirement + end + end + # For those who get to see a modal with a role dropdown, here are the options presented def self.permissible_access_level_roles(_, _) # This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087 diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index e0fecf702de..d07e4f9e298 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -2,7 +2,7 @@ class ProjectMember < Member SOURCE_TYPE = 'Project' - SOURCE_TYPE_FORMAT = /\AProject\z/.freeze + SOURCE_TYPE_FORMAT = /\AProject\z/ belongs_to :project, foreign_key: 'source_id' diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 469dba42952..6a72ed6476e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -66,7 +66,7 @@ class MergeRequest < ApplicationRecord belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff' manual_inverse_association :latest_merge_request_diff, :merge_request - # method overriden in EE + # method overridden in EE def suggested_reviewer_users User.none end @@ -162,7 +162,7 @@ class MergeRequest < ApplicationRecord # Keep states definition to be evaluated before the state_machine block to # avoid spec failures. If this gets evaluated after, the `merged` and `locked` - # states (which are overriden) can be nil. + # states (which are overridden) can be nil. # def self.available_state_names super + [:merged, :locked] @@ -279,6 +279,12 @@ class MergeRequest < ApplicationRecord def check_state?(merge_status) [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking].include?(merge_status.to_sym) end + + # rubocop: disable Style/SymbolProc + before_transition { |merge_request| merge_request.enable_transitioning } + + after_transition { |merge_request| merge_request.disable_transitioning } + # rubocop: enable Style/SymbolProc end # Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking` @@ -292,10 +298,14 @@ class MergeRequest < ApplicationRecord validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing? - validate :validate_branches, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?] + validate :validate_branches, unless: [ + :allow_broken, + :importing_or_transitioning?, + :closed_or_merged_without_fork? + ] validate :validate_fork, unless: :closed_or_merged_without_fork? - validate :validate_target_project, on: :create, unless: :importing? - validate :validate_reviewer_size_length, unless: :importing? + validate :validate_target_project, on: :create, unless: :importing_or_transitioning? + validate :validate_reviewer_size_length, unless: :importing_or_transitioning? scope :by_source_or_target_branch, ->(branch_name) do where("source_branch = :branch OR target_branch = :branch", branch: branch_name) @@ -371,6 +381,7 @@ class MergeRequest < ApplicationRecord scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) } scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) } + scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker scope :by_target_branch_wildcard, ->(wildcard_branch_name) do where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%')) @@ -550,13 +561,9 @@ class MergeRequest < ApplicationRecord end def merge_pipeline - return unless merged? - - # When the merge_method is :merge there will be a merge_commit_sha, however - # when it is fast-forward there is no merge commit, so we must fall back to - # either the squash commit (if the MR was squashed) or the diff head commit. - sha = merge_commit_sha || squash_commit_sha || diff_head_sha - target_project.latest_pipeline(target_branch, sha) + if sha = merged_commit_sha + target_project.latest_pipeline(target_branch, sha) + end end def head_pipeline_active? @@ -632,7 +639,7 @@ class MergeRequest < ApplicationRecord end end - DRAFT_REGEX = /\A*#{Gitlab::Regex.merge_request_draft}+\s*/i.freeze + DRAFT_REGEX = /\A*#{Gitlab::Regex.merge_request_draft}+\s*/i def self.draft?(title) !!(title =~ DRAFT_REGEX) @@ -734,6 +741,12 @@ class MergeRequest < ApplicationRecord true end + def supports_lock_on_merge? + return false unless merged? + + project.supports_lock_on_merge? + end + # Calls `MergeWorker` to proceed with the merge process and # updates `merge_jid` with the MergeWorker#jid. # This helps tracking enqueued and ongoing merge jobs. @@ -1218,7 +1231,7 @@ class MergeRequest < ApplicationRecord } end - def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false) + def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false) return false unless mergeable_state?( skip_ci_check: skip_ci_check, skip_discussions_check: skip_discussions_check, @@ -1227,7 +1240,7 @@ class MergeRequest < ApplicationRecord check_mergeability(sync_retry_lease: check_mergeability_retry_lease) - can_be_merged? && !should_be_rebased? + can_be_merged? && (!should_be_rebased? || skip_rebase_check) end def mergeability_checks @@ -1593,7 +1606,7 @@ class MergeRequest < ApplicationRecord # Since another process checks for matching merge request, we need # to make it possible to detect whether the query should go to the # primary. - target_project.mark_primary_write_location + target_project.sticking.stick(:project, target_project.id) end def diverged_commits_count @@ -1654,6 +1667,7 @@ class MergeRequest < ApplicationRecord variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present? variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present? + variables.append(key: 'CI_MERGE_REQUEST_SQUASH_ON_MERGE', value: squash_on_merge?.to_s) variables.concat(source_project_variables) end end @@ -1831,7 +1845,7 @@ class MergeRequest < ApplicationRecord def merged_commit_sha return unless merged? - sha = merge_commit_sha || squash_commit_sha || diff_head_sha + sha = super || merge_commit_sha || squash_commit_sha || diff_head_sha sha.presence end @@ -1996,7 +2010,7 @@ class MergeRequest < ApplicationRecord all_pipelines.for_sha_or_source_sha(diff_head_sha).first end - def etag_caching_enabled? + def real_time_notes_enabled? true end @@ -2097,6 +2111,10 @@ class MergeRequest < ApplicationRecord spammable_attribute_changed? && project.public? end + def missing_required_squash? + !squash && target_project.squash_always? + end + private attr_accessor :skip_fetch_ref @@ -2141,6 +2159,7 @@ class MergeRequest < ApplicationRecord variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path) variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url) variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s) + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED', value: ProtectedBranch.protected?(source_project, source_branch).to_s) end end diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb deleted file mode 100644 index ac0fcb41089..00000000000 --- a/app/models/metrics/dashboard/annotation.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Metrics - module Dashboard - class Annotation < ApplicationRecord - include DeleteWithLimit - - self.table_name = 'metrics_dashboard_annotations' - - validates :starting_at, presence: true - validates :description, presence: true, length: { maximum: 255 } - validates :dashboard_path, presence: true, length: { maximum: 255 } - validates :panel_xid, length: { maximum: 255 } - validate :ending_at_after_starting_at - - scope :after, ->(after) { where('starting_at >= ?', after) } - scope :before, ->(before) { where('starting_at <= ?', before) } - - scope :for_dashboard, ->(dashboard_path) { where(dashboard_path: dashboard_path) } - scope :ending_before, ->(timestamp) { where('COALESCE(ending_at, starting_at) < ?', timestamp) } - - private - - # If annotation has NULL in ending_at column that indicates, that this annotation IS TIED TO SINGLE POINT - # IN TIME designated by starting_at timestamp. It does NOT mean that annotation is ever going starting from - # stating_at timestamp - def ending_at_after_starting_at - return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at - - errors.add(:ending_at, s_("MetricsDashboardAnnotation|can't be before starting_at time")) - end - end - end -end diff --git a/app/models/metrics/users_starred_dashboard.rb b/app/models/metrics/users_starred_dashboard.rb deleted file mode 100644 index 07748eb1431..00000000000 --- a/app/models/metrics/users_starred_dashboard.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Metrics - class UsersStarredDashboard < ApplicationRecord - self.table_name = 'metrics_users_starred_dashboards' - - belongs_to :user, inverse_of: :metrics_users_starred_dashboards - belongs_to :project, inverse_of: :metrics_users_starred_dashboards - - validates :user_id, presence: true - validates :project_id, presence: true - validates :dashboard_path, presence: true, length: { maximum: 255 } - validates :dashboard_path, uniqueness: { scope: %i[user_id project_id] } - - scope :for_project, ->(project) { where(project: project) } - scope :for_project_dashboard, ->(project, path) { for_project(project).where(dashboard_path: path) } - end -end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 8de717fb61d..eb0da368c7b 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -134,7 +134,9 @@ class Milestone < ApplicationRecord end def participants - User.joins(assigned_issues: :milestone).where(milestones: { id: id }).distinct + User.joins(assigned_issues: :milestone) + .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422155') + .where(milestones: { id: id }).distinct end def self.sort_by_attribute(method) diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb index 6d0e7c35865..e7fcde2cb5c 100644 --- a/app/models/ml/model_version.rb +++ b/app/models/ml/model_version.rb @@ -14,7 +14,7 @@ module Ml belongs_to :model, class_name: 'Ml::Model' belongs_to :project - belongs_to :package, class_name: 'Packages::Package', optional: true + belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true delegate :name, to: :model diff --git a/app/models/namespace.rb b/app/models/namespace.rb index a7d03c3688a..ea0ea4de5b5 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -17,6 +17,9 @@ class Namespace < ApplicationRecord include BlocksUnsafeSerialization include Ci::NamespaceSettings include Referable + include CrossDatabaseIgnoredTables + + cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277' # Tells ActiveRecord not to store the full class name, in order to save some space # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794 @@ -145,7 +148,6 @@ class Namespace < ApplicationRecord after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? } after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? } after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) } - after_destroy :rm_dir after_save :reload_namespace_details @@ -155,7 +157,6 @@ class Namespace < ApplicationRecord # Legacy Storage specific hooks - before_destroy(prepend: true) { prepare_for_destroy } after_commit :expire_child_caches, on: :update, if: -> { Feature.enabled?(:cached_route_lookups, self, type: :ops) && saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id? @@ -166,7 +167,9 @@ class Namespace < ApplicationRecord scope :sort_by_type, -> { order(arel_table[:type].asc.nulls_first) } scope :include_route, -> { includes(:route) } scope :by_parent, -> (parent) { where(parent_id: parent) } + scope :by_root_id, -> (root_id) { where('traversal_ids[1] IN (?)', root_id) } scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) } + scope :in_organization, -> (organization) { where(organization: organization) } scope :with_statistics, -> do joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id') @@ -231,16 +234,26 @@ class Namespace < ApplicationRecord # query - The search query as a String. # # Returns an ActiveRecord::Relation. - def search(query, include_parents: false, use_minimum_char_limit: true) + def search(query, include_parents: false, use_minimum_char_limit: true, exact_matches_first: false) if include_parents - without_project_namespaces + route_columns = [Route.arel_table[:path], Route.arel_table[:name]] + namespaces = without_project_namespaces .where(id: Route.for_routable_type(Namespace.name) .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") - .fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]], + .fuzzy_search(query, route_columns, use_minimum_char_limit: use_minimum_char_limit) .select(:source_id)) + + if exact_matches_first + namespaces = namespaces + .joins(:route) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") + .order(exact_matches_first_sql(query, route_columns)) + end + + namespaces else - without_project_namespaces.fuzzy_search(query, [:path, :name], use_minimum_char_limit: use_minimum_char_limit) + without_project_namespaces.fuzzy_search(query, [:path, :name], use_minimum_char_limit: use_minimum_char_limit, exact_matches_first: exact_matches_first) end end @@ -465,7 +478,7 @@ class Namespace < ApplicationRecord return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil? strong_memoize(:first_auto_devops_config) do - if has_parent? + if parent.present? Rails.cache.fetch(first_auto_devops_config_cache_key_for(id), expires_in: 1.day) do parent.first_auto_devops_config end @@ -751,7 +764,7 @@ class Namespace < ApplicationRecord end def reload_namespace_details - return unless !project_namespace? && (previous_changes.keys & %w(description description_html cached_markdown_version)).any? && namespace_details.present? + return unless !project_namespace? && (previous_changes.keys & %w[description description_html cached_markdown_version]).any? && namespace_details.present? namespace_details.reset end diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb index 6c825b5364f..a65027733e9 100644 --- a/app/models/namespace/detail.rb +++ b/app/models/namespace/detail.rb @@ -3,7 +3,6 @@ class Namespace::Detail < ApplicationRecord include IgnorableColumns - ignore_column :free_user_cap_over_limt_notified_at, remove_with: '15.7', remove_after: '2022-11-22' ignore_column :dashboard_notification_at, remove_with: '16.5', remove_after: '2023-08-22' ignore_column :dashboard_enforcement_at, remove_with: '16.5', remove_after: '2023-08-22' ignore_column :next_over_limit_check_at, remove_with: '16.5', remove_after: '2023-08-22' diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb index 8af0cf2767c..1d11bcb574c 100644 --- a/app/models/namespace/root_storage_statistics.rb +++ b/app/models/namespace/root_storage_statistics.rb @@ -2,7 +2,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord SNIPPETS_SIZE_STAT_NAME = 'snippets_size' - STATISTICS_ATTRIBUTES = %W( + STATISTICS_ATTRIBUTES = %W[ storage_size repository_size wiki_size @@ -12,7 +12,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord #{SNIPPETS_SIZE_STAT_NAME} pipeline_artifacts_size uploads_size - ).freeze + ].freeze self.primary_key = :namespace_id @@ -36,7 +36,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord end def self.namespace_statistics_attributes - %w(storage_size dependency_proxy_size) + %w[storage_size dependency_proxy_size] end private diff --git a/app/models/namespace/traversal_hierarchy.rb b/app/models/namespace/traversal_hierarchy.rb index d2de85b5dd4..86fb562f4f4 100644 --- a/app/models/namespace/traversal_hierarchy.rb +++ b/app/models/namespace/traversal_hierarchy.rb @@ -39,9 +39,16 @@ class Namespace AND namespaces.traversal_ids::bigint[] <> cte.traversal_ids SQL - Namespace.transaction do - @root.lock!("FOR NO KEY UPDATE") - Namespace.connection.exec_query(sql) + # Hint: when a user is created, it also creates a Namespaces::UserNamespace in + # `ensure_namespace_correct`. This method is then called within the same + # transaction of the user INSERT. + Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction( + %w[namespaces], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424279' + ) do + Namespace.transaction do + @root.lock!("FOR NO KEY UPDATE") + Namespace.connection.exec_query(sql) + end end rescue ActiveRecord::Deadlocked db_deadlock_counter.increment(source: 'Namespace#sync_traversal_ids!') diff --git a/app/models/namespaces/randomized_suffix_path.rb b/app/models/namespaces/randomized_suffix_path.rb index 586d7bff5c3..b22ba789688 100644 --- a/app/models/namespaces/randomized_suffix_path.rb +++ b/app/models/namespaces/randomized_suffix_path.rb @@ -3,7 +3,7 @@ module Namespaces class RandomizedSuffixPath MAX_TRIES = 4 - LEADING_ZEROS = /^0+/.freeze + LEADING_ZEROS = /^0+/ def initialize(path) @path = path diff --git a/app/models/note.rb b/app/models/note.rb index f1760a8dc4a..8fc45436dc7 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -28,7 +28,7 @@ class Note < ApplicationRecord ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' - ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/.freeze + ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/ cache_markdown_field :note, pipeline: :note, issuable_reference_expansion_enabled: true @@ -74,6 +74,7 @@ class Note < ApplicationRecord attr_mentionable :note, pipeline: :note participant :author + belongs_to :namespace belongs_to :project belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :author, class_name: "User" @@ -104,6 +105,7 @@ class Note < ApplicationRecord validates :note, presence: true validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT } validates :project, presence: true, if: :for_project_noteable? + validates :namespace, presence: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -169,7 +171,7 @@ class Note < ApplicationRecord end end - scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) } + scope :diff_notes, -> { where(type: %w[LegacyDiffNote DiffNote]) } scope :new_diff_notes, -> { where(type: 'DiffNote') } scope :non_diff_notes, -> { where(type: NON_DIFF_NOTE_TYPES) } @@ -193,7 +195,7 @@ class Note < ApplicationRecord scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) } scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) } - before_validation :nullify_blank_type, :nullify_blank_line_code + before_validation :ensure_namespace_id, :nullify_blank_type, :nullify_blank_line_code # Syncs `confidential` with `internal` as we rename the column. # https://gitlab.com/gitlab-org/gitlab/-/issues/367923 before_create :set_internal_flag @@ -205,7 +207,7 @@ class Note < ApplicationRecord after_commit :trigger_note_subscription_create, on: :create after_commit :trigger_note_subscription_update, on: :update after_commit :trigger_note_subscription_destroy, on: :destroy - after_commit :expire_etag_cache, unless: :importing? + after_commit :broadcast_noteable_notes_changed, unless: :importing? def trigger_note_subscription_create return unless trigger_note_subscription? @@ -589,8 +591,8 @@ class Note < ApplicationRecord update_columns(attributes_to_update) end - def expire_etag_cache - noteable&.expire_note_etag_cache + def broadcast_noteable_notes_changed + noteable&.broadcast_notes_changed end def touch(*args, **kwargs) @@ -825,6 +827,16 @@ class Note < ApplicationRecord project.repository.keep_around(self.commit_id) end + def ensure_namespace_id + return if namespace_id.present? && !noteable_changed? && !project_changed? + + self.namespace_id = if for_project_noteable? + project&.project_namespace_id + elsif for_personal_snippet? + noteable&.author&.namespace&.id + end + end + def nullify_blank_type self.type = nil if self.type.blank? end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index cde7b92e74a..eb4fa9ac474 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -60,7 +60,7 @@ class NotificationSetting < ApplicationRecord end def self.allowed_fields(source = nil) - NotificationSetting.email_events(source).dup + %i(level notification_email) + NotificationSetting.email_events(source).dup + %i[level notification_email] end def email_events diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 01db0a5cf8b..b93537e0d1e 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -52,7 +52,7 @@ module Operations class << self def preload_relations - preload(strategies: :scopes) + preload(strategies: [:scopes, :user_list]) end def for_unleash_client(project, environment) diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index 9f2119949fb..893b08d7872 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -10,6 +10,7 @@ module Organizations has_many :namespaces has_many :groups + has_many :projects has_one :settings, class_name: "OrganizationSetting" @@ -38,7 +39,7 @@ module Organizations end def user?(user) - users.exists?(user.id) + organization_users.exists?(user: user) end private diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb index 2b8d0a4f51e..1fe4e28146e 100644 --- a/app/models/packages/debian.rb +++ b/app/models/packages/debian.rb @@ -4,13 +4,13 @@ module Packages module Debian TEMPORARY_PACKAGE_NAME = 'debian-temporary-package' - DISTRIBUTION_REGEX = %r{[a-z0-9][a-z0-9.-]*}i.freeze + DISTRIBUTION_REGEX = %r{[a-z0-9][a-z0-9.-]*}i COMPONENT_REGEX = DISTRIBUTION_REGEX.freeze - ARCHITECTURE_REGEX = %r{[a-z0-9][-a-z0-9]*}.freeze + ARCHITECTURE_REGEX = %r{[a-z0-9][-a-z0-9]*} - LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze + LETTER_REGEX = %r{(lib)?[a-z0-9]} - EMPTY_FILE_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'.freeze + EMPTY_FILE_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' INCOMING_PACKAGE_NAME = 'incoming' diff --git a/app/models/packages/debian/file_entry.rb b/app/models/packages/debian/file_entry.rb index 7ea0dfe8765..4ac621dcbd4 100644 --- a/app/models/packages/debian/file_entry.rb +++ b/app/models/packages/debian/file_entry.rb @@ -6,7 +6,7 @@ module Packages include ActiveModel::Model DIGESTS = %i[md5 sha1 sha256].freeze - FILENAME_REGEX = %r{\A[a-zA-Z0-9][a-zA-Z0-9_.~+-]*\z}.freeze + FILENAME_REGEX = %r{\A[a-zA-Z0-9][a-zA-Z0-9_.~+-]*\z} attr_accessor :filename, :size, diff --git a/app/models/packages/dependency_link.rb b/app/models/packages/dependency_link.rb index 51018602bdc..400b4cce208 100644 --- a/app/models/packages/dependency_link.rb +++ b/app/models/packages/dependency_link.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class Packages::DependencyLink < ApplicationRecord + include EachBatch + belongs_to :package, inverse_of: :dependency_links belongs_to :dependency, inverse_of: :dependency_links, class_name: 'Packages::Dependency' has_one :nuget_metadatum, inverse_of: :dependency_link, class_name: 'Packages::Nuget::DependencyLinkMetadatum' @@ -14,6 +16,32 @@ class Packages::DependencyLink < ApplicationRecord scope :with_dependency_type, ->(dependency_type) { where(dependency_type: dependency_type) } scope :includes_dependency, -> { includes(:dependency) } scope :for_package, ->(package) { where(package_id: package.id) } + scope :for_packages, ->(packages) { where(package: packages) } scope :preload_dependency, -> { preload(:dependency) } scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) } + scope :select_dependency_id, -> { select(:dependency_id) } + + def self.dependency_ids_grouped_by_type(packages) + inner_query = where(package_id: packages) + .select(' + package_id, + dependency_type, + ARRAY_AGG(dependency_id) as dependency_ids + ') + .group(:package_id, :dependency_type) + + cte = Gitlab::SQL::CTE.new(:dependency_links_cte, inner_query) + cte_alias = cte.table.alias(table_name) + + with(cte.to_arel) + .select(' + package_id, + JSON_OBJECT_AGG( + dependency_type, + dependency_ids + ) AS dependency_ids_by_type + ') + .from(cte_alias) + .group(:package_id) + end end diff --git a/app/models/packages/ml_model/package.rb b/app/models/packages/ml_model/package.rb new file mode 100644 index 00000000000..de2b5f8f2a8 --- /dev/null +++ b/app/models/packages/ml_model/package.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Packages + module MlModel + class Package < Packages::Package + has_one :model_version, class_name: "Ml::ModelVersion", inverse_of: :package + + validates :name, + format: Gitlab::Regex.ml_model_name_regex, + presence: true, + length: { maximum: 255 } + + validates :version, + format: Gitlab::Regex.semver_regex, + presence: true, + length: { maximum: 255 } + end + end +end diff --git a/app/models/packages/nuget/metadatum.rb b/app/models/packages/nuget/metadatum.rb index e7cf4528f16..1025af0fd24 100644 --- a/app/models/packages/nuget/metadatum.rb +++ b/app/models/packages/nuget/metadatum.rb @@ -15,8 +15,7 @@ class Packages::Nuget::Metadatum < ApplicationRecord validates :icon_url, public_url: { allow_blank: true }, length: { maximum: MAX_URL_LENGTH } validates :authors, presence: true, length: { maximum: MAX_AUTHORS_LENGTH } validates :description, presence: true, length: { maximum: MAX_DESCRIPTION_LENGTH } - validates :normalized_version, presence: true, - if: -> { Feature.enabled?(:nuget_normalized_version, package&.project) } + validates :normalized_version, presence: true validate :ensure_nuget_package_type diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb new file mode 100644 index 00000000000..643b5552d84 --- /dev/null +++ b/app/models/packages/nuget/symbol.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Packages + module Nuget + class Symbol < ApplicationRecord + include FileStoreMounter + + belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_symbols + + delegate :project_id, to: :package + + validates :package, :file, :file_path, :signature, :object_storage_key, :size, presence: true + validates :signature, uniqueness: { scope: :file_path } + validates :object_storage_key, uniqueness: true + + mount_file_store_uploader SymbolUploader + + before_validation :set_object_storage_key, on: :create + + private + + def set_object_storage_key + return unless project_id && signature + + self.object_storage_key = Gitlab::HashedPath.new( + 'packages', 'nuget', package_id, 'symbols', OpenSSL::Digest::SHA256.hexdigest(signature), + root_hash: project_id + ).to_s + end + end + end +end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index b09911f4216..02e3908b3bf 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -7,6 +7,7 @@ class Packages::Package < ApplicationRecord include Gitlab::Utils::StrongMemoize include Packages::Installable include Packages::Downloadable + include EnumInheritance DISPLAYABLE_STATUSES = [:default, :error].freeze INSTALLABLE_STATUSES = [:default, :hidden].freeze @@ -48,6 +49,7 @@ class Packages::Package < ApplicationRecord has_one :pypi_metadatum, inverse_of: :package, class_name: 'Packages::Pypi::Metadatum' has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum' has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum' + has_many :nuget_symbols, inverse_of: :package, class_name: 'Packages::Nuget::Symbol' has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum' has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum' has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum' @@ -179,11 +181,7 @@ class Packages::Package < ApplicationRecord scope :preload_conan_metadatum, -> { preload(:conan_metadatum) } scope :with_npm_scope, ->(scope) do - if Feature.enabled?(:npm_package_registry_fix_group_path_validation) - npm.where("position('/' in packages_packages.name) > 0 AND split_part(packages_packages.name, '/', 1) = :package_scope", package_scope: "@#{sanitize_sql_like(scope)}") - else - npm.where("name ILIKE :package_name", package_name: "@#{sanitize_sql_like(scope)}/%") - end + npm.where("position('/' in packages_packages.name) > 0 AND split_part(packages_packages.name, '/', 1) = :package_scope", package_scope: "@#{sanitize_sql_like(scope)}") end scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } @@ -220,6 +218,12 @@ class Packages::Package < ApplicationRecord joins(:project).reorder(keyset_order) end + def self.inheritance_column = 'package_type' + + def self.inheritance_column_to_class_map = { + ml_model: 'Packages::MlModel::Package' + }.freeze + def self.only_maven_packages_with_path(path, use_cte: false) if use_cte # This is an optimization fence which assumes that looking up the Metadatum record by path (globally) diff --git a/app/models/packages/protection.rb b/app/models/packages/protection.rb new file mode 100644 index 00000000000..ebaecf89992 --- /dev/null +++ b/app/models/packages/protection.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Packages + module Protection + def self.table_name_prefix + 'packages_protection_' + end + end +end diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb new file mode 100644 index 00000000000..bb65be92b90 --- /dev/null +++ b/app/models/packages/protection/rule.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Packages + module Protection + class Rule < ApplicationRecord + enum package_type: Packages::Package.package_types.slice(:npm) + + belongs_to :project, inverse_of: :package_protection_rules + + validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] }, + length: { maximum: 255 } + validates :package_type, presence: true + validates :push_protected_up_to_access_level, presence: true, + inclusion: { in: [ + Gitlab::Access::DEVELOPER, + Gitlab::Access::MAINTAINER, + Gitlab::Access::OWNER + ] } + end + end +end diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index 2ffb2e84cbf..e8becc833ca 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -35,7 +35,7 @@ module Pages { type: 'zip', path: deployment.file.url_or_file_path( - expire_at: ::Gitlab::Pages::CacheControl::DEPLOYMENT_EXPIRATION.from_now + expire_at: ::Gitlab::Pages::DEPLOYMENT_EXPIRATION.from_now ), global_id: global_id, sha256: deployment.file_sha256, diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb index fafbe449c8c..0a64e91bf60 100644 --- a/app/models/pages/virtual_domain.rb +++ b/app/models/pages/virtual_domain.rb @@ -2,9 +2,8 @@ module Pages class VirtualDomain - def initialize(projects:, cache: nil, trim_prefix: nil, domain: nil) + def initialize(projects:, trim_prefix: nil, domain: nil) @projects = projects - @cache = cache @trim_prefix = trim_prefix @domain = domain end @@ -18,23 +17,19 @@ module Pages end def lookup_paths - paths = projects.map do |project| - project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain) - end - - # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715 - paths = paths.select(&:source) - - paths.sort_by(&:prefix).reverse - end - - # cache_key is required by #present_cached in ::API::Internal::Pages - def cache_key - @cache_key ||= cache&.cache_key + projects + .map { |project| lookup_paths_for(project) } + .select(&:source) # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715 + .sort_by(&:prefix) + .reverse end private - attr_reader :projects, :trim_prefix, :domain, :cache + attr_reader :projects, :trim_prefix, :domain + + def lookup_paths_for(project) + Pages::LookupPath.new(project, trim_prefix: trim_prefix, domain: domain) + end end end diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index ec2293fa032..de7b2416258 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -11,13 +11,16 @@ class PagesDeployment < ApplicationRecord attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store } belongs_to :project, optional: false + + # ci_build is optional, because PagesDeployment must live even if its build/pipeline is removed. belongs_to :ci_build, class_name: 'Ci::Build', optional: true - scope :older_than, -> (id) { where('id < ?', id) } + scope :older_than, ->(id) { where('id < ?', id) } scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) } scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) } scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) } scope :project_id_in, ->(ids) { where(project_id: ids) } + scope :active, -> { where(deleted_at: nil) } validates :file, presence: true validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } @@ -32,6 +35,14 @@ class PagesDeployment < ApplicationRecord skip_callback :save, :after, :store_file! after_commit :store_file_after_commit!, on: [:create, :update] + def self.deactivate_deployments_older_than(deployment, time: nil) + now = Time.now.utc + active + .older_than(deployment.id) + .where(project_id: deployment.project_id, path_prefix: deployment.path_prefix) + .update_all(updated_at: now, deleted_at: time || now) + end + def migrated? file.filename == MIGRATED_FILE_NAME end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 88d7f0f972a..b86bc761cc1 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -9,6 +9,8 @@ class PagesDomain < ApplicationRecord VERIFICATION_THRESHOLD = 3.days.freeze SSL_RENEWAL_THRESHOLD = 30.days.freeze + MAX_CERTIFICATE_KEY_LENGTH = 8192 + enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages @@ -34,6 +36,7 @@ class PagesDomain < ApplicationRecord validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? } validate :validate_custom_domain_count_per_project, on: :create + validate :max_certificate_key_length, if: ->(domain) { domain.key.present? } attribute :auto_ssl_enabled, default: -> { ::Gitlab::LetsEncrypt.enabled? } attribute :wildcard, default: false @@ -234,6 +237,16 @@ class PagesDomain < ApplicationRecord private + def max_certificate_key_length + return unless pkey.is_a?(OpenSSL::PKey::RSA) + return if pkey.to_s.bytesize <= MAX_CERTIFICATE_KEY_LENGTH + + errors.add( + :key, + s_("PagesDomain|Certificate Key is too long. (Max %d bytes)") % MAX_CERTIFICATE_KEY_LENGTH + ) + end + def set_verification_code return if self.verification_code.present? diff --git a/app/models/performance_monitoring/prometheus_metric.rb b/app/models/performance_monitoring/prometheus_metric.rb deleted file mode 100644 index d67b1809d93..00000000000 --- a/app/models/performance_monitoring/prometheus_metric.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module PerformanceMonitoring - class PrometheusMetric - include ActiveModel::Model - - attr_accessor :id, :unit, :label, :query, :query_range - - validates :unit, presence: true - validates :query, presence: true, unless: :query_range - validates :query_range, presence: true, unless: :query - - class << self - def from_json(json_content) - build_from_hash(json_content).tap(&:validate!) - end - - private - - def build_from_hash(attributes) - return new unless attributes.is_a?(Hash) - - new( - id: attributes['id'], - unit: attributes['unit'], - label: attributes['label'], - query: attributes['query'], - query_range: attributes['query_range'] - ) - end - end - end -end diff --git a/app/models/performance_monitoring/prometheus_panel.rb b/app/models/performance_monitoring/prometheus_panel.rb deleted file mode 100644 index b33c09001ae..00000000000 --- a/app/models/performance_monitoring/prometheus_panel.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module PerformanceMonitoring - class PrometheusPanel - include ActiveModel::Model - - attr_accessor :type, :title, :y_label, :weight, :metrics, :y_axis, :max_value - - validates :title, presence: true - validates :metrics, array_members: { member_class: PerformanceMonitoring::PrometheusMetric } - - class << self - def from_json(json_content) - build_from_hash(json_content).tap(&:validate!) - end - - private - - def build_from_hash(attributes) - return new unless attributes.is_a?(Hash) - - new( - type: attributes['type'], - title: attributes['title'], - y_label: attributes['y_label'], - weight: attributes['weight'], - metrics: initialize_children_collection(attributes['metrics']) - ) - end - - def initialize_children_collection(children) - return unless children.is_a?(Array) - - children.map { |metrics| PerformanceMonitoring::PrometheusMetric.from_json(metrics) } - end - end - - def id(group_title) - Digest::SHA2.hexdigest([group_title, type, title].join) - end - end -end diff --git a/app/models/performance_monitoring/prometheus_panel_group.rb b/app/models/performance_monitoring/prometheus_panel_group.rb deleted file mode 100644 index 7f3d2a1b8f4..00000000000 --- a/app/models/performance_monitoring/prometheus_panel_group.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module PerformanceMonitoring - class PrometheusPanelGroup - include ActiveModel::Model - - attr_accessor :group, :priority, :panels - - validates :group, presence: true - validates :panels, array_members: { member_class: PerformanceMonitoring::PrometheusPanel } - - class << self - def from_json(json_content) - build_from_hash(json_content).tap(&:validate!) - end - - private - - def build_from_hash(attributes) - return new unless attributes.is_a?(Hash) - - new( - group: attributes['group'], - priority: attributes['priority'], - panels: initialize_children_collection(attributes['panels']) - ) - end - - def initialize_children_collection(children) - return unless children.is_a?(Array) - - children.map { |panels| PerformanceMonitoring::PrometheusPanel.from_json(panels) } - end - end - end -end diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 08f725de980..4dfe7252a0c 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -14,7 +14,7 @@ class PersonalAccessToken < ApplicationRecord format_with_prefix: :prefix_from_application_current_settings # PATs are 20 characters + optional configurable settings prefix (0..20) - TOKEN_LENGTH_RANGE = (20..40).freeze + TOKEN_LENGTH_RANGE = (20..40) MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS = 365 serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/plan.rb b/app/models/plan.rb index 22c1201421c..9ab22bc045a 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -5,6 +5,8 @@ class Plan < MainClusterwide::ApplicationRecord has_one :limits, class_name: 'PlanLimits' + scope :by_name, ->(name) { where(name: name) } + ALL_PLANS = [DEFAULT].freeze DEFAULT_PLANS = [DEFAULT].freeze private_constant :ALL_PLANS, :DEFAULT_PLANS diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index bc3898fafe7..7d043bae91c 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -8,15 +8,15 @@ class PoolRepository < ApplicationRecord include AfterCommitQueue belongs_to :source_project, class_name: 'Project' - validates :source_project, presence: true has_many :member_projects, class_name: 'Project' after_create :set_disk_path scope :by_source_project, ->(project) { where(source_project: project) } - scope :by_source_project_and_shard_name, ->(project, shard_name) do - by_source_project(project) + scope :by_disk_path, ->(disk_path) { where(disk_path: disk_path) } + scope :by_disk_path_and_shard_name, ->(disk_path, shard_name) do + by_disk_path(disk_path) .for_repository_storage(shard_name) end @@ -101,8 +101,8 @@ class PoolRepository < ApplicationRecord @object_pool ||= Gitlab::Git::ObjectPool.new( shard.name, disk_path + '.git', - source_project.repository.raw, - source_project.full_path + source_project&.repository&.raw, + source_project&.full_path ) end diff --git a/app/models/project.rb b/app/models/project.rb index ad8757880fd..68196f0a757 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -44,9 +44,12 @@ class Project < ApplicationRecord include IssueParent include UpdatedAtFilterable include IgnorableColumns + include CrossDatabaseIgnoredTables ignore_column :emails_disabled, remove_with: '16.3', remove_after: '2023-08-22' + cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277' + extend Gitlab::Cache::RequestCache extend Gitlab::Utils::Override @@ -68,10 +71,10 @@ class Project < ApplicationRecord }.freeze VALID_IMPORT_PORTS = [80, 443].freeze - VALID_IMPORT_PROTOCOLS = %w(http https git).freeze + VALID_IMPORT_PROTOCOLS = %w[http https git].freeze VALID_MIRROR_PORTS = [22, 80, 443].freeze - VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze + VALID_MIRROR_PROTOCOLS = %w[http https ssh git].freeze SORTING_PREFERENCE_FIELD = :projects_sort MAX_BUILD_TIMEOUT = 1.month @@ -81,6 +84,8 @@ class Project < ApplicationRecord MAX_SUGGESTIONS_TEMPLATE_LENGTH = 255 MAX_COMMIT_TEMPLATE_LENGTH = 500 + INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET = 5 + DEFAULT_MERGE_COMMIT_TEMPLATE = <<~MSG.rstrip.freeze Merge branch '%{source_branch}' into '%{target_branch}' @@ -163,6 +168,7 @@ class Project < ApplicationRecord # Relations belongs_to :pool_repository belongs_to :creator, class_name: 'User' + belongs_to :organization, class_name: 'Organizations::Organization' belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id' belongs_to :namespace # Sync deletion via DB Trigger to ensure we do not have @@ -265,6 +271,9 @@ class Project < ApplicationRecord dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :npm_metadata_caches, class_name: 'Packages::Npm::MetadataCache' has_one :packages_cleanup_policy, class_name: 'Packages::Cleanup::Policy', inverse_of: :project + has_many :package_protection_rules, + class_name: 'Packages::Protection::Rule', + inverse_of: :project has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -273,7 +282,6 @@ class Project < ApplicationRecord has_one :project_repository, inverse_of: :project has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting' has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting' - has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting' has_one :grafana_integration, inverse_of: :project has_one :project_setting, inverse_of: :project, autosave: true has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting' @@ -336,7 +344,15 @@ class Project < ApplicationRecord primary_key: :project_namespace_id, foreign_key: :member_namespace_id, inverse_of: :project, class_name: 'ProjectMember' - has_many :users, through: :project_members + has_many :users, -> { allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405") }, + through: :project_members + has_many :maintainers, + -> do + allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405") + .where(members: { access_level: Gitlab::Access::MAINTAINER }) + end, + through: :project_members, + source: :user has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id @@ -370,8 +386,6 @@ class Project < ApplicationRecord has_many :prometheus_metrics has_many :prometheus_alerts, inverse_of: :project has_many :prometheus_alert_events, inverse_of: :project - has_many :self_managed_prometheus_alert_events, inverse_of: :project - has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :project has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project has_many :alert_management_http_integrations, class_name: 'AlertManagement::HttpIntegration', inverse_of: :project @@ -476,7 +490,6 @@ class Project < ApplicationRecord accepts_nested_attributes_for :incident_management_setting, update_only: true accepts_nested_attributes_for :error_tracking_setting, update_only: true - accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true accepts_nested_attributes_for :prometheus_integration, update_only: true accepts_nested_attributes_for :alerting_setting, update_only: true @@ -492,11 +505,6 @@ class Project < ApplicationRecord delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role end - with_options to: :metrics_setting, allow_nil: true, prefix: true do - delegate :external_dashboard_url - delegate :dashboard_timezone - end - with_options to: :namespace do delegate :actual_limits, :actual_plan_name, :actual_plan, :root_ancestor, allow_nil: true delegate :maven_package_requests_forwarding, :pypi_package_requests_forwarding, :npm_package_requests_forwarding @@ -1282,7 +1290,7 @@ class Project < ApplicationRecord def design_repository strong_memoize(:design_repository) do - Gitlab::GlRepository::DESIGN.repository_for(self) + find_or_create_design_management_repository.repository end end @@ -1665,7 +1673,7 @@ class Project < ApplicationRecord return unless Gitlab::Email::IncomingEmail.supports_issue_creation? && author # check since this can come from a request parameter - return unless %w(issue merge_request).include?(address_type) + return unless %w[issue merge_request].include?(address_type) author.ensure_incoming_email_token! @@ -2757,10 +2765,6 @@ class Project < ApplicationRecord [] end - def mark_primary_write_location - self.class.sticking.mark_primary_write_location(:project, self.id) - end - def toggle_ci_cd_settings!(settings_attribute) ci_cd_settings.toggle!(settings_attribute) end @@ -2842,7 +2846,7 @@ class Project < ApplicationRecord return if old_pool_repository.blank? return if pool_repository_shard_matches_repository?(old_pool_repository) - new_pool_repository = PoolRepository.by_source_project_and_shard_name(old_pool_repository.source_project, repository_storage).take! + new_pool_repository = PoolRepository.by_disk_path_and_shard_name(old_pool_repository.disk_path, repository_storage).take! update!(pool_repository: new_pool_repository) old_pool_repository.unlink_repository(repository, disconnect: !pending_delete?) @@ -2871,10 +2875,6 @@ class Project < ApplicationRecord recipients end - def pages_lookup_path(trim_prefix: nil, domain: nil) - Pages::LookupPath.new(self, trim_prefix: trim_prefix, domain: domain) - end - def closest_setting(name) setting = read_attribute(name) setting = closest_namespace_setting(name) if setting.nil? @@ -2954,10 +2954,6 @@ class Project < ApplicationRecord jira_imports.last end - def metrics_setting - super || build_metrics_setting - end - def service_desk_enabled Gitlab::ServiceDesk.enabled?(project: self) end @@ -2965,7 +2961,11 @@ class Project < ApplicationRecord alias_method :service_desk_enabled?, :service_desk_enabled def service_desk_address - service_desk_custom_address || service_desk_incoming_address + service_desk_custom_address || service_desk_system_address + end + + def service_desk_system_address + service_desk_alias_address || service_desk_incoming_address end def service_desk_incoming_address @@ -2977,7 +2977,7 @@ class Project < ApplicationRecord config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}") end - def service_desk_custom_address + def service_desk_alias_address return unless Gitlab::Email::ServiceDeskEmail.enabled? key = service_desk_setting&.project_key || default_service_desk_suffix @@ -2985,6 +2985,13 @@ class Project < ApplicationRecord Gitlab::Email::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}") end + def service_desk_custom_address + return unless Feature.enabled?(:service_desk_custom_email, self) + return unless service_desk_setting&.custom_email_enabled? + + service_desk_setting.custom_email + end + def default_service_desk_suffix "#{id}-issue-" end @@ -3261,6 +3268,10 @@ class Project < ApplicationRecord group.crm_enabled? end + def supports_lock_on_merge? + group&.supports_lock_on_merge? || ::Feature.enabled?(:enforce_locked_labels_on_merge, self, type: :ops) + end + def path_availability base, _, host = path.partition('.') @@ -3270,6 +3281,13 @@ class Project < ApplicationRecord errors.add(:path, s_('Project|already in use')) end + def instance_runner_running_jobs_count + # excluding currently started job + ::Ci::RunningBuild.instance_type.where(project_id: self.id) + .limit(INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET + 1).count - 1 + end + strong_memoize_attr :instance_runner_running_jobs_count + private # overridden in EE @@ -3483,11 +3501,11 @@ class Project < ApplicationRecord end def sync_project_namespace? - (changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present? + (changes.keys & %w[name path namespace_id namespace visibility_level shared_runners_enabled]).any? && project_namespace.present? end def reload_project_namespace_details - return unless (previous_changes.keys & %w(description description_html cached_markdown_version)).any? && project_namespace.namespace_details.present? + return unless (previous_changes.keys & %w[description description_html cached_markdown_version]).any? && project_namespace.namespace_details.present? project_namespace.namespace_details.reset end diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index 99128d3cddf..c328e7d37c8 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -11,6 +11,11 @@ class ProjectAuthorization < ApplicationRecord validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: :project }, presence: true + scope :non_guests, -> { where('access_level > ?', ::Gitlab::Access::GUEST) } + + # TODO: To be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/418205 + before_create :assign_is_unique + def self.select_from_union(relations) from_union(relations) .select(['project_id', 'MAX(access_level) AS access_level']) @@ -25,6 +30,12 @@ class ProjectAuthorization < ApplicationRecord def self.insert_all(attributes) super(attributes, unique_by: connection.schema_cache.primary_keys(table_name)) end + + private + + def assign_is_unique + self.is_unique = true + end end ProjectAuthorization.prepend_mod_with('ProjectAuthorization') diff --git a/app/models/project_authorizations/changes.rb b/app/models/project_authorizations/changes.rb index 1d717950c1c..1f0cec1a50c 100644 --- a/app/models/project_authorizations/changes.rb +++ b/app/models/project_authorizations/changes.rb @@ -90,6 +90,8 @@ module ProjectAuthorizations log_details(entire_size: attributes.size, batch_size: BATCH_SIZE) if add_delay attributes.each_slice(BATCH_SIZE) do |attributes_batch| + attributes_batch.each { |attrs| attrs[:is_unique] = true } + ProjectAuthorization.insert_all(attributes_batch) perform_delay if add_delay end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index cc9003423be..8d049b8d1b1 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -19,6 +19,7 @@ class ProjectCiCdSetting < ApplicationRecord attribute :forward_deployment_enabled, default: true attribute :separated_caches, default: true + validates :merge_trains_skip_train_allowed, inclusion: { in: [true, false] } 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 92ba02ec777..36f1e09b2ba 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -173,6 +173,10 @@ class ProjectFeature < ApplicationRecord package_registry_access_level == PUBLIC || project.public? end + def private?(feature) + access_level(feature) == PRIVATE + end + private def set_pages_access_level @@ -201,11 +205,11 @@ class ProjectFeature < ApplicationRecord self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed end - %i(merge_requests_access_level builds_access_level).each(&validator) + %i[merge_requests_access_level builds_access_level].each(&validator) end def feature_validation_exclusion - %i(pages package_registry) + %i[pages package_registry] end override :resource_member? diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb index f16d661d4bb..a7b2c40557a 100644 --- a/app/models/project_import_state.rb +++ b/app/models/project_import_state.rb @@ -132,10 +132,17 @@ class ProjectImportState < ApplicationRecord alias_method :no_import?, :none? + # This method is coupled to the repository mirror domain. + # Use with caution in the importers domain. As an alternative, use the `#completed?` method. + # See EE-override and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4697 def in_progress? scheduled? || started? end + def completed? + finished? || failed? || canceled? + end + def started? # import? does SQL work so only run it if it looks like there's an import running status == 'started' && project.import? diff --git a/app/models/project_metrics_setting.rb b/app/models/project_metrics_setting.rb deleted file mode 100644 index c66d0f52f4c..00000000000 --- a/app/models/project_metrics_setting.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class ProjectMetricsSetting < ApplicationRecord - belongs_to :project - - validates :external_dashboard_url, - allow_nil: true, - length: { maximum: 255 }, - addressable_url: { enforce_sanitization: true, ascii_only: true } - - enum dashboard_timezone: { local: 0, utc: 1 } - - def dashboard_timezone=(val) - super(val&.downcase) - end -end diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index fec951eb7fe..69d1a9f4aeb 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -3,28 +3,25 @@ class ProjectSetting < ApplicationRecord include ::Gitlab::Utils::StrongMemoize include EachBatch + include IgnorableColumns - ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze + ALLOWED_TARGET_PLATFORMS = %w[ios osx tvos watchos android].freeze belongs_to :project, inverse_of: :project_setting scope :for_projects, ->(projects) { where(project_id: projects) } - attr_encrypted :cube_api_key, - mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, - algorithm: 'aes-256-gcm', - encode: false, - encode_iv: false + ignore_columns %i[ + encrypted_product_analytics_clickhouse_connection_string + encrypted_product_analytics_clickhouse_connection_string_iv + encrypted_jitsu_administrator_password + encrypted_jitsu_administrator_password_iv + jitsu_host + jitsu_project_xid + jitsu_administrator_email + ], remove_with: '16.5', remove_after: '2023-09-22' - attr_encrypted :jitsu_administrator_password, - mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, - algorithm: 'aes-256-gcm', - encode: false, - encode_iv: false - - attr_encrypted :product_analytics_clickhouse_connection_string, + attr_encrypted :cube_api_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_32, algorithm: 'aes-256-gcm', diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 3b9b82ee094..34754f4fc95 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -80,6 +80,7 @@ class ProjectTeam # so we filter out only members of project or project's group def members_in_project_and_ancestors members.where(id: member_user_ids) + .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422405') end def members_with_access_levels(access_levels = []) diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb index 67d765a15c0..e088fe81f6e 100644 --- a/app/models/releases/link.rb +++ b/app/models/releases/link.rb @@ -8,10 +8,10 @@ module Releases # See https://gitlab.com/gitlab-org/gitlab/-/issues/218753 # Regex modified to prevent catastrophic backtracking - FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}.freeze + FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z} FILEPATH_MAX_LENGTH = 128 - validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release } + validates :url, presence: true, addressable_url: { schemes: %w[http https ftp] }, uniqueness: { scope: :release } validates :name, presence: true, uniqueness: { scope: :release } validates :filepath, uniqueness: { scope: :release }, allow_blank: true validate :filepath_format_valid? diff --git a/app/models/repository.rb b/app/models/repository.rb index b8a46f80bc7..1c27a7a64cf 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -47,27 +47,26 @@ class Repository # # For example, for entry `:commit_count` there's a method called `commit_count` which # stores its data in the `commit_count` cache key. - CACHED_METHODS = %i(size recent_objects_size commit_count readme_path contribution_guide + CACHED_METHODS = %i[size recent_objects_size commit_count readme_path contribution_guide changelog license_blob license_gitaly gitignore gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? root_ref merged_branch_names has_visible_content? issue_template_names_hash merge_request_template_names_hash - user_defined_metrics_dashboard_paths xcode_project? has_ambiguous_refs?).freeze + xcode_project? has_ambiguous_refs?].freeze # Certain method caches should be refreshed when certain types of files are # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # the corresponding methods to call for refreshing caches. METHOD_CACHES_FOR_FILE_TYPES = { - readme: %i(readme_path), + readme: %i[readme_path], changelog: :changelog, - license: %i(license_blob license_gitaly), + license: %i[license_blob license_gitaly], contributing: :contribution_guide, gitignore: :gitignore, gitlab_ci: :gitlab_ci_yml, avatar: :avatar, issue_template: :issue_template_names_hash, merge_request_template: :merge_request_template_names_hash, - metrics_dashboard: :user_defined_metrics_dashboard_paths, xcode_config: :xcode_project? }.freeze @@ -344,13 +343,13 @@ class Repository end def expire_tags_cache - expire_method_caches(%i(tag_names tag_count has_ambiguous_refs?)) + expire_method_caches(%i[tag_names tag_count has_ambiguous_refs?]) @tags = nil @tag_names_include = nil end def expire_branches_cache - expire_method_caches(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?)) + expire_method_caches(%i[branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?]) expire_protected_branches_cache @local_branches = nil @@ -363,7 +362,7 @@ class Repository end def expire_statistics_caches - expire_method_caches(%i(size recent_objects_size commit_count)) + expire_method_caches(%i[size recent_objects_size commit_count]) end def expire_all_method_caches @@ -371,7 +370,7 @@ class Repository end def expire_avatar_cache - expire_method_caches(%i(avatar)) + expire_method_caches(%i[avatar]) end # Refreshes the method caches of this repository. @@ -412,19 +411,19 @@ class Repository end def expire_root_ref_cache - expire_method_caches(%i(root_ref)) + expire_method_caches(%i[root_ref]) end # Expires the cache(s) used to determine if a repository is empty or not. def expire_emptiness_caches return unless empty? - expire_method_caches(%i(has_visible_content?)) + expire_method_caches(%i[has_visible_content?]) raw_repository.expire_has_local_branches_cache end def expire_exists_cache - expire_method_caches(%i(exists?)) + expire_method_caches(%i[exists?]) end # expire cache that doesn't depend on repository data (when expiring) @@ -628,11 +627,6 @@ class Repository end cache_method :merge_request_template_names_hash, fallback: {} - def user_defined_metrics_dashboard_paths - Gitlab::Metrics::Dashboard::RepoDashboardFinder.list_dashboards(project) - end - cache_method :user_defined_metrics_dashboard_paths, fallback: [] - def readme head_tree&.readme end @@ -1250,6 +1244,8 @@ class Repository def get_patch_id(old_revision, new_revision) raw_repository.get_patch_id(old_revision, new_revision) + rescue Gitlab::Git::CommandError + nil end def object_pool diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index 13610d37a74..d5c839724d4 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -13,8 +13,7 @@ class ResourceLabelEvent < ResourceEvent validates :label, presence: { unless: :importing? }, on: :create validate :exactly_one_issuable, unless: :importing? - after_destroy :expire_etag_cache - after_save :expire_etag_cache + after_commit :broadcast_notes_changed, unless: :importing? enum action: { add: 1, @@ -22,7 +21,7 @@ class ResourceLabelEvent < ResourceEvent } def self.issuable_attrs - %i(issue merge_request).freeze + %i[issue merge_request].freeze end def self.preload_label_subjects(events) @@ -97,8 +96,8 @@ class ResourceLabelEvent < ResourceEvent issuable.is_a?(MergeRequest) ? :project_merge_requests_url : :project_issues_url end - def expire_etag_cache - issuable.expire_note_etag_cache + def broadcast_notes_changed + issuable.broadcast_notes_changed end def local_label? diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb index 134f71e35ad..88a86258b0a 100644 --- a/app/models/resource_state_event.rb +++ b/app/models/resource_state_event.rb @@ -14,7 +14,7 @@ class ResourceStateEvent < ResourceEvent after_create :issue_usage_metrics def self.issuable_attrs - %i(issue merge_request).freeze + %i[issue merge_request].freeze end def issuable diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb index 1cc77501d8d..644ffae5749 100644 --- a/app/models/resource_timebox_event.rb +++ b/app/models/resource_timebox_event.rb @@ -16,7 +16,7 @@ class ResourceTimeboxEvent < ResourceEvent after_create :issue_usage_metrics def self.issuable_attrs - %i(issue merge_request).freeze + %i[issue merge_request].freeze end def issuable diff --git a/app/models/review.rb b/app/models/review.rb index d47aaf027ce..98e9a314df7 100644 --- a/app/models/review.rb +++ b/app/models/review.rb @@ -31,6 +31,10 @@ class Review < ApplicationRecord def user_mentions merge_request.user_mentions.where.not(note_id: nil) end + + def from_merge_request_author? + merge_request.author_id == author_id + end end Review.prepend_mod diff --git a/app/models/self_managed_prometheus_alert_event.rb b/app/models/self_managed_prometheus_alert_event.rb deleted file mode 100644 index cf26563e92d..00000000000 --- a/app/models/self_managed_prometheus_alert_event.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class SelfManagedPrometheusAlertEvent < ApplicationRecord - include AlertEventLifecycle - - belongs_to :project, validate: true, inverse_of: :self_managed_prometheus_alert_events - belongs_to :environment, validate: true, inverse_of: :self_managed_prometheus_alert_events - has_and_belongs_to_many :related_issues, class_name: 'Issue', join_table: :issues_self_managed_prometheus_alert_events # rubocop:disable Rails/HasAndBelongsToMany - - validates :started_at, presence: true - validates :payload_key, uniqueness: { scope: :project_id } - - def self.find_or_initialize_by_payload_key(project, payload_key) - find_or_initialize_by(project: project, payload_key: payload_key) do |event| - yield event if block_given? - end - end -end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index f3a0479d3b7..30c53b978f8 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class SentNotification < ApplicationRecord + include IgnorableColumns + + ignore_column %i[id_convert_to_bigint], remove_with: '16.5', remove_after: '2023-09-22' + belongs_to :project belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :recipient, class_name: "User" diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index 9139dc22a94..a262802c8af 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -5,7 +5,7 @@ class SnippetRepository < ApplicationRecord include Shardable DEFAULT_EMPTY_FILE_NAME = 'snippetfile' - EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d+)\.txt$/.freeze + EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d+)\.txt$/ CommitError = Class.new(StandardError) InvalidPathError = Class.new(CommitError) diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index ecd3e27a9c4..7caf3a1040b 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -5,7 +5,7 @@ module Terraform include UsageStatistics include AfterCommitQueue - HEX_REGEXP = %r{\A\h+\z}.freeze + HEX_REGEXP = %r{\A\h+\z} UUID_LENGTH = 32 self.locking_column = :activerecord_lock_version diff --git a/app/models/user.rb b/app/models/user.rb index 9f85d41b133..c4e867ab571 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,7 +22,6 @@ class User < MainClusterwide::ApplicationRecord include FromUnion include BatchDestroyDependentAssociations include BatchNullifyDependentAssociations - include HasUniqueInternalUsers include IgnorableColumns include UpdateHighestRole include HasUserType @@ -31,7 +30,28 @@ class User < MainClusterwide::ApplicationRecord include RestrictedSignup include StripAttribute include EachBatch - include SafelyChangeColumnDefault + include CrossDatabaseIgnoredTables + include IgnorableColumns + + ignore_column %i[ + email_opted_in + email_opted_in_ip + email_opted_in_source_id + email_opted_in_at + ], remove_with: '16.6', remove_after: '2023-10-22' + + # `ensure_namespace_correct` needs to be moved to an after_commit (?) + cross_database_ignore_tables %w[namespaces namespace_settings], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424279' + + # `notification_settings_for` is called, and elsewhere `save` is then called. + cross_database_ignore_tables %w[notification_settings], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424284' + + # Associations with dependent: option + cross_database_ignore_tables( + %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests], + url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424285', + on: :destroy + ) DEFAULT_NOTIFICATION_LEVEL = :participating @@ -55,13 +75,11 @@ class User < MainClusterwide::ApplicationRecord :public_email ].freeze - FORBIDDEN_SEARCH_STATES = %w(blocked banned ldap_blocked).freeze + FORBIDDEN_SEARCH_STATES = %w[blocked banned ldap_blocked].freeze INCOMING_MAIL_TOKEN_PREFIX = 'glimt-' FEED_TOKEN_PREFIX = 'glft-' - columns_changing_default :project_view - # lib/tasks/tokens.rake needs to be updated when changing mail and feed tokens add_authentication_token_field :incoming_email_token, token_generator: -> { self.generate_incoming_mail_token } add_authentication_token_field :feed_token, format_with_prefix: :prefix_for_feed_token @@ -262,8 +280,6 @@ class User < MainClusterwide::ApplicationRecord has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users - has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :user - has_one :status, class_name: 'UserStatus' has_one :user_preference has_one :user_detail @@ -346,7 +362,9 @@ class User < MainClusterwide::ApplicationRecord email_to_confirm.confirm end else - add_primary_email_to_emails! + ignore_cross_database_tables_if_factory_bot(%w[emails]) do + add_primary_email_to_emails! + end end end after_commit(on: :update) do @@ -378,6 +396,7 @@ class User < MainClusterwide::ApplicationRecord :gitpod_enabled, :gitpod_enabled=, :setup_for_company, :setup_for_company=, :project_shortcut_buttons, :project_shortcut_buttons=, + :keyboard_shortcuts_enabled, :keyboard_shortcuts_enabled=, :render_whitespace_in_code, :render_whitespace_in_code=, :markdown_surround_selection, :markdown_surround_selection=, :markdown_automatic_lists, :markdown_automatic_lists=, @@ -501,11 +520,19 @@ class User < MainClusterwide::ApplicationRecord end after_transition any => :active do |user| - user.starred_projects.update_counters(star_count: 1) + user.class.temporary_ignore_cross_database_tables( + %w[projects], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424278' + ) do + user.starred_projects.update_counters(star_count: 1) + end end after_transition active: any do |user| - user.starred_projects.update_counters(star_count: -1) + user.class.temporary_ignore_cross_database_tables( + %w[projects], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424278' + ) do + user.starred_projects.update_counters(star_count: -1) + end end end @@ -884,92 +911,6 @@ class User < MainClusterwide::ApplicationRecord }x end - # Return (create if necessary) the ghost user. The ghost user - # owns records previously belonging to deleted users. - def ghost - email = 'ghost%s@example.com' - unique_internal(where(user_type: :ghost), 'ghost', email) do |u| - u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.') - u.name = 'Ghost User' - end - end - - def alert_bot - email_pattern = "alert%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :alert_bot), 'alert-bot', email_pattern) do |u| - u.bio = 'The GitLab alert bot' - u.name = 'GitLab Alert Bot' - u.avatar = bot_avatar(image: 'alert-bot.png') - end - end - - def migration_bot - email_pattern = "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :migration_bot), 'migration-bot', email_pattern) do |u| - u.bio = 'The GitLab migration bot' - u.name = 'GitLab Migration Bot' - u.confirmed_at = Time.zone.now - end - end - - def security_bot - email_pattern = "security-bot%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :security_bot), 'GitLab-Security-Bot', email_pattern) do |u| - u.bio = 'System bot that monitors detected vulnerabilities for solutions and creates merge requests with the fixes.' - u.name = 'GitLab Security Bot' - u.website_url = Gitlab::Routing.url_helpers.help_page_url('user/application_security/security_bot/index.md') - u.avatar = bot_avatar(image: 'security-bot.png') - u.confirmed_at = Time.zone.now - end - end - - def support_bot - email_pattern = "support%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :support_bot), 'support-bot', email_pattern) do |u| - u.bio = 'The GitLab support bot used for Service Desk' - u.name = 'GitLab Support Bot' - u.avatar = bot_avatar(image: 'support-bot.png') - u.confirmed_at = Time.zone.now - end - end - - def automation_bot - email_pattern = "automation%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :automation_bot), 'automation-bot', email_pattern) do |u| - u.bio = 'The GitLab automation bot used for automated workflows and tasks' - u.name = 'GitLab Automation Bot' - u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for automation-bot - end - end - - def llm_bot - email_pattern = "llm-bot%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :llm_bot), 'GitLab-Llm-Bot', email_pattern) do |u| - u.bio = 'The Gitlab LLM bot used for fetching LLM-generated content' - u.name = 'GitLab LLM Bot' - u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for llm-bot - u.confirmed_at = Time.zone.now - end - end - - def admin_bot - email_pattern = "admin-bot%s@#{Settings.gitlab.host}" - - unique_internal(where(user_type: :admin_bot), 'GitLab-Admin-Bot', email_pattern) do |u| - u.bio = 'Admin bot used for tasks that require admin privileges' - u.name = 'GitLab Admin Bot' - u.avatar = bot_avatar(image: 'admin-bot.png') - u.admin = true - u.confirmed_at = Time.zone.now - end - end - # Return true if there is only single non-internal user in the deployment, # ghost user is ignored. def single_user? @@ -2009,7 +1950,7 @@ class User < MainClusterwide::ApplicationRecord def access_level=(new_level) new_level = new_level.to_s - return unless %w(admin regular).include?(new_level) + return unless %w[admin regular].include?(new_level) self.admin = (new_level == 'admin') end @@ -2175,16 +2116,6 @@ class User < MainClusterwide::ApplicationRecord [last_activity, last_sign_in].compact.max end - REQUIRES_ROLE_VALUE = 99 - - def role_required? - role_before_type_cast == REQUIRES_ROLE_VALUE - end - - def set_role_required! - update_column(:role, REQUIRES_ROLE_VALUE) - end - def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil) callout = callouts_by_feature_name[feature_name] @@ -2354,7 +2285,7 @@ class User < MainClusterwide::ApplicationRecord def ban_and_report msg = 'Potential spammer account deletion' - attrs = { user_id: id, reporter: User.security_bot, category: 'spam' } + attrs = { user_id: id, reporter: Users::Internal.security_bot, category: 'spam' } abuse_report = AbuseReport.find_by(attrs) if abuse_report.nil? @@ -2519,7 +2450,7 @@ class User < MainClusterwide::ApplicationRecord def update_highest_role? return false unless persisted? - (previous_changes.keys & %w(state user_type)).any? + (previous_changes.keys & %w[state user_type]).any? end def update_highest_role_attribute diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 425f2cc062b..15d50071bf6 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -15,6 +15,7 @@ class UserCustomAttribute < ApplicationRecord UNBLOCKED_BY = 'unblocked_by' ARKOSE_RISK_BAND = 'arkose_risk_band' AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id' + AUTO_BANNED_BY_SPAM_LOG_ID = 'auto_banned_by_spam_log_id' ALLOW_POSSIBLE_SPAM = 'allow_possible_spam' IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt' @@ -45,6 +46,14 @@ class UserCustomAttribute < ApplicationRecord upsert_custom_attributes([custom_attribute]) end + def set_banned_by_spam_log(spam_log) + return unless spam_log + + custom_attribute = { user_id: spam_log.user_id, key: AUTO_BANNED_BY_SPAM_LOG_ID, value: spam_log.id } + + upsert_custom_attributes([custom_attribute]) + end + private def blocked_users diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb index 1c7515894fe..73bca362960 100644 --- a/app/models/user_interacted_project.rb +++ b/app/models/user_interacted_project.rb @@ -24,7 +24,7 @@ class UserInteractedProject < ApplicationRecord } cached_exists?(**attributes) do - where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w(project_id user_id)) + where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w[project_id user_id]) true end end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index eac66905d0c..8fc9f4617d0 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -35,6 +35,7 @@ class UserPreference < MainClusterwide::ApplicationRecord attribute :time_display_relative, default: true attribute :render_whitespace_in_code, default: false attribute :project_shortcut_buttons, default: true + attribute :keyboard_shortcuts_enabled, default: true enum visibility_pipeline_id_type: { id: 0, iid: 1 } diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 0d3262b2474..def0765560e 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -62,7 +62,7 @@ module Users project_quality_summary_feedback: 59, # EE-only merge_request_settings_moved_callout: 60, new_top_level_group_alert: 61, - artifacts_management_page_feedback_banner: 62, + # 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314 # 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233 branch_rules_info_callout: 65, create_runner_workflow_banner: 66, @@ -71,9 +71,11 @@ module Users project_repository_limit_alert_alert_threshold: 69, # EE-only project_repository_limit_alert_error_threshold: 70, # EE-only new_navigation_callout: 71, - code_suggestions_third_party_callout: 72, # EE-only + # 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022 namespace_over_storage_users_combined_alert: 73, # EE-only - rich_text_editor: 74 + rich_text_editor: 74, + vsd_feedback_banner: 75, # EE-only + security_policy_protected_branch_modification: 76 # EE-only } validates :feature_name, diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 1b0fd8682db..086943884a5 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -16,6 +16,12 @@ module Users greater_than_or_equal_to: 0, less_than_or_equal_to: 9999 } + validates :last_digits_hash, length: { maximum: 44 } + validates :holder_name_hash, length: { maximum: 44 } + validates :expiration_date_hash, length: { maximum: 44 } + validates :network_hash, length: { maximum: 44 } + + scope :find_or_initialize_by_user, ->(user_id) { where(user_id: user_id).first_or_initialize } scope :by_banned_user, -> { joins(:banned_user) } scope :similar_by_holder_name, ->(holder_name) do if holder_name.present? @@ -32,6 +38,11 @@ module Users ) end + before_save :set_last_digits_hash, if: -> { last_digits.present? } + before_save :set_holder_name_hash, if: -> { holder_name.present? } + before_save :set_network_hash, if: -> { network.present? } + before_save :set_expiration_date_hash, if: -> { expiration_date.present? } + def similar_records self.class.similar_to(self).order(credit_card_validated_at: :desc).includes(:user) end @@ -43,5 +54,21 @@ module Users def used_by_banned_user? self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name).exists? end + + def set_last_digits_hash + self.last_digits_hash = Gitlab::CryptoHelper.sha256(last_digits) + end + + def set_holder_name_hash + self.holder_name_hash = Gitlab::CryptoHelper.sha256(holder_name.downcase) + end + + def set_network_hash + self.network_hash = Gitlab::CryptoHelper.sha256(network.downcase) + end + + def set_expiration_date_hash + self.expiration_date_hash = Gitlab::CryptoHelper.sha256(expiration_date.to_s) + end end end diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb new file mode 100644 index 00000000000..0bcfda049fc --- /dev/null +++ b/app/models/users/group_visit.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Users + class GroupVisit < ApplicationRecord + include Users::Visitable + include PartitionedTable + + self.table_name = "groups_visits" + self.primary_key = :id + + partitioned_by :visited_at, strategy: :monthly, retain_for: 3.months + + validates :entity_id, presence: true + validates :user_id, presence: true + validates :visited_at, presence: true + end +end diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb index 3964f202be6..6affe5b5030 100644 --- a/app/models/users/project_callout.rb +++ b/app/models/users/project_callout.rb @@ -11,10 +11,10 @@ module Users enum feature_name: { awaiting_members_banner: 1, # EE-only web_hook_disabled: 2, - ultimate_feature_removal_banner: 3, + # 3 was removed https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129703, + # and cleaned up https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129924, it can be replaced namespace_storage_pre_enforcement_banner: 4, # EE-only - # 5,6,7 were unused and removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118330, - # they can be replaced. + # 5,6,7 were removed https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118330, they can be replaced license_check_deprecation_alert: 8 # EE-only } diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb new file mode 100644 index 00000000000..1d076e0be56 --- /dev/null +++ b/app/models/users/project_visit.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Users + class ProjectVisit < ApplicationRecord + include Users::Visitable + include PartitionedTable + + self.table_name = "projects_visits" + self.primary_key = :id + + partitioned_by :visited_at, strategy: :monthly, retain_for: 3.months + + validates :entity_id, presence: true + validates :user_id, presence: true + validates :visited_at, presence: true + end +end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 73156b2f040..62b837eeeb6 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -4,7 +4,8 @@ class WorkItem < Issue include Gitlab::Utils::StrongMemoize COMMON_QUICK_ACTIONS_COMMANDS = [ - :title, :reopen, :close, :cc, :tableflip, :shrug, :type, :promote_to + :title, :reopen, :close, :cc, :tableflip, :shrug, :type, :promote_to, :checkin_reminder, + :subscribe, :unsubscribe, :confidential, :award ].freeze self.table_name = 'issues' @@ -146,6 +147,18 @@ class WorkItem < Issue { common: common_params, widgets: widget_params } end + def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil) + linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id') + return linked_work_items unless authorize + + cross_project_filter = ->(work_items) { work_items.where(project: project) } + Ability.work_items_readable_by_user( + linked_work_items, + current_user, + filters: { read_cross_project: cross_project_filter } + ) + end + private override :parent_link_confidentiality @@ -241,6 +254,21 @@ class WorkItem < Issue errors.add(:work_item_type_id, _('reached maximum depth')) end end + + def linked_work_items_query(link_type) + type_condition = + if link_type == WorkItems::RelatedWorkItemLink::TYPE_RELATES_TO + " AND issue_links.link_type = #{WorkItems::RelatedWorkItemLink.link_types[link_type]}" + else + "" + end + + linked_issues_select + .joins("INNER JOIN issue_links ON + (issue_links.source_id = issues.id AND issue_links.target_id = #{id}#{type_condition}) + OR + (issue_links.target_id = issues.id AND issue_links.source_id = #{id}#{type_condition})") + end end WorkItem.prepend_mod diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb index d9e3690b6fc..ea7755b03b4 100644 --- a/app/models/work_items/parent_link.rb +++ b/app/models/work_items/parent_link.rb @@ -7,7 +7,6 @@ module WorkItems self.table_name = 'work_item_parent_links' MAX_CHILDREN = 100 - PARENT_TYPES = [:issue, :incident].freeze belongs_to :work_item belongs_to :work_item_parent, class_name: 'WorkItem' @@ -122,3 +121,5 @@ module WorkItems end end end + +WorkItems::ParentLink.prepend_mod diff --git a/app/models/work_items/related_work_item_link.rb b/app/models/work_items/related_work_item_link.rb index 4de197d3d35..a911ef5f05d 100644 --- a/app/models/work_items/related_work_item_link.rb +++ b/app/models/work_items/related_work_item_link.rb @@ -6,9 +6,13 @@ module WorkItems self.table_name = 'issue_links' + MAX_LINKS_COUNT = 100 + belongs_to :source, class_name: 'WorkItem' belongs_to :target, class_name: 'WorkItem' + validate :validate_max_number_of_links, on: :create + class << self extend ::Gitlab::Utils::Override @@ -23,5 +27,15 @@ module WorkItems 'work item' end end + + def validate_max_number_of_links + if source && source.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT + errors.add :source, s_('WorkItems|This work item would exceed the maximum number of linked items.') + end + + return unless target && target.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT + + errors.add :target, s_('WorkItems|This work item would exceed the maximum number of linked items.') + end end end diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index 369ffc660aa..b7ceeecbc7f 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -44,8 +44,6 @@ module WorkItems # where it's possible to switch between issue and incident. CHANGEABLE_BASE_TYPES = %w[issue incident test_case].freeze - WI_TYPES_WITH_CREATED_HEADER = %w[issue incident ticket].freeze - cache_markdown_field :description, pipeline: :single_line enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] } diff --git a/app/models/work_items/widgets/linked_items.rb b/app/models/work_items/widgets/linked_items.rb index 06a0f6db964..b405555c038 100644 --- a/app/models/work_items/widgets/linked_items.rb +++ b/app/models/work_items/widgets/linked_items.rb @@ -3,7 +3,7 @@ module WorkItems module Widgets class LinkedItems < Base - delegate :related_issues, to: :work_item + delegate :linked_work_items, to: :work_item end end end diff --git a/app/models/x509_certificate.rb b/app/models/x509_certificate.rb index 7c2581b8bb2..90f3bd69c47 100644 --- a/app/models/x509_certificate.rb +++ b/app/models/x509_certificate.rb @@ -17,8 +17,6 @@ class X509Certificate < ApplicationRecord # rfc 5280 - 4.2.1.2 Subject Key Identifier validates :subject_key_identifier, presence: true, format: { with: Gitlab::Regex.x509_subject_key_identifier_regex } - # rfc 5280 - 4.1.2.6 Subject - validates :subject, presence: true # rfc 5280 - 4.1.2.6 Subject (subjectAltName contains the email address) validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } # rfc 5280 - 4.1.2.2 Serial number diff --git a/app/models/x509_issuer.rb b/app/models/x509_issuer.rb index 81491d8e507..769d56a9838 100644 --- a/app/models/x509_issuer.rb +++ b/app/models/x509_issuer.rb @@ -6,13 +6,16 @@ class X509Issuer < ApplicationRecord # rfc 5280 - 4.2.1.1 Authority Key Identifier validates :subject_key_identifier, presence: true, format: { with: Gitlab::Regex.x509_subject_key_identifier_regex } # rfc 5280 - 4.1.2.4 Issuer - validates :subject, presence: true # rfc 5280 - 4.2.1.13 CRL Distribution Points # cRLDistributionPoints extension using URI:http - validates :crl_url, presence: true, public_url: true + validates :crl_url, allow_nil: true, public_url: true def self.safe_create!(attributes) create_with(attributes) .safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier]) end + + def self.with_crl_url + where.not(crl_url: nil) + end end |