diff options
Diffstat (limited to 'app/models/concerns')
36 files changed, 254 insertions, 146 deletions
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 |