diff options
Diffstat (limited to 'app/models/concerns')
23 files changed, 298 insertions, 84 deletions
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index 0c603c2d5e6..54e9a13d1ea 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -7,6 +7,7 @@ module Analytics included do validates :name, presence: true + validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :start_event_identifier, presence: true validates :end_event_identifier, presence: true validate :validate_stage_event_pairs @@ -15,6 +16,7 @@ module Analytics enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier alias_attribute :custom_stage?, :custom + scope :default_stages, -> { where(custom: false) } end def parent=(_) @@ -45,11 +47,17 @@ module Analytics !custom end - # The model that is going to be queried, Issue or MergeRequest - def subject_model + # The model class that is going to be queried, Issue or MergeRequest + def subject_class start_event.object_type end + def matches_with_stage_params?(stage_params) + default_stage? && + start_event_identifier.to_s.eql?(stage_params[:start_event_identifier].to_s) && + end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s) + end + private def validate_stage_event_pairs diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index dc1735a7e48..64df265dc25 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -27,40 +27,73 @@ module AtomicInternalId extend ActiveSupport::Concern class_methods do - def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName + def has_internal_id(column, scope:, init:, ensure_if: nil, presence: true) # rubocop:disable Naming/PredicateName # We require init here to retain the ability to recalculate in the absence of a # InternaLId record (we may delete records in `internal_ids` for example). raise "has_internal_id requires a init block, none given." unless init + raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope) - before_validation :"ensure_#{scope}_#{column}!", on: :create + before_validation :"track_#{scope}_#{column}!", on: :create + before_validation :"ensure_#{scope}_#{column}!", on: :create, if: ensure_if validates column, presence: presence define_method("ensure_#{scope}_#{column}!") do - scope_value = association(scope).reader + scope_value = internal_id_read_scope(scope) value = read_attribute(column) - return value unless scope_value - scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } - usage = self.class.table_name.to_sym - - if value.present? - InternalId.track_greatest(self, scope_attrs, usage, value, init) - else - value = InternalId.generate_next(self, scope_attrs, usage, init) + if value.nil? + # We don't have a value yet and use a InternalId record to generate + # the next value. + value = InternalId.generate_next( + self, + internal_id_scope_attrs(scope), + internal_id_scope_usage, + init) write_attribute(column, value) end value end + define_method("track_#{scope}_#{column}!") do + return unless @internal_id_needs_tracking + + scope_value = internal_id_read_scope(scope) + return unless scope_value + + value = read_attribute(column) + + if value.present? + # The value was set externally, e.g. by the user + # We update the InternalId record to keep track of the greatest value. + InternalId.track_greatest( + self, + internal_id_scope_attrs(scope), + internal_id_scope_usage, + value, + init) + + @internal_id_needs_tracking = false + end + end + + define_method("#{column}=") do |value| + super(value).tap do |v| + # Indicate the iid was set from externally + @internal_id_needs_tracking = true + end + end + define_method("reset_#{scope}_#{column}") do if value = read_attribute(column) - scope_value = association(scope).reader - scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } - usage = self.class.table_name.to_sym + did_reset = InternalId.reset( + self, + internal_id_scope_attrs(scope), + internal_id_scope_usage, + value) - if InternalId.reset(self, scope_attrs, usage, value) + if did_reset write_attribute(column, nil) end end @@ -69,4 +102,18 @@ module AtomicInternalId end end end + + def internal_id_scope_attrs(scope) + scope_value = internal_id_read_scope(scope) + + { scope_value.class.table_name.singularize.to_sym => scope_value } if scope_value + end + + def internal_id_scope_usage + self.class.table_name.to_sym + end + + def internal_id_read_scope(scope) + association(scope).reader + end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 269145309fc..a98baeb0e3d 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -38,7 +38,7 @@ module Avatarable def avatar_type unless self.avatar.image? - errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::IMAGE_EXT.join(', ')}" + errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::SAFE_IMAGE_EXT.join(', ')}" end end diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb new file mode 100644 index 00000000000..1f76eb87aa5 --- /dev/null +++ b/app/models/concerns/checksummable.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Checksummable + extend ActiveSupport::Concern + + class_methods do + def hexdigest(path) + Digest::SHA256.file(path).hexdigest + end + end +end diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index 91dda803031..49d6f3d399c 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -78,6 +78,7 @@ module Ci variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance) variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s) + variables.append(key: "CI_DEFAULT_BRANCH", value: project.default_branch) variables.concat(legacy_variables) end end diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb index dbc5ed1bc9a..76e0cbc7dff 100644 --- a/app/models/concerns/ci/pipeline_delegator.rb +++ b/app/models/concerns/ci/pipeline_delegator.rb @@ -15,7 +15,8 @@ module Ci :merge_request_ref?, :source_ref, :source_ref_slug, - :legacy_detached_merge_request_pipeline?, to: :pipeline + :legacy_detached_merge_request_pipeline?, + :merge_train_pipeline?, to: :pipeline end end end diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb deleted file mode 100644 index 957b72f3721..00000000000 --- a/app/models/concerns/deployable.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Deployable - extend ActiveSupport::Concern - - included do - after_create :create_deployment - - def create_deployment - return unless starts_environment? && !has_deployment? - - environment = project.environments.find_or_create_by( - name: expanded_environment_name - ) - - # If we failed to persist envirionment record by validation error, such as name with invalid character, - # the job will fall back to a non-environment job. - return unless environment.persisted? - - create_deployment!( - cluster_id: environment.deployment_platform&.cluster_id, - project_id: environment.project_id, - environment: environment, - ref: ref, - tag: tag, - sha: sha, - user: user, - on_stop: on_stop) - end - end -end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index e1a8725e728..fe8e9609820 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -11,6 +11,10 @@ module DeploymentPlatform private + def cluster_management_project_enabled? + Feature.enabled?(:cluster_management_project, default_enabled: true) + end + def find_deployment_platform(environment) find_platform_kubernetes_with_cte(environment) || find_instance_cluster_platform_kubernetes(environment: environment) @@ -18,7 +22,7 @@ module DeploymentPlatform # EE would override this and utilize environment argument def find_platform_kubernetes_with_cte(_environment) - Clusters::ClustersHierarchy.new(self).base_and_ancestors + Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?).base_and_ancestors .enabled.default_environment .first&.platform_kubernetes end diff --git a/app/models/concerns/group_api_compatibility.rb b/app/models/concerns/group_api_compatibility.rb new file mode 100644 index 00000000000..f02aa2035e5 --- /dev/null +++ b/app/models/concerns/group_api_compatibility.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Add methods used by the groups API +module GroupAPICompatibility + extend ActiveSupport::Concern + + def project_creation_level_str + ::Gitlab::Access.project_creation_string_options.key(project_creation_level) + end + + def project_creation_level_str=(value) + write_attribute(:project_creation_level, ::Gitlab::Access.project_creation_string_options.fetch(value)) + end + + def subgroup_creation_level_str + ::Gitlab::Access.subgroup_creation_string_options.key(subgroup_creation_level) + end + + def subgroup_creation_level_str=(value) + write_attribute(:subgroup_creation_level, ::Gitlab::Access.subgroup_creation_string_options.fetch(value)) + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index bcbbb27a9a8..c01fb4740e5 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -10,6 +10,8 @@ module HasStatus ACTIVE_STATUSES = %w[preparing pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze + PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze + EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7, scheduled: 8, preparing: 9 }.freeze @@ -17,7 +19,7 @@ module HasStatus UnknownStatusError = Class.new(StandardError) class_methods do - def status_sql + def legacy_status_sql scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all scope_warnings = respond_to?(:failed_but_allowed) ? failed_but_allowed : none @@ -53,8 +55,22 @@ module HasStatus ) end - def status - all.pluck(status_sql).first + def legacy_status + all.pluck(legacy_status_sql).first + end + + # This method should not be used. + # This method performs expensive calculation of status: + # 1. By plucking all related objects, + # 2. Or executes expensive SQL query + def slow_composite_status + if Feature.enabled?(:ci_composite_status, default_enabled: false) + Gitlab::Ci::Status::Composite + .new(all, with_allow_failure: columns_hash.key?('allow_failure')) + .status + else + legacy_status + end end def started_at diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index d02f3731cc2..852576dbbc2 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -4,7 +4,7 @@ # # Contains common functionality shared between Issues and MergeRequests # -# Used by Issue, MergeRequest +# Used by Issue, MergeRequest, Epic # module Issuable extend ActiveSupport::Concern @@ -25,6 +25,19 @@ module Issuable include UpdatedAtFilterable include IssuableStates include ClosedAtFilterable + include VersionedDescription + + TITLE_LENGTH_MAX = 255 + TITLE_HTML_LENGTH_MAX = 800 + DESCRIPTION_LENGTH_MAX = 1.megabyte + DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes + + STATE_ID_MAP = { + opened: 1, + closed: 2, + merged: 3, + locked: 4 + }.with_indifferent_access.freeze # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests @@ -72,10 +85,15 @@ module Issuable prefix: true validates :author, presence: true - validates :title, presence: true, length: { maximum: 255 } - validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true + validates :title, presence: true, length: { maximum: TITLE_LENGTH_MAX } + # we validate the description against DESCRIPTION_LENGTH_MAX only for Issuables being created + # to avoid breaking the existing Issuables which may have their descriptions longer + validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create + validate :description_max_length_for_new_records_is_valid, on: :update validate :milestone_is_valid + before_validation :truncate_description_on_import! + scope :authored, ->(user) { where(author_id: user) } scope :recent, -> { reorder(id: :desc) } scope :of_projects, ->(ids) { where(project_id: ids) } @@ -138,6 +156,16 @@ module Issuable def milestone_is_valid errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available? end + + def description_max_length_for_new_records_is_valid + if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX + errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX) + end + end + + def truncate_description_on_import! + self.description = description&.slice(0, Issuable::DESCRIPTION_LENGTH_MAX) if importing? + end end class_methods do @@ -152,13 +180,17 @@ module Issuable fuzzy_search(query, [:title]) end - # Available state values persisted in state_id column using state machine + def available_states + @available_states ||= STATE_ID_MAP.slice(*available_state_names) + end + + # Available state names used to persist state_id column using state machine # # Override this on subclasses if different states are needed # - # Check MergeRequest.available_states for example - def available_states - @available_states ||= { opened: 1, closed: 2 }.with_indifferent_access + # Check MergeRequest.available_states_names for example + def available_state_names + [:opened, :closed] end # Searches for records with a matching title or description. @@ -277,6 +309,14 @@ module Issuable end end + def state + self.class.available_states.key(state_id) + end + + def state=(value) + self.state_id = self.class.available_states[value] + end + def resource_parent project end diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb index 33bc41d7f44..f0b9f0d1f3a 100644 --- a/app/models/concerns/issuable_states.rb +++ b/app/models/concerns/issuable_states.rb @@ -4,22 +4,20 @@ module IssuableStates extend ActiveSupport::Concern # The state:string column is being migrated to state_id:integer column - # This is a temporary hook to populate state_id column with new values - # and should be removed after the state column is removed. - # Check https://gitlab.com/gitlab-org/gitlab-foss/issues/51789 for more information + # This is a temporary hook to keep state column in sync until it is removed. + # Check https: https://gitlab.com/gitlab-org/gitlab/issues/33814 for more information + # The state column can be safely removed after 2019-10-27 included do - before_save :set_state_id + before_save :sync_issuable_deprecated_state end - def set_state_id - return if state.nil? || state.empty? + def sync_issuable_deprecated_state + return if self.is_a?(Epic) + return unless respond_to?(:state) + return if state_id.nil? - # Needed to prevent breaking some migration specs that - # rollback database to a point where state_id does not exist. - # We can use this guard clause for now since this file will - # be removed in the next release. - return unless self.has_attribute?(:state_id) + deprecated_state = self.class.available_states.key(state_id) - self.state_id = self.class.available_states[state] + self.write_attribute(:state, deprecated_state) end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 377600ef6e5..9b6c57261d8 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -150,7 +150,7 @@ module Mentionable # # Returns a Hash. def detect_mentionable_changes - source = (changes.present? ? changes : previous_changes).dup + source = (changes.presence || previous_changes).dup mentionable = self.class.mentionable_attrs.map { |attr, options| attr } diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 3deb86da6cf..42b370990ac 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -6,7 +6,9 @@ module Milestoneish end def closed_issues_count(user) - count_issues_by_state(user)['closed'].to_i + closed_state_id = Issue.available_states[:closed] + + count_issues_by_state(user)[closed_state_id].to_i end def complete?(user) @@ -117,7 +119,7 @@ module Milestoneish def count_issues_by_state(user) memoize_per_user(user, :count_issues_by_state) do - issues_visible_to_user(user).reorder(nil).group(:state).count + issues_visible_to_user(user).reorder(nil).group(:state_id).count end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 6caa23ef9b7..3065e0ba6c5 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -7,6 +7,8 @@ module Noteable # avoiding n+1 queries and improving performance. NoteableMeta = Struct.new(:user_notes_count) + MAX_NOTES_LIMIT = 5_000 + class_methods do # `Noteable` class names that support replying to individual notes. def replyable_types diff --git a/app/models/concerns/notification_branch_selection.rb b/app/models/concerns/notification_branch_selection.rb index d8e18de7551..7f00b652530 100644 --- a/app/models/concerns/notification_branch_selection.rb +++ b/app/models/concerns/notification_branch_selection.rb @@ -21,7 +21,7 @@ module NotificationBranchSelection end is_default_branch = ref == project.default_branch - is_protected_branch = project.protected_branches.exists?(name: ref) + is_protected_branch = ProtectedBranch.protected?(project, ref) case branches_to_be_notified when "all" diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb index aab0589f7ca..9df77b565da 100644 --- a/app/models/concerns/prometheus_adapter.rb +++ b/app/models/concerns/prometheus_adapter.rb @@ -44,7 +44,7 @@ module PrometheusAdapter end def query_klass_for(query_name) - Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query") + Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query", false) end def build_query_args(*args) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index dfe3c391880..b645cf71443 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -127,6 +127,7 @@ module RelativePositioning if pos_after && (pos_after - pos_before) < 2 before.move_sequence_after + pos_after = before.next_relative_position end self.relative_position = self.class.position_between(pos_before, pos_after) @@ -138,6 +139,7 @@ module RelativePositioning if pos_before && (pos_after - pos_before) < 2 after.move_sequence_before + pos_before = after.prev_relative_position end self.relative_position = self.class.position_between(pos_before, pos_after) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index bdd87437e2a..129d0fbb2c0 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -51,14 +51,21 @@ module Routable # Klass.where_full_path_in(%w{gitlab-org/gitlab-foss gitlab-org/gitlab}) # # Returns an ActiveRecord::Relation. - def where_full_path_in(paths) + def where_full_path_in(paths, use_includes: true) return none if paths.empty? wheres = paths.map do |path| "(LOWER(routes.path) = LOWER(#{connection.quote(path)}))" end - includes(:route).where(wheres.join(' OR ')).references(:routes) + route = + if use_includes + includes(:route).references(:routes) + else + joins(:route) + end + + route.where(wheres.join(' OR ')) end end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 3ff4b4046d3..10bbeecc2f7 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -80,4 +80,9 @@ module Spammable def check_for_spam? true end + + # Override in Spammable if differs + def allow_possible_spam? + Feature.enabled?(:allow_possible_spam, project) + end end diff --git a/app/models/concerns/stepable.rb b/app/models/concerns/stepable.rb index d00a049a004..dea241c5dbe 100644 --- a/app/models/concerns/stepable.rb +++ b/app/models/concerns/stepable.rb @@ -11,15 +11,15 @@ module Stepable initial_result = {} steps.inject(initial_result) do |previous_result, callback| - result = method(callback).call + result = method(callback).call(previous_result) - if result[:status] == :error - result[:failed_step] = callback + if result[:status] != :success + result[:last_step] = callback break result end - previous_result.merge(result) + result end end diff --git a/app/models/concerns/versioned_description.rb b/app/models/concerns/versioned_description.rb new file mode 100644 index 00000000000..63a24aadc8a --- /dev/null +++ b/app/models/concerns/versioned_description.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module VersionedDescription + extend ActiveSupport::Concern + + included do + attr_accessor :saved_description_version + + has_many :description_versions + + after_update :save_description_version + end + + private + + def save_description_version + self.saved_description_version = nil + + return unless Feature.enabled?(:save_description_versions, issuing_parent) + return unless saved_change_to_description? + + unless description_versions.exists? + description_versions.create!( + description: description_before_last_save, + created_at: created_at + ) + end + + self.saved_description_version = description_versions.create!(description: description) + end +end diff --git a/app/models/concerns/worker_attributes.rb b/app/models/concerns/worker_attributes.rb new file mode 100644 index 00000000000..af40e9e3b19 --- /dev/null +++ b/app/models/concerns/worker_attributes.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module WorkerAttributes + extend ActiveSupport::Concern + + class_methods do + def feature_category(value) + raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned + + worker_attributes[:feature_category] = value + end + + # Special case: mark this work as not associated with a feature category + # this should be used for cross-cutting concerns, such as mailer workers. + def feature_category_not_owned! + worker_attributes[:feature_category] = :not_owned + end + + def get_feature_category + get_worker_attribute(:feature_category) + end + + def feature_category_not_owned? + get_worker_attribute(:feature_category) == :not_owned + end + + protected + + # Returns a worker attribute declared on this class or its parent class. + # This approach allows declared attributes to be inherited by + # child classes. + def get_worker_attribute(name) + worker_attributes[name] || superclass_worker_attributes(name) + end + + private + + def worker_attributes + @attributes ||= {} + end + + def superclass_worker_attributes(name) + return unless superclass.include? WorkerAttributes + + superclass.get_worker_attribute(name) + end + end +end |