diff options
Diffstat (limited to 'app/models')
156 files changed, 1551 insertions, 787 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 7cfebf0473f..f1f22d94061 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -14,7 +14,7 @@ class AbuseReport < ApplicationRecord validates :message, presence: true validates :user_id, uniqueness: { message: 'has already been reported' } - scope :by_user, -> (user) { where(user_id: user) } + scope :by_user, ->(user) { where(user_id: user) } scope :with_users, -> { includes(:reporter, :user) } # For CacheMarkdownField diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb new file mode 100644 index 00000000000..904961491b5 --- /dev/null +++ b/app/models/achievements/achievement.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Achievements + class Achievement < ApplicationRecord + include Avatarable + include StripAttribute + + belongs_to :namespace, inverse_of: :achievements, optional: false + + strip_attributes! :name, :description + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { case_sensitive: false, scope: [:namespace_id] } + validates :description, length: { maximum: 1024 } + end +end diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index 9f05c87018d..a5a539eae75 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -53,7 +53,7 @@ module AlertManagement validates :fingerprint, allow_blank: true, uniqueness: { scope: :project, conditions: -> { not_resolved }, - message: -> (object, data) { _('Cannot have multiple unresolved alerts') } + message: ->(object, data) { _('Cannot have multiple unresolved alerts') } }, unless: :resolved? validate :hosts_format @@ -74,23 +74,23 @@ module AlertManagement delegate :iid, to: :issue, prefix: true, allow_nil: true delegate :details_url, to: :present - scope :for_iid, -> (iid) { where(iid: iid) } - scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) } - scope :for_environment, -> (environment) { where(environment: environment) } - scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) } - scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) } + scope :for_iid, ->(iid) { where(iid: iid) } + scope :for_fingerprint, ->(project, fingerprint) { where(project: project, fingerprint: fingerprint) } + scope :for_environment, ->(environment) { where(environment: environment) } + scope :for_assignee_username, ->(assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) } + scope :search, ->(query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) } scope :not_resolved, -> { without_status(:resolved) } scope :with_prometheus_alert, -> { includes(:prometheus_alert) } scope :with_operations_alerts, -> { where(domain: :operations) } - scope :order_start_time, -> (sort_order) { order(started_at: sort_order) } - scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) } - scope :order_event_count, -> (sort_order) { order(events: sort_order) } + scope :order_start_time, ->(sort_order) { order(started_at: sort_order) } + scope :order_end_time, ->(sort_order) { order(ended_at: sort_order) } + scope :order_event_count, ->(sort_order) { order(events: sort_order) } # Ascending sort order sorts severity from less critical to more critical. # Descending sort order sorts severity from more critical to less critical. # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior - scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) } + scope :order_severity, ->(sort_order) { order(severity: sort_order == :asc ? :desc : :asc) } scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) } scope :counts_by_project_id, -> { group(:project_id).count } diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index b2686924363..906855d6dfc 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -28,7 +28,7 @@ module AlertManagement before_validation :ensure_token before_validation :ensure_payload_example_not_nil - scope :for_endpoint_identifier, -> (endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) } + scope :for_endpoint_identifier, ->(endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) } scope :active, -> { where(active: true) } scope :ordered_by_id, -> { order(:id) } diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb index 2e58d64ae95..a888422a6b4 100644 --- a/app/models/analytics/cycle_analytics/aggregation.rb +++ b/app/models/analytics/cycle_analytics/aggregation.rb @@ -1,24 +1,15 @@ # frozen_string_literal: true class Analytics::CycleAnalytics::Aggregation < ApplicationRecord - include IgnorableColumns include FromUnion belongs_to :group, optional: false validates :incremental_runtimes_in_seconds, :incremental_processed_records, :full_runtimes_in_seconds, :full_processed_records, presence: true, length: { maximum: 10 }, allow_blank: true - scope :priority_order, -> (column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) } + scope :priority_order, ->(column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) } scope :enabled, -> { where('enabled IS TRUE') } - # These columns were added with wrong naming convention, the columns were never used. - ignore_column :last_full_run_processed_records, remove_with: '15.1', remove_after: '2022-05-22' - ignore_column :last_full_run_runtimes_in_seconds, remove_with: '15.1', remove_after: '2022-05-22' - ignore_column :last_full_run_issues_updated_at, remove_with: '15.1', remove_after: '2022-05-22' - ignore_column :last_full_run_mrs_updated_at, remove_with: '15.1', remove_after: '2022-05-22' - ignore_column :last_full_run_issues_id, remove_with: '15.1', remove_after: '2022-05-22' - ignore_column :last_full_run_merge_requests_id, remove_with: '15.1', remove_after: '2022-05-22' - def cursor_for(mode, model) { updated_at: self["last_#{mode}_#{model.table_name}_updated_at"], diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb index 02e239ca0ef..c1245d8dce7 100644 --- a/app/models/analytics/usage_trends/measurement.rb +++ b/app/models/analytics/usage_trends/measurement.rb @@ -23,9 +23,9 @@ module Analytics validates :recorded_at, uniqueness: { scope: :identifier } scope :order_by_latest, -> { order(recorded_at: :desc) } - scope :with_identifier, -> (identifier) { where(identifier: identifier) } - scope :recorded_after, -> (date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? } - scope :recorded_before, -> (date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? } + scope :with_identifier, ->(identifier) { where(identifier: identifier) } + scope :recorded_after, ->(date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? } + scope :recorded_before, ->(date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? } def self.identifier_query_mapping { diff --git a/app/models/appearance.rb b/app/models/appearance.rb index bd948c2c32a..4a046b3ab20 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -3,10 +3,10 @@ class Appearance < ApplicationRecord include CacheableAttributes include CacheMarkdownField - include ObjectStorage::BackgroundMove include WithUploads attribute :title, default: '' + attribute :short_title, default: '' attribute :description, default: '' attribute :new_project_guidelines, default: '' attribute :profile_image_guidelines, default: '' diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index adbbddd635c..3fb1f58f3e0 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -11,6 +11,7 @@ class ApplicationSetting < ApplicationRecord ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22' ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22' ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18' + ignore_column :send_user_confirmation_email, remove_with: '15.8', remove_after: '2022-12-18' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ @@ -20,7 +21,7 @@ class ApplicationSetting < ApplicationRecord 'Admin Area > Settings > General > Kroki' enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true - enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 } + enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }, _prefix: true add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required } add_authentication_token_field :health_check_access_token @@ -87,7 +88,7 @@ class ApplicationSetting < ApplicationRecord validates :grafana_url, system_hook_url: { - blocked_message: "is blocked: %{exception_message}. " + GRAFANA_URL_ERROR_MESSAGE + blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}" }, if: :grafana_url_absolute? @@ -226,6 +227,10 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_terraform_state_size_bytes, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :default_artifacts_expire_in, presence: true, duration: true validates :container_expiration_policies_enable_historic_entries, @@ -412,12 +417,10 @@ class ApplicationSetting < ApplicationRecord allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } - # rubocop:disable Cop/StaticTranslationDefinition validates :deactivate_dormant_users_period, presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") }, + numericality: { only_integer: true, greater_than_or_equal_to: 90, message: N_("'%{value}' days of inactivity must be greater than or equal to 90") }, if: :deactivate_dormant_users? - # rubocop:enable Cop/StaticTranslationDefinition Gitlab::SSHPublicKey.supported_types.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } @@ -466,7 +469,7 @@ class ApplicationSetting < ApplicationRecord validates :external_auth_client_key, presence: true, - if: -> (setting) { setting.external_auth_client_cert.present? } + if: ->(setting) { setting.external_auth_client_cert.present? } validates :lets_encrypt_notification_email, devise_email: true, @@ -488,17 +491,17 @@ class ApplicationSetting < ApplicationRecord validates :eks_access_key_id, length: { in: 16..128 }, - if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } + if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } validates :eks_secret_access_key, presence: true, - if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } + if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } validates_with X509CertificateCredentialsValidator, certificate: :external_auth_client_cert, pkey: :external_auth_client_key, pass: :external_auth_client_key_pass, - if: -> (setting) { setting.external_auth_client_cert.present? } + if: ->(setting) { setting.external_auth_client_cert.present? } validates :default_ci_config_path, format: { without: %r{(\.{2}|\A/)}, @@ -687,6 +690,10 @@ class ApplicationSetting < ApplicationRecord validates :disable_admin_oauth_scopes, inclusion: { in: [true, false], message: N_('must be a boolean value') } + validates :bulk_import_enabled, + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } + before_validation :ensure_uuid! before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed? before_validation :normalize_default_branch_name diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 308c05d638c..229c4e68d79 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -76,6 +76,7 @@ module ApplicationSettingImplementation eks_account_id: nil, eks_integration_enabled: false, eks_secret_access_key: nil, + email_confirmation_setting: 'off', email_restrictions_enabled: false, email_restrictions: nil, external_pipeline_validation_service_timeout: nil, @@ -113,6 +114,7 @@ module ApplicationSettingImplementation max_attachment_size: Settings.gitlab['max_attachment_size'], max_export_size: 0, max_import_size: 0, + max_terraform_state_size_bytes: 0, max_yaml_size_bytes: 1.megabyte, max_yaml_depth: 100, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, @@ -146,7 +148,6 @@ module ApplicationSettingImplementation require_two_factor_authentication: false, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], rsa_key_restriction: default_min_key_size(:rsa), - send_user_confirmation_email: false, session_expire_delay: Settings.gitlab['session_expire_delay'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_text: nil, @@ -243,7 +244,8 @@ module ApplicationSettingImplementation search_rate_limit_unauthenticated: 10, users_get_by_id_limit: 300, users_get_by_id_limit_allowlist: [], - can_create_group: true + can_create_group: true, + bulk_import_enabled: false } end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 0ad17cd8869..5cc87be388f 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -28,11 +28,11 @@ class AuditEvent < ApplicationRecord validates :entity_type, presence: true validates :ip_address, ip_address: true - scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) } - scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) } - scope :by_author_id, -> (author_id) { where(author_id: author_id) } - scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) } - scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) } + scope :by_entity_type, ->(entity_type) { where(entity_type: entity_type) } + scope :by_entity_id, ->(entity_id) { where(entity_id: entity_id) } + scope :by_author_id, ->(author_id) { where(author_id: author_id) } + scope :by_entity_username, ->(username) { where(entity_id: find_user_id(username)) } + scope :by_author_username, ->(username) { where(author_id: find_user_id(username)) } after_initialize :initialize_details diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index e9530a80d9f..f41f0a8be84 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -23,11 +23,11 @@ class AwardEmoji < ApplicationRecord scope :downvotes, -> { named(DOWNVOTE_NAME) } scope :upvotes, -> { named(UPVOTE_NAME) } - scope :named, -> (names) { where(name: names) } - scope :awarded_by, -> (users) { where(user: users) } + scope :named, ->(names) { where(name: names) } + scope :awarded_by, ->(users) { where(user: users) } - after_save :expire_cache after_destroy :expire_cache + after_save :expire_cache class << self def votes_for_collection(ids, type) diff --git a/app/models/badge.rb b/app/models/badge.rb index 4339d419b48..0676de10d02 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -8,6 +8,8 @@ class Badge < ApplicationRecord # the placeholder is found. PLACEHOLDERS = { 'project_path' => :full_path, + 'project_title' => :title, + 'project_name' => :path, 'project_id' => :id, 'default_branch' => :default_branch, 'commit_sha' => ->(project) { project.commit&.sha } diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb index cac6b2192d0..4b7a178566c 100644 --- a/app/models/blob_viewer/metrics_dashboard_yml.rb +++ b/app/models/blob_viewer/metrics_dashboard_yml.rb @@ -25,11 +25,7 @@ module BlobViewer private def parse_blob_data - if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project) - exhaustive_metrics_dashboard_validation - else - old_metrics_dashboard_validation - end + old_metrics_dashboard_validation end def old_metrics_dashboard_validation @@ -41,14 +37,5 @@ module BlobViewer rescue ActiveModel::ValidationError => e e.model.errors.messages.map { |messages| messages.join(': ') } end - - def exhaustive_metrics_dashboard_validation - yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw! - Gitlab::Metrics::Dashboard::Validator - .errors(yaml, dashboard_path: blob.path, project: project) - .map(&:message) - rescue Gitlab::Config::Loader::FormatError => e - [e.message] - end end end diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb index dc273e256a8..65299d6dd12 100644 --- a/app/models/board_group_recent_visit.rb +++ b/app/models/board_group_recent_visit.rb @@ -12,7 +12,7 @@ class BoardGroupRecentVisit < ApplicationRecord validates :group, presence: true validates :board, presence: true - scope :by_user_parent, -> (user, group) { where(user: user, group: group) } + scope :by_user_parent, ->(user, group) { where(user: user, group: group) } def self.board_parent_relation :group diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb index 723afd6feab..c5122392b91 100644 --- a/app/models/board_project_recent_visit.rb +++ b/app/models/board_project_recent_visit.rb @@ -12,7 +12,7 @@ class BoardProjectRecentVisit < ApplicationRecord validates :project, presence: true validates :board, presence: true - scope :by_user_parent, -> (user, project) { where(user: user, project: project) } + scope :by_user_parent, ->(user, project) { where(user: user, project: project) } def self.board_parent_relation :project diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index 2200a66b3c2..2565ad5f2b8 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -17,7 +17,7 @@ class BulkImport < ApplicationRecord enum source_type: { gitlab: 0 } scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) } - scope :order_by_created_at, -> (direction) { order(created_at: direction) } + scope :order_by_created_at, ->(direction) { order(created_at: direction) } state_machine :status, initial: :created do state :created, value: 0 diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index a2542e669e1..e49c4e09a50 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -53,7 +53,7 @@ class BulkImports::Entity < ApplicationRecord scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) } scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) } scope :by_bulk_import_id, ->(bulk_import_id) { where(bulk_import_id: bulk_import_id) } - scope :order_by_created_at, -> (direction) { order(created_at: direction) } + scope :order_by_created_at, ->(direction) { order(created_at: direction) } alias_attribute :destination_slug, :destination_name diff --git a/app/models/bulk_imports/export_upload.rb b/app/models/bulk_imports/export_upload.rb index a9cba5119af..4304032b28c 100644 --- a/app/models/bulk_imports/export_upload.rb +++ b/app/models/bulk_imports/export_upload.rb @@ -3,7 +3,6 @@ module BulkImports class ExportUpload < ApplicationRecord include WithUploads - include ObjectStorage::BackgroundMove self.table_name = 'bulk_import_export_uploads' diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb index 357f4629078..b04ef1cb7ae 100644 --- a/app/models/bulk_imports/tracker.rb +++ b/app/models/bulk_imports/tracker.rb @@ -26,7 +26,7 @@ class BulkImports::Tracker < ApplicationRecord entity_scope = where(bulk_import_entity_id: entity_id) next_stage_scope = entity_scope.with_status(:created).select('MIN(stage)') - entity_scope.where(stage: next_stage_scope) + entity_scope.where(stage: next_stage_scope).with_status(:created) } def self.stage_running?(entity_id, stage) diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index d6051d70503..662fb3cffa8 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -18,8 +18,11 @@ module Ci belongs_to :project belongs_to :trigger_request + + # To be removed upon :ci_bridge_remove_sourced_pipelines feature flag removal has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", - foreign_key: :source_job_id + foreign_key: :source_job_id, + inverse_of: :source_bridge has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline @@ -86,8 +89,20 @@ module Ci end end + def sourced_pipelines + if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project) + raise 'Ci::Bridge does not have sourced_pipelines association' + end + + super + end + def has_downstream_pipeline? - sourced_pipelines.exists? + if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project) + sourced_pipeline.present? + else + sourced_pipelines.exists? + end end def downstream_pipeline_params diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index f44ba124fe2..7f42b21bc87 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -7,7 +7,6 @@ module Ci include Ci::Contextable include TokenAuthenticatable include AfterCommitQueue - include ObjectStorage::BackgroundMove include Presentable include Importable include Ci::HasRef @@ -47,7 +46,7 @@ module Ci # DELETE queries when the Ci::Build is destroyed. The next step is to remove `dependent: :destroy`. # Details: https://gitlab.com/gitlab-org/gitlab/-/issues/24644#note_689472685 has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent - has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id + has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id, inverse_of: :job has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id has_many :pages_deployments, inverse_of: :ci_build @@ -71,6 +70,7 @@ module Ci delegate :harbor_integration, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true delegate :ensure_persistent_ref, to: :pipeline + delegate :enable_debug_trace!, to: :metadata serialize :options # rubocop:disable Cop/ActiveRecordSerialize serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize @@ -90,7 +90,7 @@ module Ci scope :with_downloadable_artifacts, -> do where('EXISTS (?)', Ci::JobArtifact.select(1) - .where('ci_builds.id = ci_job_artifacts.job_id') + .where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id") .where(file_type: Ci::JobArtifact::DOWNLOADABLE_TYPES) ) end @@ -98,7 +98,7 @@ module Ci scope :with_erasable_artifacts, -> do where('EXISTS (?)', Ci::JobArtifact.select(1) - .where('ci_builds.id = ci_job_artifacts.job_id') + .where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id") .where(file_type: Ci::JobArtifact.erasable_file_types) ) end @@ -108,11 +108,11 @@ module Ci end scope :with_existing_job_artifacts, ->(query) do - where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query)) + where('EXISTS (?)', ::Ci::JobArtifact.select(1).where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id").merge(query)) end scope :without_archived_trace, -> do - where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id").trace) end scope :with_artifacts, ->(artifact_scope) do @@ -155,7 +155,7 @@ module Ci scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) } scope :scheduled_actions, -> { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } scope :ref_protected, -> { where(protected: true) } - scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) } + 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 @@ -172,8 +172,6 @@ module Ci add_authentication_token_field :token, encrypted: :required - before_save :ensure_token, unless: :assign_token_on_scheduling? - after_save :stick_build_if_status_changed after_create unless: :importing? do |build| @@ -247,11 +245,8 @@ module Ci !build.waiting_for_deployment_approval? # If false is returned, it stops the transition end - before_transition any => [:pending] do |build, transition| - if build.assign_token_on_scheduling? - build.ensure_token - end - + before_transition any => [:pending] do |build| + build.ensure_token true end @@ -419,12 +414,12 @@ module Ci end def waiting_for_deployment_approval? - manual? && starts_environment? && deployment&.blocked? + manual? && deployment_job? && deployment&.blocked? end def outdated_deployment? strong_memoize(:outdated_deployment) do - starts_environment? && + deployment_job? && incomplete? && project.ci_forward_deployment_enabled? && deployment&.older_than_last_successful_deployment? @@ -528,7 +523,7 @@ module Ci environment.present? end - def starts_environment? + def deployment_job? has_environment_keyword? && self.environment_action == 'start' end @@ -722,7 +717,7 @@ module Ci end def ensure_trace_metadata! - Ci::BuildTraceMetadata.find_or_upsert_for!(id) + Ci::BuildTraceMetadata.find_or_upsert_for!(id, partition_id) end def artifacts_expose_as @@ -866,6 +861,10 @@ module Ci Gitlab::Ci::Build::Step.from_after_script(self)].compact end + def runtime_hooks + Gitlab::Ci::Build::Hook.from_hooks(self) + end + def image Gitlab::Ci::Build::Image.from_image(self) end @@ -995,7 +994,7 @@ module Ci # Virtual deployment status depending on the environment status. def deployment_status - return unless starts_environment? + return unless deployment_job? if success? return successful_deployment_status @@ -1136,8 +1135,15 @@ module Ci end end - def assign_token_on_scheduling? - ::Feature.enabled?(:ci_assign_job_token_on_scheduling, project) + def partition_id_token_prefix + partition_id.to_s(16) if Feature.enabled?(:ci_build_partition_id_token_prefix, project) + end + + override :format_token + def format_token(token) + return token if partition_id_token_prefix.nil? + + "#{partition_id_token_prefix}_#{token}" end protected @@ -1208,11 +1214,11 @@ module Ci if project.ci_cd_settings.opt_in_jwt? id_tokens_variables else - legacy_jwt_variables.concat(id_tokens_variables) + predefined_jwt_variables.concat(id_tokens_variables) end end - def legacy_jwt_variables + def predefined_jwt_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| jwt = Gitlab::Ci::Jwt.for_build(self) jwt_v2 = Gitlab::Ci::JwtV2.for_build(self) @@ -1229,7 +1235,7 @@ module Ci Gitlab::Ci::Variables::Collection.new.tap do |variables| id_tokens.each do |var_name, token_data| - token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['id_token']['aud']) + token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['aud']) variables.append(key: var_name, value: token, public: false, masked: true) end diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 2f28509f812..9b4794abb2e 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -5,21 +5,16 @@ module Ci # Data that should be persisted forever, should be stored with Ci::Build model. class BuildMetadata < Ci::ApplicationRecord BuildTimeout = Struct.new(:value, :source) - ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_metadata_routing_table include Ci::Partitionable include Presentable include ChronicDurationAttribute include Gitlab::Utils::StrongMemoize - self.table_name = 'ci_builds_metadata' + self.table_name = 'p_ci_builds_metadata' self.primary_key = 'id' - self.sequence_name = 'ci_builds_metadata_id_seq' - partitionable scope: :build, through: { - table: :p_ci_builds_metadata, - flag: ROUTING_FEATURE_FLAG - } + partitionable scope: :build belongs_to :build, class_name: 'CommitStatus' belongs_to :project @@ -63,6 +58,12 @@ module Ci runtime_runner_features[:cancel_gracefully] == true end + def enable_debug_trace! + self.debug_trace_enabled = true + save! if changes.any? + true + end + private def set_build_project diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index d4cbbfac4ab..3fa17d6d286 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -2,15 +2,18 @@ module Ci class BuildNeed < Ci::ApplicationRecord + include Ci::Partitionable include BulkInsertSafe 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 :optional, inclusion: { in: [true, false] } - scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') } + scope :scoped_build, -> { where("#{Ci::Build.quoted_table_name}.id = #{quoted_table_name}.build_id") } scope :artifacts, -> { where(artifacts: true) } end end diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb index 53cf0697e2e..3684dac06c7 100644 --- a/app/models/ci/build_pending_state.rb +++ b/app/models/ci/build_pending_state.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class Ci::BuildPendingState < Ci::ApplicationRecord + include Ci::Partitionable + belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id + partitionable scope: :build + enum state: Ci::Stage.statuses enum failure_reason: CommitStatus.failure_reasons diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb index b674c1b1a0e..b2d99fab295 100644 --- a/app/models/ci/build_report_result.rb +++ b/app/models/ci/build_report_result.rb @@ -2,11 +2,15 @@ module Ci class BuildReportResult < Ci::ApplicationRecord + include Ci::Partitionable + self.primary_key = :build_id belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results belongs_to :project, class_name: "Project", inverse_of: :build_report_results + partitionable scope: :build + validates :build, :project, presence: true validates :data, json_schema: { filename: "build_report_result_data" } diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 0f37ce70964..20c0b04e228 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -4,6 +4,8 @@ module Ci # The purpose of this class is to store Build related runner session. # Data will be removed after transitioning from running to any state. class BuildRunnerSession < Ci::ApplicationRecord + include Ci::Partitionable + TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' DEFAULT_SERVICE_NAME = 'build' DEFAULT_PORT_NAME = 'default_port' @@ -12,6 +14,8 @@ module Ci belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session + partitionable scope: :build + validates :build, presence: true validates :url, public_url: { schemes: %w(https) } diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 7baa98b59f9..57d8b9ba368 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -2,6 +2,7 @@ module Ci class BuildTraceChunk < Ci::ApplicationRecord + include Ci::Partitionable include ::Comparable include ::FastDestroyAll include ::Checksummable @@ -10,6 +11,8 @@ module Ci belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id + partitionable scope: :build + attribute :data_store, default: :redis_trace_chunks after_create { metrics.increment_trace_operation(operation: :chunked) } @@ -28,8 +31,8 @@ module Ci redis_trace_chunks: 4 }.freeze - STORE_TYPES = DATA_STORES.keys.to_h do |store| - [store, "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize] + STORE_TYPES = DATA_STORES.keys.index_with do |store| + "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize end.freeze LIVE_STORES = %i[redis redis_trace_chunks].freeze diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index 86de90983ff..00cf1531483 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -2,6 +2,8 @@ module Ci class BuildTraceMetadata < Ci::ApplicationRecord + include Ci::Partitionable + MAX_ATTEMPTS = 5 self.table_name = 'ci_build_trace_metadata' self.primary_key = :build_id @@ -9,15 +11,17 @@ module Ci belongs_to :build, class_name: 'Ci::Build' belongs_to :trace_artifact, class_name: 'Ci::JobArtifact' + partitionable scope: :build + validates :build, presence: true validates :archival_attempts, presence: true - def self.find_or_upsert_for!(build_id) - record = find_by(build_id: build_id) + def self.find_or_upsert_for!(build_id, partition_id) + record = find_by(build_id: build_id, partition_id: partition_id) return record if record - upsert({ build_id: build_id }, unique_by: :build_id) - find_by!(build_id: build_id) + upsert({ build_id: build_id, partition_id: partition_id }, unique_by: :build_id) + find_by!(build_id: build_id, partition_id: partition_id) end # The job is retried around 5 times during the 7 days retention period for diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb index da0bbbacddd..1bf32e04a15 100644 --- a/app/models/ci/freeze_period.rb +++ b/app/models/ci/freeze_period.rb @@ -4,6 +4,10 @@ module Ci class FreezePeriod < Ci::ApplicationRecord include StripAttribute include Ci::NamespacedModelName + include Gitlab::Utils::StrongMemoize + + STATUS_ACTIVE = :active + STATUS_INACTIVE = :inactive default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope @@ -14,5 +18,60 @@ module Ci validates :freeze_start, cron: true, presence: true validates :freeze_end, cron: true, presence: true validates :cron_timezone, cron_freeze_period_timezone: true, presence: true + + def active? + status == STATUS_ACTIVE + end + + def status + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:status") do + within_freeze_period? ? STATUS_ACTIVE : STATUS_INACTIVE + end + end + + def time_start + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_start") do + freeze_start_parsed_cron.previous_time_from(time_zone_now) + end + end + + def next_time_start + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:next_time_start") do + freeze_start_parsed_cron.next_time_from(time_zone_now) + end + end + + def time_end_from_now + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_now") do + freeze_end_parsed_cron.next_time_from(time_zone_now) + end + end + + def time_end_from_start + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_start") do + freeze_end_parsed_cron.next_time_from(time_start) + end + end + + private + + def within_freeze_period? + time_start <= time_zone_now && time_zone_now <= time_end_from_start + end + + def freeze_start_parsed_cron + Gitlab::Ci::CronParser.new(freeze_start, cron_timezone) + end + strong_memoize_attr :freeze_start_parsed_cron + + def freeze_end_parsed_cron + Gitlab::Ci::CronParser.new(freeze_end, cron_timezone) + end + strong_memoize_attr :freeze_end_parsed_cron + + def time_zone_now + Time.zone.now + end + strong_memoize_attr :time_zone_now end end diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb deleted file mode 100644 index e810bb3f229..00000000000 --- a/app/models/ci/freeze_period_status.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Ci - class FreezePeriodStatus - attr_reader :project - - def initialize(project:) - @project = project - end - - def execute - project.freeze_periods.any? { |period| within_freeze_period?(period) } - end - - def within_freeze_period?(period) - start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone) - end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone) - - start_freeze = start_freeze_cron.previous_time_from(time_zone_now) - end_freeze = end_freeze_cron.next_time_from(start_freeze) - - start_freeze <= time_zone_now && time_zone_now <= end_freeze - end - - private - - def time_zone_now - @time_zone_now ||= Time.zone.now - end - end -end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 922806a21c3..53c358f4eba 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -5,7 +5,6 @@ module Ci include Ci::Partitionable include IgnorableColumns include AfterCommitQueue - include ObjectStorage::BackgroundMove include UpdateProjectStatistics include UsageStatistics include Sortable @@ -52,7 +51,8 @@ module Ci cobertura: 'cobertura-coverage.xml', terraform: 'tfplan.json', cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094 - requirements: 'requirements.json', + requirements: 'requirements.json', # Will be DEPRECATED soon: https://gitlab.com/groups/gitlab-org/-/epics/9203 + requirements_v2: 'requirements_v2.json', coverage_fuzzing: 'gl-coverage-fuzzing.json', api_fuzzing: 'gl-api-fuzzing-report.json', cyclonedx: 'gl-sbom.cdx.json' @@ -95,6 +95,7 @@ module Ci load_performance: :raw, terraform: :raw, requirements: :raw, + requirements_v2: :raw, coverage_fuzzing: :raw, api_fuzzing: :raw }.freeze @@ -119,6 +120,7 @@ module Ci sast secret_detection requirements + requirements_v2 cluster_image_scanning cyclonedx ].freeze @@ -209,7 +211,8 @@ module Ci load_performance: 25, ## EE-specific api_fuzzing: 26, ## EE-specific cluster_image_scanning: 27, ## EE-specific - cyclonedx: 28 ## EE-specific + cyclonedx: 28, ## EE-specific + requirements_v2: 29 ## EE-specific } # `file_location` indicates where actual files are stored. diff --git a/app/models/ci/job_token/allowlist.rb b/app/models/ci/job_token/allowlist.rb new file mode 100644 index 00000000000..9e9a0a68ebd --- /dev/null +++ b/app/models/ci/job_token/allowlist.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +module Ci + module JobToken + class Allowlist + def initialize(source_project, direction:) + @source_project = source_project + @direction = direction + end + + def includes?(target_project) + source_links + .with_target(target_project) + .exists? + end + + def projects + Project.from_union(target_projects, remove_duplicates: false) + end + + private + + def source_links + Ci::JobToken::ProjectScopeLink + .with_source(@source_project) + .where(direction: @direction) + end + + def target_project_ids + source_links + # pluck needed to avoid ci and main db join + .pluck(:target_project_id) + end + + def target_projects + [ + Project.id_in(@source_project), + Project.id_in(target_project_ids) + ] + end + end + end +end diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb index 3fdf07123e6..b784f93651a 100644 --- a/app/models/ci/job_token/project_scope_link.rb +++ b/app/models/ci/job_token/project_scope_link.rb @@ -12,8 +12,8 @@ module Ci belongs_to :target_project, class_name: 'Project' belongs_to :added_by, class_name: 'User' - scope :from_project, ->(project) { where(source_project: project) } - scope :to_project, ->(project) { where(target_project: project) } + scope :with_source, ->(project) { where(source_project: project) } + scope :with_target, ->(project) { where(target_project: project) } validates :source_project, presence: true validates :target_project, presence: true diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index 1aa49b95201..e320c0f92d1 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -1,49 +1,58 @@ # frozen_string_literal: true -# This model represents the surface where a CI_JOB_TOKEN can be used. -# A Scope is initialized with the project that the job token belongs to, -# and indicates what are all the other projects that the token could access. +# This model represents the scope of access for a CI_JOB_TOKEN. # -# By default a job token can only access its own project, which is the same -# project that defines the scope. -# By adding ScopeLinks to the scope we can allow other projects to be accessed -# by the job token. This works as an allowlist of projects for a job token. +# A scope is initialized with a project. +# +# Projects can be added to the scope by adding ScopeLinks to +# create an allowlist of projects in either access direction (inbound, outbound). +# +# Currently, projects in the outbound allowlist can be accessed via the token +# in the source project. +# +# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access +# the source project. +# +# CI_JOB_TOKEN should be considered untrusted without these features enabled. # -# If a project is not included in the scope we should not allow the job user -# to access it since operations using CI_JOB_TOKEN should be considered untrusted. module Ci module JobToken class Scope - attr_reader :source_project + attr_reader :current_project - def initialize(project) - @source_project = project + def initialize(current_project) + @current_project = current_project end - def includes?(target_project) - # if the setting is disabled any project is considered to be in scope. - return true unless source_project.ci_outbound_job_token_scope_enabled? + def allows?(accessed_project) + self_referential?(accessed_project) || outbound_allows?(accessed_project) + end - target_project.id == source_project.id || - Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists? + def outbound_projects + outbound_allowlist.projects end + # Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project def all_projects - Project.from_union(target_projects, remove_duplicates: false) + outbound_projects end private - def target_project_ids - Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id) + def outbound_allows?(accessed_project) + # if the setting is disabled any project is considered to be in scope. + return true unless @current_project.ci_outbound_job_token_scope_enabled? + + outbound_allowlist.includes?(accessed_project) + end + + def outbound_allowlist + Ci::JobToken::Allowlist.new(@current_project, direction: :outbound) end - def target_projects - [ - Project.id_in(source_project), - Project.id_in(target_project_ids) - ] + def self_referential?(accessed_project) + @current_project.id == accessed_project.id end end end diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb index 332a78b66ae..998f0647ad5 100644 --- a/app/models/ci/job_variable.rb +++ b/app/models/ci/job_variable.rb @@ -2,12 +2,15 @@ module Ci class JobVariable < Ci::ApplicationRecord + include Ci::Partitionable include Ci::NewHasVariable include Ci::RawVariable include BulkInsertSafe belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id + partitionable scope: :job + alias_attribute :secret_value, :value validates :key, uniqueness: { scope: :job_id }, unless: :dotenv_source? diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb index 0fa6a234a3d..2b1eb67d4f2 100644 --- a/app/models/ci/pending_build.rb +++ b/app/models/ci/pending_build.rb @@ -3,11 +3,14 @@ module Ci class PendingBuild < Ci::ApplicationRecord include EachBatch + include Ci::Partitionable belongs_to :project belongs_to :build, class_name: 'Ci::Build' belongs_to :namespace, inverse_of: :pending_builds, class_name: 'Namespace' + partitionable scope: :build + validates :namespace, presence: true scope :ref_protected, -> { where(protected: true) } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 020f5cf9d8e..05207fb1ca0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -350,9 +350,13 @@ module Ci scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } scope :for_ref, -> (ref) { where(ref: ref) } scope :for_branch, -> (branch) { for_ref(branch).where(tag: false) } - scope :for_id, -> (id) { where(id: id) } scope :for_iid, -> (iid) { where(iid: iid) } scope :for_project, -> (project_id) { where(project_id: project_id) } + scope :for_name, -> (name) do + name_column = Ci::PipelineMetadata.arel_table[:name] + + joins(:pipeline_metadata).where(name_column.lower.eq(name.downcase)) + end scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) } scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) } scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) } @@ -721,7 +725,7 @@ module Ci def freeze_period? strong_memoize(:freeze_period) do - Ci::FreezePeriodStatus.new(project: project).execute + project.freeze_periods.any?(&:active?) end end @@ -1341,13 +1345,14 @@ module Ci persistent_ref.create end + # For dependent bridge jobs we reset the upstream bridge recursively + # to reflect that a downstream pipeline is running again def reset_source_bridge!(current_user) # break recursion when no source_pipeline bridge (first upstream pipeline) return unless bridge_waiting? return unless current_user.can?(:update_pipeline, source_bridge.pipeline) - source_bridge.pending! - Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass + Ci::EnqueueJobService.new(source_bridge, current_user: current_user).execute(&:pending!) # rubocop:disable CodeReuse/ServiceClass end # EE-only diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 96e5567e85e..20ff07e88ba 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -16,7 +16,7 @@ module Ci belongs_to :owner, class_name: 'User' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_many :pipelines - has_many :variables, class_name: 'Ci::PipelineScheduleVariable', validate: false + has_many :variables, class_name: 'Ci::PipelineScheduleVariable' validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } @@ -78,8 +78,6 @@ module Ci ref.start_with? 'refs/tags/' end - private - def worker_cron_expression Settings.cron_jobs['pipeline_schedule_worker']['cron'] end diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb index 718ed14edeb..00251ea06fd 100644 --- a/app/models/ci/pipeline_schedule_variable.rb +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -9,6 +9,6 @@ module Ci alias_attribute :secret_value, :value - validates :key, uniqueness: { scope: :pipeline_schedule_id } + validates :key, presence: true, uniqueness: { scope: :pipeline_schedule_id } end end diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index eb805ffae0a..37c82c125aa 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -104,8 +104,8 @@ module Ci to: :pipeline def clone(current_user:, new_job_variables_attributes: []) - new_attributes = self.class.clone_accessors.to_h do |attribute| - [attribute, public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend + new_attributes = self.class.clone_accessors.index_with do |attribute| + public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend end if persisted_environment.present? diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb index 6d25f747a9d..b788e4f58c1 100644 --- a/app/models/ci/resource_group.rb +++ b/app/models/ci/resource_group.rb @@ -24,11 +24,18 @@ module Ci # NOTE: This is concurrency-safe method that the subquery in the `UPDATE` # works as explicit locking. def assign_resource_to(processable) - resources.free.limit(1).update_all(build_id: processable.id) > 0 + attrs = { + build_id: processable.id, + partition_id: processable.partition_id + } + + resources.free.limit(1).update_all(attrs) > 0 end def release_resource_from(processable) - resources.retained_by(processable).update_all(build_id: nil) > 0 + attrs = { build_id: nil, partition_id: nil } + + resources.retained_by(processable).update_all(attrs) > 0 end def upcoming_processables diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 3be627989b1..a7f3ff938c3 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -89,6 +89,9 @@ module Ci scope :ordered, -> { order(id: :desc) } scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) } + scope :with_running_builds, -> do + where('EXISTS(?)', ::Ci::Build.running.select(1).where('ci_builds.runner_id = ci_runners.id')) + end # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb` scope :deprecated_shared, -> { instance_type } diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb index 82390ccc538..502ceae3675 100644 --- a/app/models/ci/runner_namespace.rb +++ b/app/models/ci/runner_namespace.rb @@ -15,6 +15,8 @@ module Ci validates :runner_id, uniqueness: { scope: :namespace_id } validate :group_runner_type + scope :for_runner, ->(runner_id) { where(runner_id: runner_id) } + def recent_runners ::Ci::Runner.belonging_to_group(namespace_id).recent end diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb index ae38d54862d..43214b0c336 100644 --- a/app/models/ci/running_build.rb +++ b/app/models/ci/running_build.rb @@ -1,7 +1,18 @@ # frozen_string_literal: true module Ci + # This model represents metadata for a running build. + # Despite the generic RunningBuild name, in this first iteration it applies only to shared runners + # (see Ci::RunningBuild.upsert_shared_runner_build!). + # The decision to insert all of the running builds here was deferred to avoid the pressure on the database as + # at this time that was not necessary. + # We can reconsider the decision to limit this only to shared runners when there is more evidence that inserting all + # of the running builds there is worth the additional pressure. class RunningBuild < Ci::ApplicationRecord + include Ci::Partitionable + + partitionable scope: :build + belongs_to :project belongs_to :build, class_name: 'Ci::Build' belongs_to :runner, class_name: 'Ci::Runner' diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb index df38398e5a9..1e6c48bbef5 100644 --- a/app/models/ci/secure_file.rb +++ b/app/models/ci/secure_file.rb @@ -17,20 +17,19 @@ module Ci validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT } validates :checksum, :file_store, :name, :project_id, presence: true validates :name, uniqueness: { scope: :project } + + attribute :metadata, :ind_jsonb validates :metadata, json_schema: { filename: "ci_secure_file_metadata" }, allow_nil: true + attribute :file_store, default: -> { Ci::SecureFileUploader.default_store } + mount_file_store_uploader Ci::SecureFileUploader + after_initialize :generate_key_data before_validation :assign_checksum scope :order_by_created_at, -> { order(created_at: :desc) } scope :project_id_in, ->(ids) { where(project_id: ids) } - serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize - - attribute :file_store, default: -> { Ci::SecureFileUploader.default_store } - - mount_file_store_uploader Ci::SecureFileUploader - def checksum_algorithm CHECKSUM_ALGORITHM end diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb index 2df504cd3de..855e68d1db1 100644 --- a/app/models/ci/sources/pipeline.rb +++ b/app/models/ci/sources/pipeline.rb @@ -3,6 +3,7 @@ module Ci module Sources class Pipeline < Ci::ApplicationRecord + include Ci::Partitionable include Ci::NamespacedModelName self.table_name = "ci_sources_pipelines" @@ -15,6 +16,11 @@ module Ci belongs_to :source_bridge, class_name: "Ci::Bridge", foreign_key: :source_job_id belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id + partitionable scope: :pipeline + + before_validation :set_source_partition_id, on: :create + validates :source_partition_id, presence: true + validates :project, presence: true validates :pipeline, presence: true @@ -23,6 +29,15 @@ module Ci validates :source_pipeline, presence: true scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) } + + private + + def set_source_partition_id + return if source_partition_id_changed? && source_partition_id.present? + return unless source_job + + self.source_partition_id = source_job.partition_id + end end end end diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb index a5aa3b70e37..cfef1249164 100644 --- a/app/models/ci/unit_test_failure.rb +++ b/app/models/ci/unit_test_failure.rb @@ -2,6 +2,8 @@ module Ci class UnitTestFailure < Ci::ApplicationRecord + include Ci::Partitionable + REPORT_WINDOW = 14.days validates :unit_test, :build, :failed_at, presence: true @@ -9,6 +11,8 @@ module Ci belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id + partitionable scope: :build + scope :deletable, -> { where('failed_at < ?', REPORT_WINDOW.ago) } def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current) diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb index 1607d0b6d19..e2dcff13a69 100644 --- a/app/models/clusters/agent_token.rb +++ b/app/models/clusters/agent_token.rb @@ -25,5 +25,9 @@ module Clusters active: 0, revoked: 1 } + + def to_ability_name + :cluster + end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 54de45ebba7..5175842e5de 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -359,6 +359,10 @@ class Commit end def has_signature? + if signature_type == :SSH && !ssh_signatures_enabled? + return false + end + signature_type && signature_type != :NONE end @@ -378,6 +382,10 @@ class Commit @signature_type ||= raw_signature_type || :NONE end + def ssh_signatures_enabled? + Feature.enabled?(:ssh_commit_signatures, project) + end + def signature strong_memoize(:signature) do case signature_type @@ -385,6 +393,8 @@ class Commit gpg_commit.signature when :X509 Gitlab::X509::Commit.new(self).signature + when :SSH + Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled? else nil end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index e2f0de52bc9..87029cb2033 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -148,7 +148,7 @@ class CommitRange def sha_start return unless sha_from - exclude_start? ? sha_from + '^' : sha_from + exclude_start? ? "#{sha_from}^" : sha_from end def commit_start diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb index 2ae59853520..a9e8ca2dd33 100644 --- a/app/models/commit_signatures/gpg_signature.rb +++ b/app/models/commit_signatures/gpg_signature.rb @@ -2,6 +2,7 @@ module CommitSignatures class GpgSignature < ApplicationRecord include CommitSignature + include SignatureType sha_attribute :gpg_key_primary_keyid @@ -10,6 +11,14 @@ module CommitSignatures validates :gpg_key_primary_keyid, presence: true + def signed_by_user + gpg_key&.user + end + + def type + :gpg + end + def self.with_key_and_subkeys(gpg_key) subkey_ids = gpg_key.subkeys.pluck(:id) diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb index 7a8d0653fcd..1e64e2b2978 100644 --- a/app/models/commit_signatures/ssh_signature.rb +++ b/app/models/commit_signatures/ssh_signature.rb @@ -3,7 +3,16 @@ module CommitSignatures class SshSignature < ApplicationRecord include CommitSignature + include SignatureType belongs_to :key, optional: true + + def type + :ssh + end + + def signed_by_user + key&.user + end end end diff --git a/app/models/commit_signatures/x509_commit_signature.rb b/app/models/commit_signatures/x509_commit_signature.rb index 2cbb331dd7e..4edbc147502 100644 --- a/app/models/commit_signatures/x509_commit_signature.rb +++ b/app/models/commit_signatures/x509_commit_signature.rb @@ -2,15 +2,24 @@ module CommitSignatures class X509CommitSignature < ApplicationRecord include CommitSignature + include SignatureType belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false validates :x509_certificate_id, presence: true + def type + :x509 + end + def x509_commit return unless commit Gitlab::X509::Commit.new(commit) end + + def signed_by_user + commit&.committer + end end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index b32502c3ee2..f419fa8518e 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -16,7 +16,6 @@ module Avatarable included do prepend ShadowMethods - include ObjectStorage::BackgroundMove include Gitlab::Utils::StrongMemoize include ApplicationHelper diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index ec0cf36d875..6a855198697 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -40,7 +40,7 @@ module CacheMarkdownField # Banzai is less strict about authors, so don't always have an author key context[:author] = self.author if self.respond_to?(:author) - context[:markdown_engine] = :common_mark + context[:markdown_engine] = Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE if Feature.enabled?(:personal_snippet_reference_filters, context[:author]) context[:user] = self.parent_user diff --git a/app/models/concerns/cached_commit.rb b/app/models/concerns/cached_commit.rb index 183d5728743..0fb72552dd5 100644 --- a/app/models/concerns/cached_commit.rb +++ b/app/models/concerns/cached_commit.rb @@ -4,8 +4,8 @@ module CachedCommit extend ActiveSupport::Concern def to_hash - Gitlab::Git::Commit::SERIALIZE_KEYS.each_with_object({}) do |key, hash| - hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend + Gitlab::Git::Commit::SERIALIZE_KEYS.index_with do |key| + public_send(key) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb index 68a6714c892..d6ba0f4488f 100644 --- a/app/models/concerns/ci/partitionable.rb +++ b/app/models/concerns/ci/partitionable.rb @@ -25,10 +25,21 @@ module Ci PARTITIONABLE_MODELS = %w[ CommitStatus Ci::BuildMetadata - Ci::Stage + Ci::BuildNeed + Ci::BuildReportResult + Ci::BuildRunnerSession + Ci::BuildTraceChunk + Ci::BuildTraceMetadata + Ci::BuildPendingState Ci::JobArtifact - Ci::PipelineVariable + Ci::JobVariable Ci::Pipeline + Ci::PendingBuild + Ci::RunningBuild + Ci::PipelineVariable + Ci::Sources::Pipeline + Ci::Stage + Ci::UnitTestFailure ].freeze def self.check_inclusion(klass) @@ -57,14 +68,31 @@ module Ci end class_methods do - def partitionable(scope:, through: nil) - if through - define_singleton_method(:routing_table_name) { through[:table] } - define_singleton_method(:routing_table_name_flag) { through[:flag] } + def partitionable(scope:, through: nil, partitioned: false) + handle_partitionable_through(through) + handle_partitionable_dml(partitioned) + handle_partitionable_scope(scope) + end - include Partitionable::Switch - end + private + + def handle_partitionable_through(options) + return unless options + + define_singleton_method(:routing_table_name) { options[:table] } + define_singleton_method(:routing_table_name_flag) { options[:flag] } + + include Partitionable::Switch + end + + def handle_partitionable_dml(partitioned) + define_singleton_method(:partitioned?) { partitioned } + return unless partitioned + + include Partitionable::PartitionedFilter + end + def handle_partitionable_scope(scope) define_method(:partition_scope_value) do strong_memoize(:partition_scope_value) do next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing? diff --git a/app/models/concerns/ci/partitionable/partitioned_filter.rb b/app/models/concerns/ci/partitionable/partitioned_filter.rb new file mode 100644 index 00000000000..4adae3be26a --- /dev/null +++ b/app/models/concerns/ci/partitionable/partitioned_filter.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Ci + module Partitionable + # Used to patch the save, update, delete, destroy methods to use the + # partition_id attributes for their SQL queries. + module PartitionedFilter + extend ActiveSupport::Concern + + if Rails::VERSION::MAJOR >= 7 + # These methods are updated in Rails 7 to use `_primary_key_constraints_hash` + # by default, so this patch will no longer be required. + # + # rubocop:disable Gitlab/NoCodeCoverageComment + # :nocov: + raise "`#{__FILE__}` should be double checked" if Rails.env.test? + + warn "Update `#{__FILE__}`. Patches Rails internals for partitioning" + # :nocov: + # rubocop:enable Gitlab/NoCodeCoverageComment + else + def _update_row(attribute_names, attempted_action = "update") + self.class._update_record( + attributes_with_values(attribute_names), + _primary_key_constraints_hash + ) + end + + def _delete_row + self.class._delete_record(_primary_key_constraints_hash) + end + end + + # Introduced in Rails 7, but updated to include `partition_id` filter. + # https://github.com/rails/rails/blob/a4dbb153fd390ac31bb9808809e7ac4d3a2c5116/activerecord/lib/active_record/persistence.rb#L1031-L1033 + def _primary_key_constraints_hash + { @primary_key => id_in_database, partition_id: partition_id } # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + end + end +end diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb index 5bdfa9a2966..7f1fbbefd94 100644 --- a/app/models/concerns/commit_signature.rb +++ b/app/models/concerns/commit_signature.rb @@ -44,7 +44,7 @@ module CommitSignature project.commit(commit_sha) end - def user - commit.committer + def signed_by_user + raise NoMethodError, 'must implement `signed_by_user` method' end end diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb index 03e062a9855..f1efbba67e1 100644 --- a/app/models/concerns/counter_attribute.rb +++ b/app/models/concerns/counter_attribute.rb @@ -17,14 +17,29 @@ # counter_attribute :storage_size # end # +# It's possible to define a conditional counter attribute. You need to pass a proc +# that must accept a single argument, the object instance on which this concern is +# included. +# +# @example: +# +# class ProjectStatistics +# include CounterAttribute +# +# counter_attribute :conditional_one, if: -> { |object| object.use_counter_attribute? } +# end +# # To increment the counter we can use the method: -# delayed_increment_counter(:commit_count, 3) +# increment_counter(:commit_count, 3) +# +# This method would determine whether it would increment the counter using Redis, +# or fallback to legacy increment on ActiveRecord counters. # # It is possible to register callbacks to be executed after increments have # been flushed to the database. Callbacks are not executed if there are no increments # to flush. # -# counter_attribute_after_flush do |statistic| +# counter_attribute_after_commit do |statistic| # Namespaces::ScheduleAggregationWorker.perform_async(statistic.namespace_id) # end # @@ -32,99 +47,51 @@ module CounterAttribute extend ActiveSupport::Concern extend AfterCommitQueue include Gitlab::ExclusiveLeaseHelpers - - LUA_STEAL_INCREMENT_SCRIPT = <<~EOS - local increment_key, flushed_key = KEYS[1], KEYS[2] - local increment_value = redis.call("get", increment_key) or 0 - local flushed_value = redis.call("incrby", flushed_key, increment_value) - if flushed_value == 0 then - redis.call("del", increment_key, flushed_key) - else - redis.call("del", increment_key) - end - return flushed_value - EOS - - WORKER_DELAY = 10.minutes - WORKER_LOCK_TTL = 10.minutes + include Gitlab::Utils::StrongMemoize class_methods do - def counter_attribute(attribute) - counter_attributes << attribute + def counter_attribute(attribute, if: nil) + counter_attributes << { + attribute: attribute, + if_proc: binding.local_variable_get(:if) # can't read `if` directly + } end def counter_attributes - @counter_attributes ||= Set.new + @counter_attributes ||= [] end - def after_flush_callbacks - @after_flush_callbacks ||= [] + def after_commit_callbacks + @after_commit_callbacks ||= [] end - # perform registered callbacks after increments have been flushed to the database - def counter_attribute_after_flush(&callback) - after_flush_callbacks << callback - end - - def counter_attribute_enabled?(attribute) - counter_attributes.include?(attribute) + # perform registered callbacks after increments have been committed to the database + def counter_attribute_after_commit(&callback) + after_commit_callbacks << callback end end - # This method must only be called by FlushCounterIncrementsWorker - # because it should run asynchronously and with exclusive lease. - # This will - # 1. temporarily move the pending increment for a given attribute - # to a relative "flushed" Redis key, delete the increment key and return - # the value. If new increments are performed at this point, the increment - # key is recreated as part of `delayed_increment_counter`. - # The "flushed" key is used to ensure that we can keep incrementing - # counters in Redis while flushing existing values. - # 2. then the value is used to update the counter in the database. - # 3. finally the "flushed" key is deleted. - def flush_increments_to_database!(attribute) - lock_key = counter_lock_key(attribute) - - with_exclusive_lease(lock_key) do - previous_db_value = read_attribute(attribute) - increment_key = counter_key(attribute) - flushed_key = counter_flushed_key(attribute) - increment_value = steal_increments(increment_key, flushed_key) - new_db_value = nil - - next if increment_value == 0 - - transaction do - update_counters_with_lease({ attribute => increment_value }) - redis_state { |redis| redis.del(flushed_key) } - new_db_value = reset.read_attribute(attribute) - end + def counter_attribute_enabled?(attribute) + counter_attribute = self.class.counter_attributes.find { |registered| registered[:attribute] == attribute } + return false unless counter_attribute + return true unless counter_attribute[:if_proc] - execute_after_flush_callbacks + counter_attribute[:if_proc].call(self) + end - log_flush_counter(attribute, increment_value, previous_db_value, new_db_value) + def counter(attribute) + strong_memoize_with(:counter, attribute) do + # This needs #to_sym because attribute could come from a Sidekiq param, + # which would be a string. + build_counter_for(attribute.to_sym) end end - def delayed_increment_counter(attribute, increment) - raise ArgumentError, "#{attribute} is not a counter attribute" unless counter_attribute_enabled?(attribute) - + def increment_counter(attribute, increment) return if increment == 0 run_after_commit_or_now do - increment_counter(attribute, increment) - - FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute) - end - - true - end - - def increment_counter(attribute, increment) - if counter_attribute_enabled?(attribute) - new_value = redis_state do |redis| - redis.incrby(counter_key(attribute), increment) - end + new_value = counter(attribute).increment(increment) log_increment_counter(attribute, increment, new_value) end @@ -137,74 +104,33 @@ module CounterAttribute end def reset_counter!(attribute) - if counter_attribute_enabled?(attribute) - detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do - update!(attribute => 0) - clear_counter!(attribute) - end - - log_clear_counter(attribute) + detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do + counter(attribute).reset! end - end - def get_counter_value(attribute) - if counter_attribute_enabled?(attribute) - redis_state do |redis| - redis.get(counter_key(attribute)).to_i - end - end + log_clear_counter(attribute) end - def counter_key(attribute) - "project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}" - end - - def counter_flushed_key(attribute) - counter_key(attribute) + ':flushed' - end - - def counter_lock_key(attribute) - counter_key(attribute) + ':lock' - end - - def counter_attribute_enabled?(attribute) - self.class.counter_attribute_enabled?(attribute) + def execute_after_commit_callbacks + self.class.after_commit_callbacks.each do |callback| + callback.call(self.reset) + end end private - def database_lock_key - "project:{#{project_id}}:#{self.class}:#{id}" - end - - def steal_increments(increment_key, flushed_key) - redis_state do |redis| - redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key]) - end - end + def build_counter_for(attribute) + raise ArgumentError, %(attribute "#{attribute}" does not exist) unless has_attribute?(attribute) - def clear_counter!(attribute) - redis_state do |redis| - redis.del(counter_key(attribute)) - end - end - - def execute_after_flush_callbacks - self.class.after_flush_callbacks.each do |callback| - callback.call(self) + if counter_attribute_enabled?(attribute) + Gitlab::Counters::BufferedCounter.new(self, attribute) + else + Gitlab::Counters::LegacyCounter.new(self, attribute) end end - def redis_state(&block) - Gitlab::Redis::SharedState.with(&block) - end - - def with_exclusive_lease(lock_key) - in_lock(lock_key, ttl: WORKER_LOCK_TTL) do - yield - end - rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError - # a worker is already updating the counters + def database_lock_key + "project:{#{project_id}}:#{self.class}:#{id}" end # detect_race_on_record uses a lease to monitor access @@ -258,19 +184,6 @@ module CounterAttribute Gitlab::AppLogger.info(payload) end - def log_flush_counter(attribute, increment, previous_db_value, new_db_value) - payload = Gitlab::ApplicationContext.current.merge( - message: 'Flush counter attribute to database', - attribute: attribute, - project_id: project_id, - increment: increment, - previous_db_value: previous_db_value, - new_db_value: new_db_value - ) - - Gitlab::AppLogger.info(payload) - end - def log_clear_counter(attribute) payload = Gitlab::ApplicationContext.current.merge( message: 'Clear counter attribute', diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index ad070090dd5..1af655277b8 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -13,10 +13,11 @@ module HasUserType project_bot: 6, migration_bot: 7, security_bot: 8, - automation_bot: 9 + automation_bot: 9, + admin_bot: 11 }.with_indifferent_access.freeze - BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot].freeze + BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot admin_bot].freeze NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze @@ -24,7 +25,6 @@ module HasUserType scope :humans, -> { where(user_type: :human) } scope :bots, -> { where(user_type: BOT_USER_TYPES) } scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) } - scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) } scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) } scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) } scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) } diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 31b2a8d7cc1..9f0cd96a8f8 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -366,7 +366,7 @@ module Issuable select(issuable_columns) .select(extra_select_columns) - .from("#{table_name}") + .from(table_name.to_s) .joins("JOIN LATERAL(#{highest_priority}) as highest_priorities ON TRUE") .group(group_columns) .reorder(highest_priority_arel_with_direction.nulls_last) diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index a95bed7ad42..e95a8a42aa6 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -9,6 +9,12 @@ module Milestoneable extend ActiveSupport::Concern + class_methods do + def milestone_releases_subquery + Milestone.joins(:releases).where("#{table_name}.milestone_id = milestones.id") + end + end + included do belongs_to :milestone @@ -17,9 +23,15 @@ module Milestoneable scope :any_milestone, -> { where.not(milestone_id: nil) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) } - scope :any_release, -> { joins_milestone_releases } - scope :with_release, -> (tag, project_id) { joins_milestone_releases.where(milestones: { releases: { tag: tag, project_id: project_id } }) } - scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) } + scope :any_release, -> do + where("EXISTS (?)", milestone_releases_subquery) + end + scope :with_release, -> (tag, project_id) do + where("EXISTS (?)", milestone_releases_subquery.where(releases: { tag: tag, project_id: project_id })) + end + scope :without_particular_release, -> (tag, project_id) do + where("EXISTS (?)", milestone_releases_subquery.where.not(releases: { tag: tag, project_id: project_id })) + end scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) } @@ -30,11 +42,6 @@ module Milestoneable .where(milestone_releases: { release_id: nil }) end - scope :joins_milestone_releases, -> do - joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id - JOIN releases ON milestone_releases.release_id = releases.id").distinct - end - private def milestone_is_valid diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb index 4ad8d16fcb9..794748483e4 100644 --- a/app/models/concerns/sensitive_serializable_hash.rb +++ b/app/models/concerns/sensitive_serializable_hash.rb @@ -19,8 +19,6 @@ module SensitiveSerializableHash # In general, prefer NOT to use serializable_hash / to_json / as_json in favor # of serializers / entities instead which has an allowlist of attributes def serializable_hash(options = nil) - return super if options && options[:unsafe_serialization_hash] - options = options.try(:dup) || {} options[:except] = Array(options[:except]).dup diff --git a/app/models/concerns/signature_type.rb b/app/models/concerns/signature_type.rb new file mode 100644 index 00000000000..804f42b6f72 --- /dev/null +++ b/app/models/concerns/signature_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SignatureType + TYPES = %i[gpg ssh x509].freeze + + def type + raise NoMethodError, 'must implement `type` method' + end + + TYPES.each do |type| + define_method("#{type}?") { self.type == type } + end +end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index eccb004b503..6532a18d1b8 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -72,7 +72,7 @@ module Sortable private - def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) + def highest_label_priority(target_column:, project_column:, target_type_column: nil, target_type: nil, excluded_labels: []) query = Label.select(LabelPriority.arel_table[:priority].minimum.as('label_priority')) .left_join_priorities .joins(:label_links) diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index ee5774d4868..05addcf83d2 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -63,14 +63,15 @@ module Taskable def task_status(short: false) return '' if description.blank? - prep, completed = if short - ['/', ''] - else - [' of ', ' completed'] - end - sum = tasks.summary - "#{sum.complete_count}#{prep}#{sum.item_count} #{'checklist item'.pluralize(sum.item_count)}#{completed}" + checklist_item_noun = n_('checklist item', 'checklist items', sum.item_count) + if short + format(s_('Tasks|%{complete_count}/%{total_count} %{checklist_item_noun}'), +checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count) + else + format(s_('Tasks|%{complete_count} of %{total_count} %{checklist_item_noun} completed'), +checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count) + end end # Return a short string that describes the current state of this Taskable's diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 54fe9eac2bc..2b7447dc700 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -15,12 +15,13 @@ module TimeTrackable alias_method :time_spent?, :time_spent - default_value_for :time_estimate, value: 0, allows_nil: false + attribute :time_estimate, default: 0 validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false validate :check_negative_time_spent has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent + after_initialize :set_time_estimate_default_value end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -67,6 +68,13 @@ module TimeTrackable val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val) end + def set_time_estimate_default_value + return if new_record? + return unless has_attribute?(:time_estimate) + + self.time_estimate ||= self.class.column_defaults['time_estimate'] + end + private def reset_spent_time diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 7da4e31b472..db0fcd915b3 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -98,6 +98,8 @@ class ContainerRepository < ApplicationRecord ) end + before_update :set_status_updated_at_to_now, if: :status_changed? + state_machine :migration_state, initial: :default, use_transactions: false do state :pre_importing do validates :migration_pre_import_started_at, presence: true @@ -521,11 +523,20 @@ class ContainerRepository < ApplicationRecord end def set_delete_ongoing_status - update_columns(status: :delete_ongoing, delete_started_at: Time.zone.now) + now = Time.zone.now + update_columns( + status: :delete_ongoing, + delete_started_at: now, + status_updated_at: now + ) end def set_delete_scheduled_status - update_columns(status: :delete_scheduled, delete_started_at: nil) + update_columns( + status: :delete_scheduled, + delete_started_at: nil, + status_updated_at: Time.zone.now + ) end def migration_in_active_state? @@ -623,6 +634,10 @@ class ContainerRepository < ApplicationRecord tag end end + + def set_status_updated_at_to_now + self.status_updated_at = Time.zone.now + end end ContainerRepository.prepend_mod_with('ContainerRepository') diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb index 5eda9b4bf15..91656d4f846 100644 --- a/app/models/customer_relations/organization.rb +++ b/app/models/customer_relations/organization.rb @@ -85,8 +85,8 @@ class CustomerRelations::Organization < ApplicationRecord private def self.default_state_counts - states.keys.each_with_object({}) do |key, memo| - memo[key] = 0 + states.keys.index_with do |key| + 0 end end diff --git a/app/models/dependency_proxy/group_setting.rb b/app/models/dependency_proxy/group_setting.rb index 3a7ae66a263..b39ea36644a 100644 --- a/app/models/dependency_proxy/group_setting.rb +++ b/app/models/dependency_proxy/group_setting.rb @@ -3,7 +3,5 @@ class DependencyProxy::GroupSetting < ApplicationRecord belongs_to :group - attribute :enabled, default: true - validates :group, presence: true end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 66d1ce01814..498ca9c4f30 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -37,6 +37,7 @@ class DeployToken < ApplicationRecord message: "can contain only letters, digits, '_', '-', '+', and '.'" } + validates :expires_at, iso8601_date: true, on: :create validates :deploy_token_type, presence: true enum deploy_token_type: { group_type: 1, diff --git a/app/models/deployment.rb b/app/models/deployment.rb index ea92b978d3a..1254ce1c90a 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -363,6 +363,10 @@ class Deployment < ApplicationRecord deployable&.user || user end + def triggered_by?(user) + deployed_by == user + end + def link_merge_requests(relation) # NOTE: relation.select will perform column deduplication, # when id == environment_id it will outputs 2 columns instead of 3 @@ -441,9 +445,10 @@ class Deployment < ApplicationRecord # default tag limit is 100, 0 means no limit # when refs_by_oid is passed an SHA, returns refs for that commit def tags(limit: 100) - project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || [] + strong_memoize_with(:tag, limit) do + project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || [] + end end - strong_memoize_attr :tags private diff --git a/app/models/environment.rb b/app/models/environment.rb index 2d3f342953f..f1edfb3a34b 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,6 +6,7 @@ class Environment < ApplicationRecord include FastDestroyAll::Helpers include Presentable include NullifyIfBlank + include FromUnion self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 55.seconds @@ -27,27 +28,29 @@ class Environment < ApplicationRecord 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. - has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment - has_one :last_visible_deployment, -> { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment' + # NOTE: + # 1) no-op arguments is to prevent accidental legacy preloading. See: https://gitlab.com/gitlab-org/gitlab/-/issues/369240 + # 2) If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader. + has_one :last_deployment, -> (_env) { success.ordered }, class_name: 'Deployment', inverse_of: :environment + has_one :last_visible_deployment, -> (_env) { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment' + has_one :upcoming_deployment, -> (_env) { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment Deployment::FINISHED_STATUSES.each do |status| - has_one :"last_#{status}_deployment", -> { where(status: status).ordered }, + has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered }, class_name: 'Deployment', inverse_of: :environment end Deployment::UPCOMING_STATUSES.each do |status| - has_one :"last_#{status}_deployment", -> { where(status: status).ordered_as_upcoming }, + has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered_as_upcoming }, class_name: 'Deployment', inverse_of: :environment end - has_one :upcoming_deployment, -> { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment before_validation :generate_slug, if: ->(env) { env.slug.blank? } + before_validation :ensure_environment_tier before_save :set_environment_type - before_save :ensure_environment_tier after_save :clear_reactive_cache! validates :name, @@ -68,6 +71,10 @@ class Environment < ApplicationRecord length: { maximum: 255 }, allow_nil: true + # Currently, the tier presence is validaed for newly created environments. + # After the `BackfillEnvironmentTiers` background migration has been completed, we should remove `on: :create`. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/385253. + validates :tier, presence: true, on: :create validate :safe_external_url validate :merge_request_not_changed @@ -87,7 +94,6 @@ class Environment < ApplicationRecord scope :in_review_folder, -> { where(environment_type: "review") } scope :for_name, -> (name) { where(name: name) } - scope :preload_cluster, -> { preload(last_deployment: :cluster) } scope :preload_project, -> { preload(:project) } scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) } scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) } @@ -96,7 +102,16 @@ class Environment < ApplicationRecord # Search environments which have names like the given query. # Do not set a large limit unless you've confirmed that it works on gitlab.com scale. scope :for_name_like, -> (query, limit: 5) do - where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit) + top_level = 'LOWER(environments.name) LIKE LOWER(?) || \'%\'' + + where(top_level, sanitize_sql_like(query)).limit(limit) + end + + scope :for_name_like_within_folder, -> (query, limit: 5) do + within_folder = 'LOWER(ltrim(environments.name, environments.environment_type'\ + ' || \'/\')) LIKE LOWER(?) || \'%\'' + + where(within_folder, sanitize_sql_like(query)).limit(limit) end scope :for_project, -> (project) { where(project_id: project) } @@ -106,7 +121,6 @@ class Environment < ApplicationRecord scope :with_rank, -> do select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)') end - scope :for_id, -> (id) { where(id: id) } scope :with_deployment, -> (sha, status: nil) do deployments = Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha) @@ -197,12 +211,19 @@ class Environment < ApplicationRecord update_all(auto_delete_at: at_time) end + def self.nested + group('COALESCE(environment_type, id::text)', 'COALESCE(environment_type, name)') + .select('COALESCE(environment_type, id::text), COALESCE(environment_type, name) AS name', + 'COUNT(*) AS size', 'MAX(id) AS last_id') + .order('name ASC') + end + class << self def count_by_state environments_count_by_state = group(:state).count - valid_states.each_with_object({}) do |state, count_hash| - count_hash[state] = environments_count_by_state[state.to_s] || 0 + valid_states.index_with do |state| + environments_count_by_state[state.to_s] || 0 end end end @@ -490,6 +511,12 @@ class Environment < ApplicationRecord environment_type.nil? end + def deploy_freezes + Gitlab::SafeRequestStore.fetch("project:#{project_id}:freeze_periods_for_environments") do + project.freeze_periods + end + end + private # We deliberately avoid using AddressableUrlValidator to allow users to update their environments even if they have diff --git a/app/models/event.rb b/app/models/event.rb index a1417db3410..ed65b367b8a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -132,7 +132,7 @@ class Event < ApplicationRecord where( 'action IN (?) OR (target_type IN (?) AND action IN (?))', [actions[:pushed], actions[:commented]], - %w(MergeRequest Issue), [actions[:created], actions[:closed], actions[:merged]] + %w(MergeRequest Issue WorkItem), [actions[:created], actions[:closed], actions[:merged]] ) end @@ -380,13 +380,11 @@ class Event < ApplicationRecord protected def capability - @capability ||= begin - capabilities.flat_map do |ability, syms| - if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend - [ability] - else - [] - end + @capability ||= capabilities.flat_map do |ability, syms| + if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend + [ability] + else + [] end end end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 6c8bfc35334..b02074849a1 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -3,8 +3,6 @@ class GenericCommitStatus < CommitStatus EXTERNAL_STAGE_IDX = 1_000_000 - before_validation :set_default_values - validates :target_url, addressable_url: true, length: { maximum: 255 }, allow_nil: true @@ -13,12 +11,6 @@ class GenericCommitStatus < CommitStatus # GitHub compatible API alias_attribute :context, :name - def set_default_values - self.context ||= 'default' - self.stage ||= 'external' - self.stage_idx ||= EXTERNAL_STAGE_IDX - end - def tags [:external] end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 2db074e733e..1bf35179393 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -40,8 +40,8 @@ class GpgKey < ApplicationRecord unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint, :extract_primary_keyid - after_commit :update_invalid_gpg_signatures, on: :create after_create :generate_subkeys + after_commit :update_invalid_gpg_signatures, on: :create def primary_keyid super&.upcase diff --git a/app/models/group.rb b/app/models/group.rb index 098116ed800..0cdd7dd8596 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -20,6 +20,7 @@ class Group < Namespace include BulkUsersByEmailLoad include ChronicDurationAttribute include RunnerTokenExpirationInterval + include Todoable extend ::Gitlab::Utils::Override @@ -119,7 +120,7 @@ class Group < Namespace has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id - has_many :protected_branches, inverse_of: :group + has_many :protected_branches, inverse_of: :group, foreign_key: :namespace_id has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting' @@ -154,10 +155,10 @@ class Group < Namespace prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX after_create :post_create_hook + after_create -> { create_or_load_association(:group_feature) } + after_update :path_changed_hook, if: :saved_change_to_path? after_destroy :post_destroy_hook after_commit :update_two_factor_requirement - after_update :path_changed_hook, if: :saved_change_to_path? - after_create -> { create_or_load_association(:group_feature) } scope :with_users, -> { includes(:users) } @@ -165,7 +166,16 @@ class Group < Namespace scope :by_id, ->(groups) { where(id: groups) } - scope :by_ids_or_paths, -> (ids, paths) { by_id(ids).or(where(path: paths)) } + scope :by_ids_or_paths, -> (ids, paths) do + return by_id(ids) unless paths.present? + + ids_by_full_path = Route + .for_routable_type(Namespace.name) + .where('LOWER(routes.path) IN (?)', paths.map(&:downcase)) + .select(:namespace_id) + + Group.from_union([by_id(ids), by_id(ids_by_full_path), where('LOWER(path) IN (?)', paths.map(&:downcase))]) + end scope :for_authorized_group_members, -> (user_ids) do joins(:group_members) @@ -550,6 +560,11 @@ class Group < Namespace members_with_parents.pluck(Arel.sql('DISTINCT members.user_id')) end + def self_and_hierarchy_intersecting_with_user_groups(user) + user_groups = GroupsFinder.new(user).execute.unscope(:order) + self_and_hierarchy.unscope(:order).where(id: user_groups) + end + def self_and_ancestors_ids strong_memoize(:self_and_ancestors_ids) do self_and_ancestors.pluck(:id) @@ -831,6 +846,7 @@ class Group < Namespace def has_project_with_service_desk_enabled? Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists? end + strong_memoize_attr :has_project_with_service_desk_enabled?, :has_project_with_service_desk_enabled def activity_path Gitlab::Routing.url_helpers.activity_group_path(self) @@ -887,6 +903,10 @@ class Group < Namespace feature_flag_enabled_for_self_or_ancestor?(:work_items) end + def work_items_mvc_feature_flag_enabled? + feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc) + end + def work_items_mvc_2_feature_flag_enabled? feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2) end diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb index c65b00a6de0..9495df7ab6d 100644 --- a/app/models/group_deploy_key.rb +++ b/app/models/group_deploy_key.rb @@ -12,6 +12,11 @@ class GroupDeployKey < Key joins(:group_deploy_keys_groups).where(group_deploy_keys_groups: { group_id: group_ids }).uniq end + # Remove usage_type because it defined in Key class but doesn't have a column in group_deploy_keys table + def self.defined_enums + super.without('usage_type') + end + def type 'DeployKey' end diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb index cdcfd3f3ff5..4599ebf8717 100644 --- a/app/models/hooks/active_hook_filter.rb +++ b/app/models/hooks/active_hook_filter.rb @@ -18,10 +18,6 @@ class ActiveHookFilter branch_name = Gitlab::Git.branch_name(data[:ref]) - if Feature.disabled?(:enhanced_webhook_support_regex) - return RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name) - end - case @hook.branch_filter_strategy when 'all_branches' true diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 27119d3a95a..94ced96bbde 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -13,4 +13,9 @@ class ServiceHook < WebHook override :parent delegate :parent, to: :integration + + override :executable? + def executable? + true + end end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 946cdda2e75..189291a38ec 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -41,12 +41,9 @@ class WebHook < ApplicationRecord after_initialize :initialize_url_variables before_validation :reset_token - before_validation :set_branch_filter_nil, \ - if: -> { branch_filter_strategy_all_branches? && enhanced_webhook_support_regex? } - validates :push_events_branch_filter, \ - untrusted_regexp: true, if: -> { branch_filter_strategy_regex? && enhanced_webhook_support_regex? } - validates :push_events_branch_filter, \ - "web_hooks/wildcard_branch_filter": true, if: -> { branch_filter_strategy_wildcard? } + before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches? + validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex? + validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard? validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' } validate :no_missing_url_variables @@ -59,8 +56,6 @@ class WebHook < ApplicationRecord }, _prefix: true scope :executable, -> do - next all unless Feature.enabled?(:web_hooks_disable_failed) - where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current) end @@ -69,23 +64,17 @@ class WebHook < ApplicationRecord where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current) end - def self.web_hooks_disable_failed?(hook) - Feature.enabled?(:web_hooks_disable_failed, hook.parent) - end - def executable? !temporarily_disabled? && !permanently_disabled? end def temporarily_disabled? - return false unless web_hooks_disable_failed? return false if recent_failures <= FAILURE_THRESHOLD disabled_until.present? && disabled_until >= Time.current end def permanently_disabled? - return false unless web_hooks_disable_failed? return false if disabled_until.present? recent_failures > FAILURE_THRESHOLD @@ -197,7 +186,7 @@ class WebHook < ApplicationRecord end # See app/validators/json_schemas/web_hooks_url_variables.json - VARIABLE_REFERENCE_RE = /\{([A-Za-z_][A-Za-z0-9_]+)\}/.freeze + VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze def interpolated_url return url unless url.include?('{') @@ -232,10 +221,6 @@ class WebHook < ApplicationRecord backoff_count.succ.clamp(1, MAX_FAILURES) end - def web_hooks_disable_failed? - self.class.web_hooks_disable_failed?(self) - end - def initialize_url_variables self.url_variables = {} if encrypted_url_variables.nil? end @@ -257,10 +242,6 @@ class WebHook < ApplicationRecord errors.add(:url, "Invalid URL template. Missing keys: #{missing}") end - def enhanced_webhook_support_regex? - Feature.enabled?(:enhanced_webhook_support_regex) - end - def set_branch_filter_nil self.push_events_branch_filter = nil end diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb index bc363cce8dd..bdb53653637 100644 --- a/app/models/import_export_upload.rb +++ b/app/models/import_export_upload.rb @@ -2,7 +2,6 @@ class ImportExportUpload < ApplicationRecord include WithUploads - include ObjectStorage::BackgroundMove belongs_to :project belongs_to :group diff --git a/app/models/integration.rb b/app/models/integration.rb index 41278dce22d..a630a6dee11 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -19,7 +19,7 @@ class Integration < ApplicationRecord INTEGRATION_NAMES = %w[ asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord - drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira + drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao ].freeze @@ -41,7 +41,9 @@ class Integration < ApplicationRecord Integrations::BaseCi Integrations::BaseIssueTracker Integrations::BaseMonitoring + Integrations::BaseSlackNotification Integrations::BaseSlashCommands + Integrations::BaseThirdPartyWiki ].freeze SECTION_TYPE_CONFIGURATION = 'configuration' diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb index 2cfd71c9eb2..b8cfd718007 100644 --- a/app/models/integrations/asana.rb +++ b/app/models/integrations/asana.rb @@ -42,10 +42,8 @@ module Integrations end def client - @_client ||= begin - ::Asana::Client.new do |c| - c.authentication :access_token, api_key - end + @_client ||= ::Asana::Client.new do |c| + c.authentication :access_token, api_key end end diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index b4e97f0871e..fc5e6a88c2d 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -16,7 +16,7 @@ module Integrations help: -> { s_('BambooService|Bamboo build plan key.') }, non_empty_password_title: -> { s_('BambooService|Enter new build key') }, non_empty_password_help: -> { s_('BambooService|Leave blank to use your current build key.') }, - placeholder: -> { s_('KEY') }, + placeholder: -> { _('KEY') }, required: true field :username, diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index 750aa60b185..f2a707c2214 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -33,7 +33,10 @@ module Integrations boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch - validates :webhook, presence: true, public_url: true, if: :activated? + validates :webhook, + presence: true, + public_url: true, + if: -> (integration) { integration.activated? && integration.requires_webhook? } validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true def initialize_properties @@ -73,8 +76,6 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', help: "#{webhook_help}", required: true }.freeze, - { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze, { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze, { type: 'select', @@ -96,19 +97,24 @@ module Integrations ['Match all of the labels', MATCH_ALL_LABELS] ] }.freeze - ].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 end def execute(data) - return unless supported_events.include?(data[:object_kind]) - - return unless webhook.present? - object_kind = data[:object_kind] + return false unless should_execute?(object_kind) + data = custom_data(data) - return unless notify_label?(data) + return false unless notify_label?(data) # WebHook events often have an 'update' event that follows a 'open' or # 'close' action. Ignore update events for now to prevent duplicate @@ -168,8 +174,17 @@ module Integrations self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend end + def requires_webhook? + true + end + private + def should_execute?(object_kind) + supported_events.include?(object_kind) && + (!requires_webhook? || webhook.present?) + end + def log_usage(_, _) # Implement in child class end diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index cb785afdcfe..7a2a91aa0d2 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -32,13 +32,15 @@ module Integrations true end + private + override :log_usage def log_usage(event, user_id) return unless user_id return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event) - key = "i_ecosystem_slack_service_#{event}_notification" + key = "#{metrics_key_prefix}_#{event}_notification" Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id) @@ -55,8 +57,13 @@ module Integrations label: Integration::SNOWPLOW_EVENT_LABEL, property: key, user: User.find(user_id), + context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context], **optional_arguments ) end + + def metrics_key_prefix + raise NotImplementedError + end end end diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb index 314f0a6ee5d..11ff7547325 100644 --- a/app/models/integrations/base_slash_commands.rb +++ b/app/models/integrations/base_slash_commands.rb @@ -60,7 +60,7 @@ module Integrations # rubocop: disable CodeReuse/ServiceClass def find_chat_user(params) - ChatNames::FindUserService.new(self, params).execute + ChatNames::FindUserService.new(params[:team_id], params[:user_id]).execute end # rubocop: enable CodeReuse/ServiceClass diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb index c1c43af99bf..31e9a171d1b 100644 --- a/app/models/integrations/confluence.rb +++ b/app/models/integrations/confluence.rb @@ -10,7 +10,7 @@ module Integrations validate :validate_confluence_url_is_cloud, if: :activated? field :confluence_url, - title: -> { s_('Confluence Cloud Workspace URL') }, + title: -> { _('Confluence Cloud Workspace URL') }, placeholder: 'https://example.atlassian.net/wiki', required: true diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index 27bed5d3f76..80eecc14d0f 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -9,7 +9,7 @@ module Integrations URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/" SUPPORTED_EVENTS = %w[ - pipeline job archive_trace + pipeline build archive_trace ].freeze TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze @@ -48,8 +48,8 @@ module Integrations field :archive_trace_events, storage: :attribute, type: 'checkbox', - title: -> { s_('Logs') }, - checkbox_label: -> { s_('Enable logs collection') }, + title: -> { _('Logs') }, + checkbox_label: -> { _('Enable logs collection') }, help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') } field :datadog_service, @@ -156,10 +156,10 @@ module Integrations end def execute(data) + return unless supported_events.include?(data[:object_kind]) + object_kind = data[:object_kind] object_kind = 'job' if object_kind == 'build' - return unless supported_events.include?(object_kind) - data = hook_data(data, object_kind) execute_web_hook!(data, "#{object_kind} hook") end diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb index 52efb29f2c1..d7625cfb3d2 100644 --- a/app/models/integrations/flowdock.rb +++ b/app/models/integrations/flowdock.rb @@ -1,28 +1,12 @@ # frozen_string_literal: true +# This integration is scheduled for removal. +# All records must be deleted before the class can be removed. +# https://gitlab.com/gitlab-org/gitlab/-/issues/379197 module Integrations class Flowdock < Integration - validates :token, presence: true, if: :activated? - - field :token, - type: 'password', - help: -> { s_('FlowdockService|Enter your Flowdock token.') }, - non_empty_password_title: -> { s_('ProjectService|Enter new token') }, - non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, - placeholder: '1b609b52537...', - required: true - - def title - 'Flowdock' - end - - def description - s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.') - end - - def help - docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer' - s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } + def readonly? + true end def self.to_param @@ -30,22 +14,7 @@ module Integrations end def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - ::Flowdock::Git.post( - data[:ref], - data[:before], - data[:after], - token: token, - repo: project.repository, - repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", - commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s", - diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" - ) + %w[] end end end diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index 65492bfd9c2..45302a0bd09 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -132,11 +132,9 @@ module Integrations end def client - @client ||= begin - JIRA::Client.new(options).tap do |client| - # Replaces JIRA default http client with our implementation - client.request_client = Gitlab::Jira::HttpClient.new(client.options) - end + @client ||= JIRA::Client.new(options).tap do |client| + # Replaces JIRA default http client with our implementation + client.request_client = Gitlab::Jira::HttpClient.new(client.options) end end @@ -406,6 +404,7 @@ module Integrations label: Integration::SNOWPLOW_EVENT_LABEL, property: key, user: user, + context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context], **optional_arguments ) end diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb index dd1c98ee06b..e3c5c22ad3a 100644 --- a/app/models/integrations/mattermost.rb +++ b/app/models/integrations/mattermost.rb @@ -5,7 +5,7 @@ module Integrations include SlackMattermostNotifier def title - s_('Mattermost notifications') + _('Mattermost notifications') end def description diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb index 7148de66aee..3973b492b6d 100644 --- a/app/models/integrations/packagist.rb +++ b/app/models/integrations/packagist.rb @@ -5,15 +5,15 @@ module Integrations include HasWebHook field :username, - title: -> { s_('Username') }, - help: -> { s_('Enter your Packagist username.') }, + title: -> { _('Username') }, + help: -> { _('Enter your Packagist username.') }, placeholder: '', required: true field :token, type: 'password', - title: -> { s_('Token') }, - help: -> { s_('Enter your Packagist token.') }, + title: -> { _('Token') }, + help: -> { _('Enter your Packagist token.') }, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, placeholder: '', diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index 791e27c5db7..6bb6b6d60f6 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -112,7 +112,7 @@ module Integrations user: user_key, device: device, priority: priority, - title: "#{project.full_name}", + title: project.full_name.to_s, message: message, url: data[:project][:web_url], url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name } diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb index 89326b8174f..07d2d802915 100644 --- a/app/models/integrations/slack.rb +++ b/app/models/integrations/slack.rb @@ -20,5 +20,12 @@ module Integrations def webhook_help 'https://hooks.slack.com/services/…' end + + private + + override :metrics_key_prefix + def metrics_key_prefix + 'i_ecosystem_slack_service' + end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index fc083002c41..1dd11ff8315 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -91,7 +91,7 @@ class Issue < ApplicationRecord has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus' has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany - has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue + has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue, validate: false has_many :prometheus_alerts, through: :prometheus_alert_events has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues @@ -105,9 +105,10 @@ class Issue < ApplicationRecord validates :project, presence: true validates :issue_type, presence: true - validates :namespace, presence: true, if: -> { project.present? } + validates :namespace, presence: true validates :work_item_type, presence: true + validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed? validate :due_date_after_start_date validate :parent_link_confidentiality @@ -180,7 +181,7 @@ class Issue < ApplicationRecord scope :without_hidden, -> { if Feature.enabled?(:ban_user_feature_flag) - where.not(author_id: Users::BannedUser.all.select(:user_id)) + where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id')) else all end @@ -216,8 +217,8 @@ class Issue < ApplicationRecord before_validation :ensure_namespace_id, :ensure_work_item_type - after_commit :expire_etag_cache, unless: :importing? after_save :ensure_metrics, unless: :importing? + after_commit :expire_etag_cache, unless: :importing? after_create_commit :record_create_action, unless: :importing? attr_spammable :title, spam_title: true @@ -743,6 +744,17 @@ class Issue < ApplicationRecord self.work_item_type = WorkItems::Type.default_by_type(issue_type) end + + def allowed_work_item_type_change + return unless changes[:work_item_type_id] + + involved_types = WorkItems::Type.where(id: changes[:work_item_type_id].compact).pluck(:base_type).uniq + disallowed_types = involved_types - WorkItems::Type::CHANGEABLE_BASE_TYPES + + return if disallowed_types.empty? + + errors.add(:work_item_type_id, format(_('can not be changed to %{new_type}'), new_type: work_item_type&.name)) + end end Issue.prepend_mod_with('Issue') diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb deleted file mode 100644 index 05607fc3a08..00000000000 --- a/app/models/issue_collection.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -# IssueCollection can be used to reduce a list of issues down to a subset. -# -# IssueCollection is not meant to be some sort of Enumerable, instead it's meant -# to take a list of issues and return a new list of issues based on some -# criteria. For example, given a list of issues you may want to return a list of -# issues that can be read or updated by a given user. -class IssueCollection - attr_reader :collection - - def initialize(collection) - @collection = collection - end - - # Returns all the issues that can be updated by the user. - def updatable_by_user(user) - return collection if user.admin? - - # Given all the issue projects we get a list of projects that the current - # user has at least reporter access to. - projects_with_reporter_access = user - .projects_with_reporter_access_limited_to(project_ids) - .pluck(:id) - - collection.select do |issue| - if projects_with_reporter_access.include?(issue.project_id) - true - elsif issue.is_a?(Issue) - issue.assignee_or_author?(user) - else - false - end - end - end - - alias_method :visible_to, :updatable_by_user - - private - - def project_ids - @project_ids ||= collection.map(&:project_id).uniq - end -end diff --git a/app/models/issue_email_participant.rb b/app/models/issue_email_participant.rb index 76a96151350..dd963bc9e7e 100644 --- a/app/models/issue_email_participant.rb +++ b/app/models/issue_email_participant.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class IssueEmailParticipant < ApplicationRecord + include BulkInsertSafe + belongs_to :issue validates :email, uniqueness: { scope: [:issue_id], case_sensitive: false } diff --git a/app/models/iteration.rb b/app/models/iteration.rb index c6269313d8b..ebec24731ed 100644 --- a/app/models/iteration.rb +++ b/app/models/iteration.rb @@ -4,9 +4,6 @@ class Iteration < ApplicationRecord include IgnorableColumns - # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372126 - ignore_column :project_id, remove_with: '15.7', remove_after: '2022-11-18' - self.table_name = 'sprints' def self.reference_prefix diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb index 23813fa138f..0e88d1ceae9 100644 --- a/app/models/jira_connect_installation.rb +++ b/app/models/jira_connect_installation.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class JiraConnectInstallation < ApplicationRecord + include Gitlab::Routing + attr_encrypted :shared_secret, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', @@ -37,13 +39,19 @@ class JiraConnectInstallation < ApplicationRecord def audience_url return unless proxy? - Gitlab::Utils.append_path(instance_url, '/-/jira_connect') + Gitlab::Utils.append_path(instance_url, jira_connect_base_path) end def audience_installed_event_url return unless proxy? - Gitlab::Utils.append_path(instance_url, '/-/jira_connect/events/installed') + Gitlab::Utils.append_path(instance_url, jira_connect_events_installed_path) + end + + def audience_uninstalled_event_url + return unless proxy? + + Gitlab::Utils.append_path(instance_url, jira_connect_events_uninstalled_path) end def proxy? diff --git a/app/models/key.rb b/app/models/key.rb index 78b0a38bcaa..1f2234129ed 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -32,12 +32,18 @@ class Key < ApplicationRecord delegate :name, :email, to: :user, prefix: true - after_commit :add_to_authorized_keys, on: :create + enum usage_type: { + auth_and_signing: 0, + auth: 1, + signing: 2 + } + after_create :post_create_hook after_create :refresh_user_cache - after_commit :remove_from_authorized_keys, on: :destroy after_destroy :post_destroy_hook after_destroy :refresh_user_cache + after_commit :add_to_authorized_keys, on: :create + after_commit :remove_from_authorized_keys, on: :destroy alias_attribute :fingerprint_md5, :fingerprint alias_attribute :name, :title @@ -45,6 +51,8 @@ class Key < ApplicationRecord scope :preload_users, -> { preload(:user) } scope :for_user, -> (user) { where(user: user) } scope :order_last_used_at_desc, -> { reorder(arel_table[:last_used_at].desc.nulls_last) } + scope :auth, -> { where(usage_type: [:auth, :auth_and_signing]) } + scope :signing, -> { where(usage_type: [:signing, :auth_and_signing]) } # Date is set specifically in this scope to improve query time. scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) } diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 8aa48561e60..e1f28c0e117 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -4,7 +4,6 @@ class LfsObject < ApplicationRecord include AfterCommitQueue include Checksummable include EachBatch - include ObjectStorage::BackgroundMove include FileStoreMounter has_many :lfs_objects_projects diff --git a/app/models/member.rb b/app/models/member.rb index 80c5fd7e468..107530daf51 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -61,6 +61,7 @@ class Member < ApplicationRecord validate :access_level_inclusion validate :validate_member_role_access_level validate :validate_access_level_locked_for_member_role, on: :update + validate :validate_member_role_belongs_to_same_root_namespace scope :with_invited_user_state, -> do joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email') @@ -515,12 +516,22 @@ class Member < ApplicationRecord end end + def validate_member_role_belongs_to_same_root_namespace + return unless member_role_id + + return if member_namespace.id == member_role.namespace_id + return if member_namespace.root_ancestor.id == member_role.namespace_id + + errors.add(:member_namespace, _("must be in same hierarchy as custom role's namespace")) + end + def send_invite # override in subclass end def send_request notification_service.new_access_request(self) + todo_service.create_member_access_request(self) if source_type != 'Project' end def post_create_hook @@ -579,6 +590,12 @@ class Member < ApplicationRecord end # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass + def todo_service + TodoService.new + end + # rubocop: enable CodeReuse/ServiceClass + def notifiable_options {} end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index ad1ad1e74fe..796b05b7fff 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -55,6 +55,12 @@ class GroupMember < Member { group: group } end + def last_owner_of_the_group? + return false unless access_level == Gitlab::Access::OWNER + + group.member_last_owner?(self) || group.member_last_blocked_owner?(self) + end + private override :refresh_member_authorized_projects diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb index b4e3d6874ef..e9d7b1d3f80 100644 --- a/app/models/members/member_role.rb +++ b/app/models/members/member_role.rb @@ -1,18 +1,30 @@ # frozen_string_literal: true class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass + include IgnorableColumns + ignore_column :download_code, remove_with: '15.9', remove_after: '2023-01-22' + has_many :members belongs_to :namespace validates :namespace, presence: true validates :base_access_level, presence: true validate :belongs_to_top_level_namespace + validate :validate_namespace_locked, on: :update + + validates_associated :members private def belongs_to_top_level_namespace return if !namespace || namespace.root? - errors.add(:namespace, s_("must be top-level namespace")) + errors.add(:namespace, s_("MemberRole|must be top-level namespace")) + end + + def validate_namespace_locked + return unless namespace_id_changed? + + errors.add(:namespace, s_("MemberRole|can't be changed")) end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 1099e0f48c0..6aa6afb595d 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -96,6 +96,10 @@ class ProjectMember < Member { project: project } end + def holder_of_the_personal_namespace? + project.personal_namespace_holder?(user) + end + private override :access_level_inclusion diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 735c0df1529..78c6d983a3d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -121,6 +121,7 @@ class MergeRequest < ApplicationRecord has_many :draft_notes has_many :reviews, inverse_of: :merge_request + has_many :reviewed_by_users, -> { distinct }, through: :reviews, source: :author has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request KNOWN_MERGE_PARAMS = [ @@ -139,6 +140,7 @@ class MergeRequest < ApplicationRecord after_create :ensure_merge_request_diff, unless: :skip_ensure_merge_request_diff after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed + after_save :keep_around_commit, unless: :importing? after_commit :ensure_metrics, on: [:create, :update], unless: :importing? after_commit :expire_etag_cache, unless: :importing? @@ -246,7 +248,9 @@ class MergeRequest < ApplicationRecord end after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition| - GraphqlTriggers.merge_request_merge_status_updated(merge_request) + merge_request.run_after_commit do + GraphqlTriggers.merge_request_merge_status_updated(merge_request) + end end # rubocop: disable CodeReuse/ServiceClass @@ -438,8 +442,6 @@ class MergeRequest < ApplicationRecord .pick(MergeRequest::Metrics.time_to_merge_expression) end - after_save :keep_around_commit, unless: :importing? - alias_attribute :project, :target_project alias_attribute :project_id, :target_project_id @@ -1270,7 +1272,7 @@ class MergeRequest < ApplicationRecord end def mergeable_discussions_state? - return true unless project.only_allow_merge_if_all_discussions_are_resolved? + return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true) unresolved_notes.none?(&:to_be_resolved?) end @@ -1382,7 +1384,7 @@ class MergeRequest < ApplicationRecord def default_merge_commit_message(include_description: false, user: nil) if self.target_project.merge_commit_template.present? && !include_description - return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self, current_user: user).merge_message + return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).merge_commit_message end closes_issues_references = visible_closing_issues_for.map do |issue| @@ -1398,7 +1400,7 @@ class MergeRequest < ApplicationRecord message << "Closes #{closes_issues_references.to_sentence}" end - message << "#{description}" if include_description && description.present? + message << description if include_description && description.present? message << "See merge request #{to_reference(full: true)}" message.join("\n\n") @@ -1406,7 +1408,7 @@ class MergeRequest < ApplicationRecord def default_squash_commit_message(user: nil) if self.target_project.squash_commit_template.present? - return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self, current_user: user).squash_message + return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).squash_commit_message end title @@ -1451,9 +1453,9 @@ class MergeRequest < ApplicationRecord end def mergeable_ci_state? - return true unless project.only_allow_merge_if_pipeline_succeeds? + return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true) return false unless actual_head_pipeline - return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped? + return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped? actual_head_pipeline.success? end diff --git a/app/models/merge_request/predictions.rb b/app/models/merge_request/predictions.rb deleted file mode 100644 index ef9e00b5f74..00000000000 --- a/app/models/merge_request/predictions.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class MergeRequest::Predictions < ApplicationRecord # rubocop:disable Style/ClassAndModuleChildren - belongs_to :merge_request, inverse_of: :predictions - - validates :suggested_reviewers, json_schema: { filename: 'merge_request_predictions_suggested_reviewers' } -end diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb index ebbdecf8aa7..281e11c7c13 100644 --- a/app/models/merge_request_context_commit.rb +++ b/app/models/merge_request_context_commit.rb @@ -12,7 +12,7 @@ class MergeRequestContextCommit < ApplicationRecord validates :sha, presence: true validates :sha, uniqueness: { message: 'has already been added' } - serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize + attribute :trailers, :ind_jsonb validates :trailers, json_schema: { filename: 'git_trailers' } # Sort by committed date in descending order to ensure latest commits comes on the top diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 98a9ccc2040..cff8911d84b 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,7 +6,6 @@ class MergeRequestDiff < ApplicationRecord include ManualInverseAssociation include EachBatch include Gitlab::Utils::StrongMemoize - include ObjectStorage::BackgroundMove include BulkInsertableAssociations # Don't display more than 100 commits at once @@ -267,7 +266,7 @@ class MergeRequestDiff < ApplicationRecord end # This method will rely on repository branch sha - # in case start_commit_sha is nil. Its necesarry for old merge request diff + # in case start_commit_sha is nil. It's necessary for old merge request diff # created before version 8.4 to work def safe_start_commit_sha start_commit_sha || merge_request.target_branch_sha @@ -414,6 +413,29 @@ class MergeRequestDiff < ApplicationRecord end end + def paginated_diffs(page, per_page) + fetching_repository_diffs({}) do |comparison| + reorder_diff_files! + + collection = Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff.new( + self, + page, + per_page + ) + + if comparison + comparison.diffs( + paths: collection.diff_paths, + page: collection.current_page, + per_page: collection.limit_value, + count: collection.total_count + ) + else + collection + end + end + end + def diffs(diff_options = nil) fetching_repository_diffs(diff_options) do |comparison| # It should fetch the repository when diffs are cleaned by the system. diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 152fb195c97..7e2efa2049b 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -35,7 +35,7 @@ class MergeRequestDiffCommit < ApplicationRecord sha_attribute :sha alias_attribute :id, :sha - serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize + attribute :trailers, :ind_jsonb validates :trailers, json_schema: { filename: 'git_trailers' } scope :with_users, -> { preload(:commit_author, :committer) } diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index f7da4418624..f24161d598f 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -11,6 +11,7 @@ module Ml belongs_to :user has_many :metrics, class_name: 'Ml::CandidateMetric' has_many :params, class_name: 'Ml::CandidateParam' + has_many :metadata, class_name: 'Ml::CandidateMetadata' has_many :latest_metrics, -> { latest }, class_name: 'Ml::CandidateMetric', inverse_of: :candidate attribute :iid, default: -> { SecureRandom.uuid } @@ -18,7 +19,21 @@ module Ml scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) } def artifact_root - "/ml_candidate_#{iid}/-/" + "/#{package_name}/#{package_version}/" + end + + def artifact + ::Packages::Generic::PackageFinder.new(experiment.project).execute!(package_name, package_version) + rescue ActiveRecord::RecordNotFound + nil + end + + def package_name + "ml_candidate_#{iid}" + end + + def package_version + '-' end class << self diff --git a/app/models/ml/candidate_metadata.rb b/app/models/ml/candidate_metadata.rb new file mode 100644 index 00000000000..06b893c211f --- /dev/null +++ b/app/models/ml/candidate_metadata.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Ml + class CandidateMetadata < ApplicationRecord + validates :candidate, presence: true + validates :name, + length: { maximum: 250 }, + presence: true, + uniqueness: { scope: :candidate, message: ->(candidate, _) { "'#{candidate.name}' already taken" } } + validates :value, length: { maximum: 5000 }, presence: true + + belongs_to :candidate, class_name: 'Ml::Candidate' + end +end diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index 05b238b960d..0a326b0e005 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -10,6 +10,7 @@ module Ml belongs_to :project belongs_to :user has_many :candidates, class_name: 'Ml::Candidate' + has_many :metadata, class_name: 'Ml::ExperimentMetadata' has_internal_id :iid, scope: :project diff --git a/app/models/ml/experiment_metadata.rb b/app/models/ml/experiment_metadata.rb new file mode 100644 index 00000000000..93496807e1a --- /dev/null +++ b/app/models/ml/experiment_metadata.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Ml + class ExperimentMetadata < ApplicationRecord + validates :experiment, presence: true + validates :name, + length: { maximum: 250 }, + presence: true, + uniqueness: { scope: :experiment, message: ->(exp, _) { "'#{exp.name}' already taken" } } + validates :value, length: { maximum: 5000 }, presence: true + + belongs_to :experiment, class_name: 'Ml::Experiment' + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 51c39ad4ec3..d7d53956656 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -86,6 +86,7 @@ class Namespace < ApplicationRecord has_many :issues, inverse_of: :namespace has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory' + has_many :achievements, class_name: 'Achievements::Achievement' validates :owner, presence: true, if: ->(n) { n.owner_required? } validates :name, @@ -131,26 +132,28 @@ class Namespace < ApplicationRecord to: :namespace_settings, allow_nil: true delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=, to: :namespace_settings + delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=, + to: :namespace_settings delegate :maven_package_requests_forwarding, :pypi_package_requests_forwarding, :npm_package_requests_forwarding, to: :package_settings - after_save :reload_namespace_details - - after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } - before_create :sync_share_with_group_lock_with_parent before_update :sync_share_with_group_lock_with_parent, if: :parent_changed? 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 + + after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } + after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear # Legacy Storage specific hooks - after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) } before_destroy(prepend: true) { prepare_for_destroy } - after_destroy :rm_dir 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? @@ -330,6 +333,13 @@ class Namespace < ApplicationRecord type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?) end + def bot_user_namespace? + return false unless user_namespace? + return false unless owner && owner.bot? + + true + end + def owner_required? user_namespace? end @@ -507,6 +517,10 @@ class Namespace < ApplicationRecord root? && actual_plan.paid? end + def prevent_delete? + paid? + end + def actual_limits # We default to PlanLimits.new otherwise a lot of specs would fail # On production each plan should already have associated limits record @@ -541,12 +555,10 @@ class Namespace < ApplicationRecord def shared_runners_setting if shared_runners_enabled SR_ENABLED + elsif allow_descendants_override_disabled_shared_runners + SR_DISABLED_WITH_OVERRIDE else - if allow_descendants_override_disabled_shared_runners - SR_DISABLED_WITH_OVERRIDE - else - SR_DISABLED_AND_UNOVERRIDABLE - end + SR_DISABLED_AND_UNOVERRIDABLE end end @@ -597,6 +609,10 @@ class Namespace < ApplicationRecord namespace_settings&.enabled_git_access_protocol end + def all_ancestors_have_runner_registration_enabled? + namespace_settings&.all_ancestors_have_runner_registration_enabled? + end + private def cluster_enabled_granted? diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 3e6371b0c4d..5081d5cdafe 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -59,6 +59,16 @@ class NamespaceSetting < ApplicationRecord all_ancestors_allow_diff_preview_in_email? end + def runner_registration_enabled? + runner_registration_enabled && all_ancestors_have_runner_registration_enabled? + end + + def all_ancestors_have_runner_registration_enabled? + return true unless namespace.has_parent? + + !self.class.where(namespace_id: namespace.ancestors, runner_registration_enabled: false).exists? + end + private def all_ancestors_allow_diff_preview_in_email? diff --git a/app/models/namespace_statistics.rb b/app/models/namespace_statistics.rb index 04ca05d85ff..a17ca2e2c1d 100644 --- a/app/models/namespace_statistics.rb +++ b/app/models/namespace_statistics.rb @@ -10,8 +10,8 @@ class NamespaceStatistics < ApplicationRecord # rubocop:disable Gitlab/Namespace scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) } before_save :update_storage_size - after_save :update_root_storage_statistics, if: :saved_change_to_storage_size? after_destroy :update_root_storage_statistics + after_save :update_root_storage_statistics, if: :saved_change_to_storage_size? delegate :group_namespace?, to: :namespace diff --git a/app/models/note.rb b/app/models/note.rb index 8e1f4979602..052df6142c5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -168,10 +168,10 @@ class Note < ApplicationRecord # Syncs `confidential` with `internal` as we rename the column. # https://gitlab.com/gitlab-org/gitlab/-/issues/367923 before_create :set_internal_flag + after_destroy :expire_etag_cache after_save :keep_around_commit, if: :for_project_noteable?, unless: -> { importing? || skip_keep_around_commits } after_save :expire_etag_cache, unless: :importing? after_save :touch_noteable, unless: :importing? - after_destroy :expire_etag_cache after_commit :notify_after_create, on: :create after_commit :notify_after_destroy, on: :destroy diff --git a/app/models/operations/feature_flags_client.rb b/app/models/operations/feature_flags_client.rb index e8c237abbc5..5a05d76254d 100644 --- a/app/models/operations/feature_flags_client.rb +++ b/app/models/operations/feature_flags_client.rb @@ -19,11 +19,11 @@ module Operations before_validation :ensure_token! - def self.find_for_project_and_token(project, token) - return unless project + def self.find_for_project_and_token(project_id, token) + return unless project_id return unless token - where(project_id: project).find_by_token(token) + where(project_id: project_id).find_by_token(token) end def self.update_last_feature_flag_updated_at!(project) diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 317db51f4ef..17c5415939c 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -149,6 +149,7 @@ class Packages::Package < ApplicationRecord end scope :preload_composer, -> { preload(:composer_metadatum) } scope :preload_npm_metadatum, -> { preload(:npm_metadatum) } + scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) } scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } diff --git a/app/models/packages/rpm/repository_file.rb b/app/models/packages/rpm/repository_file.rb index 4b5fa59c6ee..614ec9b3e56 100644 --- a/app/models/packages/rpm/repository_file.rb +++ b/app/models/packages/rpm/repository_file.rb @@ -8,6 +8,8 @@ module Packages include Packages::Installable INSTALLABLE_STATUSES = [:default].freeze + FILELISTS_FILENAME = 'filelists.xml' + FILELISTS_SIZE_LIMITATION = 20.megabytes enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 } @@ -20,6 +22,14 @@ module Packages mount_file_store_uploader Packages::Rpm::RepositoryFileUploader update_project_statistics project_statistics_name: :packages_size + + def self.has_oversized_filelists?(project_id:) + where( + project_id: project_id, + file_name: FILELISTS_FILENAME, + size: [FILELISTS_SIZE_LIMITATION..] + ).exists? + end end end end diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index c1056d4f6cb..cf0f0f9e92f 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -19,11 +19,13 @@ module Pages def access_control project.private_pages? end + strong_memoize_attr :access_control def https_only domain_https = domain ? domain.https? : true project.pages_https_only? && domain_https end + strong_memoize_attr :https_only def source return unless deployment&.file @@ -41,6 +43,7 @@ module Pages file_count: deployment.file_count } end + strong_memoize_attr :source def prefix if project.pages_group_root? @@ -49,6 +52,7 @@ module Pages project.full_path.delete_prefix(trim_prefix) + '/' end end + strong_memoize_attr :prefix private diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb index 119cc7fc166..fafbe449c8c 100644 --- a/app/models/pages/virtual_domain.rb +++ b/app/models/pages/virtual_domain.rb @@ -28,6 +28,7 @@ module Pages 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 end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 328c67a0711..4e3f4b0c328 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -17,6 +17,7 @@ class PagesDomain < ApplicationRecord has_many :acme_orders, class_name: "PagesDomainAcmeOrder" has_many :serverless_domain_clusters, class_name: 'Serverless::DomainCluster', inverse_of: :pages_domain + after_initialize :set_verification_code before_validation :clear_auto_ssl_failure, unless: :auto_ssl_enabled validates :domain, hostname: { allow_numeric_hostname: true } @@ -44,8 +45,6 @@ class PagesDomain < ApplicationRecord key: Settings.attr_encrypted_db_key_base, algorithm: 'aes-256-cbc' - after_initialize :set_verification_code - scope :for_project, ->(project) { where(project: project) } scope :enabled, -> { where('enabled_until >= ?', Time.current) } diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb index 4804f620a99..37bf080ae49 100644 --- a/app/models/performance_monitoring/prometheus_dashboard.rb +++ b/app/models/performance_monitoring/prometheus_dashboard.rb @@ -53,8 +53,6 @@ module PerformanceMonitoring # This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398 # implementation. For new existing logic was reused to faster deliver MVC def schema_validation_warnings - return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project) - self.class.from_json(reload_schema) [] rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => e @@ -65,11 +63,6 @@ module PerformanceMonitoring private - def run_custom_validation - Gitlab::Metrics::Dashboard::Validator - .errors(reload_schema, dashboard_path: path, project: environment&.project) - end - # dashboard finder methods are somehow limited, #find includes checking if # user is authorised to view selected dashboard, but modifies schema, which in some cases may # cause false positives returned from validation, and #find_raw does not authorise users diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 3126dba9d6d..887ef36cc17 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -18,6 +18,7 @@ class PersonalAccessToken < ApplicationRecord belongs_to :user + after_initialize :set_default_scopes, if: :persisted? before_save :ensure_token scope :active, -> { not_revoked.not_expired } @@ -41,8 +42,6 @@ class PersonalAccessToken < ApplicationRecord validates :scopes, presence: true validate :validate_scopes - after_initialize :set_default_scopes, if: :persisted? - def revoke! update!(revoked: true) end diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb index b0dd52c9657..d26778957d5 100644 --- a/app/models/postgresql/detached_partition.rb +++ b/app/models/postgresql/detached_partition.rb @@ -7,5 +7,9 @@ module Postgresql def fully_qualified_table_name "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}" end + + def table_schema + Gitlab::Database::GitlabSchema.table_schema(table_name) + end end end diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb index 06e3034e56a..4156c672518 100644 --- a/app/models/programming_language.rb +++ b/app/models/programming_language.rb @@ -10,4 +10,22 @@ class ProgrammingLanguage < ApplicationRecord sanitized_names = names.map(&method(:sanitize_sql_like)) where(arel_table[:name].matches_any(sanitized_names)) end + + def self.most_popular(limit = 25) + sql = <<~SQL + SELECT + mcv + FROM + pg_stats + CROSS JOIN LATERAL + unnest(most_common_vals::text::int[]) mt(mcv) + WHERE + tablename = 'repository_languages' and attname='programming_language_id' + LIMIT + $1 + SQL + ids = connection.exec_query(sql, 'SQL', [limit]).rows.flatten + + where(id: ids).order(:name) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 0c4f76fb2b9..73dbb55a07b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -89,68 +89,56 @@ class Project < ApplicationRecord cache_markdown_field :description, pipeline: :description - default_value_for :packages_enabled, true - default_value_for :archived, false - default_value_for :resolve_outdated_diff_discussions, false - default_value_for(:repository_storage) do - Repository.pick_storage_shard - end + attribute :packages_enabled, default: true + attribute :archived, default: false + attribute :resolve_outdated_diff_discussions, default: false + attribute :repository_storage, default: -> { Repository.pick_storage_shard } + attribute :shared_runners_enabled, default: -> { Gitlab::CurrentSettings.shared_runners_enabled } + attribute :only_allow_merge_if_all_discussions_are_resolved, default: false + attribute :remove_source_branch_after_merge, default: true + attribute :autoclose_referenced_issues, default: true + attribute :ci_config_path, default: -> { Gitlab::CurrentSettings.default_ci_config_path } - default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled } default_value_for :issues_enabled, gitlab_config_features.issues default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets - default_value_for :only_allow_merge_if_all_discussions_are_resolved, false - default_value_for :remove_source_branch_after_merge, true - default_value_for :autoclose_referenced_issues, true - default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path } add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required }, prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + # Storage specific hooks + after_initialize :use_hashed_storage before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } - before_save :ensure_runners_token before_validation :ensure_project_namespace_in_sync - before_validation :set_package_registry_access_level, if: :packages_enabled_changed? before_validation :remove_leading_spaces_on_name - - after_save :update_project_statistics, if: :saved_change_to_namespace_id? - - after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? } - - after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? } - - after_save :save_topics - - after_save :reload_project_namespace_details + after_validation :check_pending_delete + before_save :ensure_runners_token after_create -> { create_or_load_association(:project_feature) } - after_create -> { create_or_load_association(:ci_cd_settings) } - after_create -> { create_or_load_association(:container_expiration_policy) } - after_create -> { create_or_load_association(:pages_metadatum) } - after_create :set_timestamps_for_create + after_create :check_repository_absence! after_update :update_forks_visibility_level - before_destroy :remove_private_deploy_keys + after_destroy :remove_exports + after_save :update_project_statistics, if: :saved_change_to_namespace_id? - use_fast_destroy :build_trace_chunks + after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? } - after_destroy :remove_exports + after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? } - after_validation :check_pending_delete + after_save :save_topics - # Storage specific hooks - after_initialize :use_hashed_storage - after_create :check_repository_absence! + after_save :reload_project_namespace_details + + use_fast_destroy :build_trace_chunks has_many :project_topics, -> { order(:id) }, class_name: 'Projects::ProjectTopic' has_many :topics, through: :project_topics, class_name: 'Projects::Topic' @@ -196,7 +184,6 @@ class Project < ApplicationRecord has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush' has_one :ewm_integration, class_name: 'Integrations::Ewm' has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki' - has_one :flowdock_integration, class_name: 'Integrations::Flowdock' has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat' has_one :harbor_integration, class_name: 'Integrations::Harbor' has_one :irker_integration, class_name: 'Integrations::Irker' @@ -231,6 +218,17 @@ class Project < ApplicationRecord has_one :fork_network_member has_one :fork_network, through: :fork_network_member has_one :forked_from_project, through: :fork_network_member + + # Projects with a very large number of notes may time out destroying them + # through the foreign key. Additionally, the deprecated attachment uploader + # for notes requires us to use dependent: :destroy to avoid orphaning uploaded + # files. + # + # https://gitlab.com/gitlab-org/gitlab/-/issues/207222 + # Order of this association is important for project deletion. + # has_many :notes` should be the first association among all `has_many` associations. + has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id' has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project has_many :fork_network_projects, through: :fork_network, source: :projects @@ -259,25 +257,30 @@ class Project < ApplicationRecord has_one :service_desk_setting, class_name: 'ServiceDeskSetting' # Merge requests for target project should be removed with it - has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project + has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' - has_many :issues + has_many :issues, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus' has_many :incident_management_timeline_event_tags, inverse_of: :project, class_name: 'IncidentManagement::TimelineEventTag' - has_many :labels, class_name: 'ProjectLabel' - has_many :integrations + has_many :labels, class_name: 'ProjectLabel', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :events has_many :milestones - # Projects with a very large number of notes may time out destroying them - # through the foreign key. Additionally, the deprecated attachment uploader - # for notes requires us to use dependent: :destroy to avoid orphaning uploaded - # files. - # - # https://gitlab.com/gitlab-org/gitlab/-/issues/207222 - has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - + has_many :integrations + has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration' + has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration' + has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration' + has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration' + has_many :deployment_hooks_integrations, -> { deployment_hooks }, class_name: 'Integration' + has_many :issue_hooks_integrations, -> { issue_hooks }, class_name: 'Integration' + has_many :job_hooks_integrations, -> { job_hooks }, class_name: 'Integration' + has_many :merge_request_hooks_integrations, -> { merge_request_hooks }, class_name: 'Integration' + has_many :note_hooks_integrations, -> { note_hooks }, class_name: 'Integration' + has_many :pipeline_hooks_integrations, -> { pipeline_hooks }, class_name: 'Integration' + has_many :push_hooks_integrations, -> { push_hooks }, class_name: 'Integration' + has_many :tag_push_hooks_integrations, -> { tag_push_hooks }, class_name: 'Integration' + has_many :wiki_page_hooks_integrations, -> { wiki_page_hooks }, class_name: 'Integration' has_many :snippets, class_name: 'ProjectSnippet' has_many :hooks, class_name: 'ProjectHook' has_many :protected_branches @@ -380,7 +383,7 @@ class Project < ApplicationRecord has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true has_many :custom_attributes, class_name: 'ProjectCustomAttribute' - has_many :project_badges, class_name: 'ProjectBadge' + has_many :project_badges, class_name: 'ProjectBadge', inverse_of: :project has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :remote_mirrors, inverse_of: :project @@ -650,6 +653,11 @@ class Project < ApplicationRecord .where(repository_languages: { programming_language_id: lang_id_query }) end + scope :with_programming_language_id, ->(language_id) do + joins(:repository_languages) + .where(repository_languages: { programming_language_id: language_id }) + end + scope :service_desk_enabled, -> { where(service_desk_enabled: true) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) } @@ -742,6 +750,29 @@ class Project < ApplicationRecord end end + # Defines instance methods: + # + # - only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: false) + # - allow_merge_on_skipped_pipeline?(inherit_group_setting: false) + # - only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: false) + # - only_allow_merge_if_pipeline_succeeds_locked? + # - allow_merge_on_skipped_pipeline_locked? + # - only_allow_merge_if_all_discussions_are_resolved_locked? + def self.cascading_with_parent_namespace(attribute) + # method overriden in EE + define_method("#{attribute}?") do |inherit_group_setting: false| + self.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend + end + + define_method("#{attribute}_locked?") do + false + end + end + + cascading_with_parent_namespace :only_allow_merge_if_pipeline_succeeds + cascading_with_parent_namespace :allow_merge_on_skipped_pipeline + cascading_with_parent_namespace :only_allow_merge_if_all_discussions_are_resolved + def self.with_feature_available_for_user(feature, user) with_project_feature.merge(ProjectFeature.with_feature_available_for_user(feature, user)) end @@ -1691,8 +1722,14 @@ class Project < ApplicationRecord def execute_integrations(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope run_after_commit_or_now do - integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend - integration.async_execute(data) + if use_integration_relations? + association("#{hooks_scope}_integrations").reader.each do |integration| + integration.async_execute(data) + end + else + integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend + integration.async_execute(data) + end end end end @@ -2301,6 +2338,7 @@ class Project < ApplicationRecord .append(key: 'CI_PROJECT_PATH', value: full_path) .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug) .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path) + .append(key: 'CI_PROJECT_NAMESPACE_ID', value: namespace.id.to_s) .append(key: 'CI_PROJECT_ROOT_NAMESPACE', value: namespace.root_ancestor.path) .append(key: 'CI_PROJECT_URL', value: web_url) .append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level)) @@ -2700,7 +2738,13 @@ class Project < ApplicationRecord def access_request_approvers_to_be_notified access_request_approvers = members.owners_and_maintainers - access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) + recipients = access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) + + if recipients.blank? + recipients = group.access_request_approvers_to_be_notified + end + + recipients end def pages_lookup_path(trim_prefix: nil, domain: nil) @@ -2994,6 +3038,10 @@ class Project < ApplicationRecord group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self) end + def work_items_mvc_feature_flag_enabled? + group&.work_items_mvc_feature_flag_enabled? || Feature.enabled?(:work_items_mvc) + end + def work_items_mvc_2_feature_flag_enabled? group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2) end @@ -3321,6 +3369,12 @@ class Project < ApplicationRecord ProjectFeature::PRIVATE end end + + def use_integration_relations? + strong_memoize(:use_integration_relations) do + Feature.enabled?(:cache_project_integrations, self) + end + end end Project.prepend_mod_with('Project') diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb index decc71ee193..d26ce5465cd 100644 --- a/app/models/project_export_job.rb +++ b/app/models/project_export_job.rb @@ -1,11 +1,24 @@ # frozen_string_literal: true class ProjectExportJob < ApplicationRecord + include EachBatch + + EXPIRES_IN = 7.days + belongs_to :project has_many :relation_exports, class_name: 'Projects::ImportExport::RelationExport' validates :project, :jid, :status, presence: true + STATUS = { + queued: 0, + started: 1, + finished: 2, + failed: 3 + }.freeze + + scope :prunable, -> { where("updated_at < ?", EXPIRES_IN.ago) } + state_machine :status, initial: :queued do event :start do transition [:queued] => :started @@ -19,9 +32,17 @@ class ProjectExportJob < ApplicationRecord transition [:queued, :started] => :failed end - state :queued, value: 0 - state :started, value: 1 - state :finished, value: 2 - state :failed, value: 3 + state :queued, value: STATUS[:queued] + state :started, value: STATUS[:started] + state :finished, value: STATUS[:finished] + state :failed, value: STATUS[:failed] + end + + class << self + def prune_expired_jobs + prunable.each_batch do |relation| # rubocop:disable Style/SymbolProc + relation.delete_all + end + end end end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 0570be85ad1..506f6305791 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -11,21 +11,21 @@ class ProjectStatistics < ApplicationRecord attribute :snippets_size, default: 0 counter_attribute :build_artifacts_size + counter_attribute :packages_size - counter_attribute_after_flush do |project_statistic| - project_statistic.refresh_storage_size! + counter_attribute_after_commit do |project_statistics| + project_statistics.refresh_storage_size! - Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id) + Namespaces::ScheduleAggregationWorker.perform_async(project_statistics.namespace_id) end before_save :update_storage_size COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size, :container_registry_size].freeze - INCREMENTABLE_COLUMNS = { - packages_size: %i[storage_size], - pipeline_artifacts_size: %i[storage_size], - snippets_size: %i[storage_size] - }.freeze + INCREMENTABLE_COLUMNS = [ + :pipeline_artifacts_size, + :snippets_size + ].freeze NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size, :uploads_size, :container_registry_size].freeze STORAGE_SIZE_COMPONENTS = [ :repository_size, @@ -120,35 +120,27 @@ class ProjectStatistics < ApplicationRecord # we have to update the storage_size separately. # # For counter attributes, storage_size will be refreshed after the counter is flushed, - # through counter_attribute_after_flush + # through counter_attribute_after_commit # # For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS def self.increment_statistic(project, key, amount) - raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key) - return if amount == 0 - project.statistics.try do |project_statistics| - if counter_attribute_enabled?(key) - project_statistics.delayed_increment_counter(key, amount) - else - project_statistics.legacy_increment_statistic(key, amount) - end + project_statistics.increment_statistic(key, amount) end end - def self.incrementable_attribute?(key) - INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key) - end - - def legacy_increment_statistic(key, amount) - increment_columns!(key, amount) + def increment_statistic(key, amount) + raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key) - Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker - project.namespace_id) + increment_counter(key, amount) end private + def incrementable_attribute?(key) + INCREMENTABLE_COLUMNS.include?(key) || counter_attribute_enabled?(key) + end + def storage_size_components STORAGE_SIZE_COMPONENTS end @@ -157,16 +149,6 @@ class ProjectStatistics < ApplicationRecord storage_size_components.map { |component| "COALESCE (#{component}, 0)" }.join(' + ').freeze end - def increment_columns!(key, amount) - increments = { key => amount } - additional = INCREMENTABLE_COLUMNS.fetch(key, []) - additional.each do |column| - increments[column] = amount - end - - update_counters_with_lease(increments) - end - def schedule_namespace_aggregation_worker run_after_commit do Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) diff --git a/app/models/projects/forks/divergence_counts.rb b/app/models/projects/forks/divergence_counts.rb new file mode 100644 index 00000000000..7d630b00083 --- /dev/null +++ b/app/models/projects/forks/divergence_counts.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Projects + module Forks + # Class for calculating the divergence of a fork with the source project + class DivergenceCounts + LATEST_COMMITS_COUNT = 10 + EXPIRATION_TIME = 8.hours + + def initialize(project, ref) + @project = project + @fork_repo = project.repository + @source_repo = project.fork_source.repository + @ref = ref + end + + def counts + ahead, behind = divergence_counts + + { ahead: ahead, behind: behind } + end + + private + + attr_reader :project, :fork_repo, :source_repo, :ref + + def cache_key + @cache_key ||= ['project_forks', project.id, ref, 'divergence_counts'] + end + + def divergence_counts + fork_sha = fork_repo.commit(ref).sha + source_sha = source_repo.commit.sha + + cached_source_sha, cached_fork_sha, counts = Rails.cache.read(cache_key) + return counts if cached_source_sha == source_sha && cached_fork_sha == fork_sha + + counts = calculate_divergence_counts(fork_sha, source_sha) + + Rails.cache.write(cache_key, [source_sha, fork_sha, counts], expires_in: EXPIRATION_TIME) + + counts + end + + def calculate_divergence_counts(fork_sha, source_sha) + # If the upstream latest commit exists in the fork repo, then + # it's possible to calculate divergence counts within the fork repository. + return fork_repo.diverging_commit_count(fork_sha, source_sha) if fork_repo.commit(source_sha) + + # Otherwise, we need to find a commit that exists both in the fork and upstream + # in order to use this commit as a base for calculating divergence counts. + # Considering the fact that a user usually creates a fork to contribute to the upstream, + # it is expected that they have a limited number of commits ahead of upstream. + # Let's take the latest N commits and check their existence upstream. + last_commits_shas = fork_repo.commits(ref, limit: LATEST_COMMITS_COUNT).map(&:sha) + existence_hash = source_repo.check_objects_exist(last_commits_shas) + first_matched_commit_sha = last_commits_shas.find { |sha| existence_hash[sha] } + + # If we can't find such a commit, we return early and tell the user that the branches + # have diverged and action is required. + return unless first_matched_commit_sha + + # Otherwise, we use upstream to calculate divergence counts from the matched commit + ahead, behind = source_repo.diverging_commit_count(first_matched_commit_sha, source_sha) + # And add the number of commits a fork is ahead of the first matched commit + ahead += last_commits_shas.index(first_matched_commit_sha) + + [ahead, behind] + end + end + end +end diff --git a/app/models/projects/import_export/relation_export_upload.rb b/app/models/projects/import_export/relation_export_upload.rb index 965dc39d19f..12cfb3415d8 100644 --- a/app/models/projects/import_export/relation_export_upload.rb +++ b/app/models/projects/import_export/relation_export_upload.rb @@ -4,7 +4,6 @@ module Projects module ImportExport class RelationExportUpload < ApplicationRecord include WithUploads - include ObjectStorage::BackgroundMove self.table_name = 'project_relation_export_uploads' diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb index 9080f3d9de1..59440947d71 100644 --- a/app/models/prometheus_alert.rb +++ b/app/models/prometheus_alert.rb @@ -20,8 +20,8 @@ class PrometheusAlert < ApplicationRecord has_many :related_issues, through: :prometheus_alert_events has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :prometheus_alert - after_save :clear_prometheus_adapter_cache! after_destroy :clear_prometheus_adapter_cache! + after_save :clear_prometheus_adapter_cache! validates :environment, :project, :prometheus_metric, :threshold, :operator, presence: true validates :runbook_url, length: { maximum: 255 }, allow_blank: true, diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 80967c1b072..c59ef4cd80b 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -14,10 +14,12 @@ class ProtectedBranch < ApplicationRecord scope :allowing_force_push, -> { where(allow_force_push: true) } - scope :get_ids_by_name, -> (name) { where(name: name).pluck(:id) } - protected_ref_access_levels :merge, :push + def self.get_ids_by_name(name) + where(name: name).pluck(:id) + end + def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) # Maintainers, owners and admins are allowed to create the default branch diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index f8d500e106b..b830cf313af 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -20,12 +20,11 @@ class RemoteMirror < ApplicationRecord belongs_to :project, inverse_of: :remote_mirrors - validates :url, presence: true, public_url: { schemes: %w(ssh git http https), allow_blank: true, enforce_user: true } - - after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available } - after_update :reset_fields, if: :saved_change_to_mirror_url? + validates :url, presence: true, public_url: { schemes: Project::VALID_MIRROR_PROTOCOLS, allow_blank: true, enforce_user: true } before_validation :store_credentials + after_update :reset_fields, if: :saved_change_to_mirror_url? + after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available } scope :enabled, -> { where(enabled: true) } scope :started, -> { with_update_status(:started) } diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index a1753df9294..a1426540cf5 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -14,8 +14,8 @@ class ResourceLabelEvent < ResourceEvent validates :label, presence: { unless: :importing? }, on: :create validate :exactly_one_issuable, unless: :importing? - after_save :expire_etag_cache after_destroy :expire_etag_cache + after_save :expire_etag_cache enum action: { add: 1, diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb index 6dd7415d928..738f18ca5e3 100644 --- a/app/models/service_desk_setting.rb +++ b/app/models/service_desk_setting.rb @@ -53,7 +53,7 @@ class ServiceDeskSetting < ApplicationRecord def projects_with_same_slug_and_key_exists? return false unless project_key - settings = self.class.with_project_key(project_key).preload(:project) + settings = self.class.with_project_key(project_key).where.not(project_id: project_id).preload(:project) project_slug = self.project.full_path_slug settings.any? do |setting| diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb index 6fb6f0ef713..44bff0e1e5b 100644 --- a/app/models/snippet_statistics.rb +++ b/app/models/snippet_statistics.rb @@ -12,8 +12,8 @@ class SnippetStatistics < ApplicationRecord delegate :repository, :project, :project_id, to: :snippet - after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics? after_destroy :update_author_root_storage_statistics, unless: :project_snippet? + after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics? def update_commit_count self.commit_count = repository.commit_count diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb index dea7165af9f..a60c0d2f3bc 100644 --- a/app/models/synthetic_note.rb +++ b/app/models/synthetic_note.rb @@ -10,6 +10,7 @@ class SyntheticNote < Note system: true, author: event.user, created_at: event.created_at, + updated_at: event.created_at, discussion_id: event.discussion_id, noteable: resource, event: event, diff --git a/app/models/todo.rb b/app/models/todo.rb index f2fa0df852a..32ec4accb4b 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -19,6 +19,7 @@ class Todo < ApplicationRecord DIRECTLY_ADDRESSED = 7 MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature REVIEW_REQUESTED = 9 + MEMBER_ACCESS_REQUESTED = 10 ACTION_NAMES = { ASSIGNED => :assigned, @@ -29,10 +30,11 @@ class Todo < ApplicationRecord APPROVAL_REQUIRED => :approval_required, UNMERGEABLE => :unmergeable, DIRECTLY_ADDRESSED => :directly_addressed, - MERGE_TRAIN_REMOVED => :merge_train_removed + MERGE_TRAIN_REMOVED => :merge_train_removed, + MEMBER_ACCESS_REQUESTED => :member_access_requested }.freeze - ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED].freeze + ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze belongs_to :author, class_name: "User" belongs_to :note @@ -198,6 +200,16 @@ class Todo < ApplicationRecord action == MERGE_TRAIN_REMOVED end + def member_access_requested? + action == MEMBER_ACCESS_REQUESTED + end + + def access_request_url + return "" unless self.target_type == 'Namespace' + + Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests') + end + def done? state == 'done' end @@ -209,6 +221,8 @@ class Todo < ApplicationRecord def body if note.present? note.note + elsif member_access_requested? + target.full_path else target.title end @@ -246,6 +260,8 @@ class Todo < ApplicationRecord def target_reference if for_commit? target.reference_link_text + elsif member_access_requested? + target.full_path else target.to_reference end diff --git a/app/models/upload.rb b/app/models/upload.rb index ac7ebb31abc..a4fbc703146 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -16,14 +16,13 @@ class Upload < ApplicationRecord scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) } scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) } - before_save :calculate_checksum!, if: :foreground_checksummable? - after_commit :schedule_checksum, if: :needs_checksum? - - after_commit :update_project_statistics, on: [:create, :destroy], if: :project? - + before_save :calculate_checksum!, if: :foreground_checksummable? # as the FileUploader is not mounted, the default CarrierWave ActiveRecord # hooks are not executed and the file will not be deleted after_destroy :delete_file!, if: -> { uploader_class <= FileUploader } + after_commit :schedule_checksum, if: :needs_checksum? + + after_commit :update_project_statistics, on: [:create, :destroy], if: :project? class << self def inner_join_local_uploads_projects diff --git a/app/models/user.rb b/app/models/user.rb index b4b8a7ef7ad..ba3f7922c9c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,6 +46,8 @@ class User < ApplicationRecord MAX_USERNAME_LENGTH = 255 MIN_USERNAME_LENGTH = 2 + MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT = 100 + SECONDARY_EMAIL_ATTRIBUTES = [ :commit_email, :notification_email, @@ -58,16 +60,16 @@ class User < ApplicationRecord add_authentication_token_field :feed_token add_authentication_token_field :static_object_token, encrypted: :optional - default_value_for :admin, false - default_value_for(:external) { Gitlab::CurrentSettings.user_default_external } - default_value_for(:can_create_group) { Gitlab::CurrentSettings.can_create_group } - default_value_for :can_create_team, false - default_value_for :hide_no_ssh_key, false - default_value_for :hide_no_password, false - default_value_for :project_view, :files - default_value_for :notified_of_own_activity, false - default_value_for :preferred_language, I18n.default_locale - default_value_for :theme_id, gitlab_config.default_theme + attribute :admin, default: false + attribute :external, default: -> { Gitlab::CurrentSettings.user_default_external } + attribute :can_create_group, default: -> { Gitlab::CurrentSettings.can_create_group } + attribute :can_create_team, default: false + attribute :hide_no_ssh_key, default: false + attribute :hide_no_password, default: false + attribute :project_view, default: :files + attribute :notified_of_own_activity, default: false + attribute :preferred_language, default: -> { I18n.default_locale } + attribute :theme_id, default: -> { gitlab_config.default_theme } attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, @@ -298,16 +300,17 @@ class User < ApplicationRecord validates :website_url, allow_blank: true, url: true, if: :website_url_changed? + after_initialize :set_projects_limit before_validation :sanitize_attrs + before_validation :ensure_namespace_correct + after_validation :set_username_errors before_save :default_private_profile_to_false before_save :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } - before_validation :ensure_namespace_correct before_save :ensure_namespace_correct # in case validation is skipped before_save :ensure_user_detail_assigned - after_validation :set_username_errors after_update :username_changed_hook, if: :saved_change_to_username? after_destroy :post_destroy_hook after_destroy :remove_key_cache @@ -328,8 +331,6 @@ class User < ApplicationRecord update_invalid_gpg_signatures if previous_changes.key?('email') end - after_initialize :set_projects_limit - # User's Layout preference enum layout: { fixed: 0, fluid: 1 } @@ -360,6 +361,7 @@ class User < ApplicationRecord :diffs_deletion_color, :diffs_deletion_color=, :diffs_addition_color, :diffs_addition_color=, :use_legacy_web_ide, :use_legacy_web_ide=, + :use_new_navigation, :use_new_navigation=, to: :user_preference delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -376,6 +378,14 @@ class User < ApplicationRecord accepts_nested_attributes_for :credit_card_validation, update_only: true, allow_destroy: true state_machine :state, initial: :active do + # state_machine uses this method at class loading time to fetch the default + # value for the `state` column but in doing so it also evaluates all other + # columns default values which could trigger the recursive generation of + # ApplicationSetting records. We're setting it to `nil` here because we + # don't have a database default for the `state` column. + # + def owner_class_attribute_default; end + event :block do transition active: :blocked transition deactivated: :blocked @@ -811,7 +821,7 @@ class User < ApplicationRecord # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) - find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id)) + find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').auth.where(id: key_id)) end def find_by_full_path(path, follow_redirects: false) @@ -896,6 +906,18 @@ class User < ApplicationRecord 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? @@ -1759,12 +1781,10 @@ class User < ApplicationRecord end def ci_owned_runners - @ci_owned_runners ||= begin - Ci::Runner + @ci_owned_runners ||= Ci::Runner .from_union([ci_owned_project_runners_from_project_members, ci_owned_project_runners_from_group_members, ci_owned_group_runners]) - end end def owns_runner?(runner) @@ -1773,7 +1793,11 @@ class User < ApplicationRecord def notification_email_for(notification_group) # Return group-specific email address if present, otherwise return global notification email address - notification_group&.notification_email_for(self) || notification_email_or_default + group_email = if notification_group && notification_group.respond_to?(:notification_email_for) + notification_group.notification_email_for(self) + end + + group_email || notification_email_or_default end def notification_settings_for(source, inherit: false) @@ -1866,6 +1890,7 @@ class User < ApplicationRecord def invalidate_issue_cache_counts Rails.cache.delete(['users', id, 'assigned_open_issues_count']) + Rails.cache.delete(['users', id, 'max_assigned_open_issues_count']) if Feature.enabled?(:limit_assigned_issues_count) end def invalidate_merge_request_cache_counts @@ -2323,9 +2348,7 @@ class User < ApplicationRecord end def check_password_weakness - if Feature.enabled?(:block_weak_passwords) && - password.present? && - Security::WeakPasswords.weak_for_user?(password, self) + if password.present? && Security::WeakPasswords.weak_for_user?(password, self) errors.add(:password, _('must not contain commonly used combinations of words and letters')) end end diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index 2e662faea6a..0570bc2f395 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -19,7 +19,7 @@ class UserDetail < ApplicationRecord validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true - validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true + validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed? before_validation :sanitize_attrs before_save :prevent_nil_bio diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index c6ebd550daf..bc2c6b526b8 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -26,10 +26,10 @@ class UserPreference < ApplicationRecord ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' - default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false - default_value_for :time_display_relative, value: true, allows_nil: false - default_value_for :time_format_in_24h, value: false, allows_nil: false - default_value_for :render_whitespace_in_code, value: false, allows_nil: false + attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT } + attribute :time_display_relative, default: true + attribute :time_format_in_24h, default: false + attribute :render_whitespace_in_code, default: false class << self def notes_filters @@ -59,6 +59,67 @@ class UserPreference < ApplicationRecord self[notes_filter_field_for(resource)] end + def tab_width + read_attribute(:tab_width) || self.class.column_defaults['tab_width'] + end + + def tab_width=(value) + if value.nil? + default = self.class.column_defaults['tab_width'] + super(default) + else + super(value) + end + end + + def time_display_relative + value = read_attribute(:time_display_relative) + return value unless value.nil? + + self.class.column_defaults['time_display_relative'] + end + + def time_display_relative=(value) + if value.nil? + default = self.class.column_defaults['time_display_relative'] + super(default) + else + super(value) + end + end + + def time_format_in_24h + value = read_attribute(:time_format_in_24h) + return value unless value.nil? + + self.class.column_defaults['time_format_in_24h'] + end + + def time_format_in_24h=(value) + if value.nil? + default = self.class.column_defaults['time_format_in_24h'] + super(default) + else + super(value) + end + end + + def render_whitespace_in_code + value = read_attribute(:render_whitespace_in_code) + return value unless value.nil? + + self.class.column_defaults['render_whitespace_in_code'] + end + + def render_whitespace_in_code=(value) + if value.nil? + default = self.class.column_defaults['render_whitespace_in_code'] + super(default) + else + super(value) + end + end + private def notes_filter_field_for(resource) diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index b037d07658d..3f9353214ee 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -63,7 +63,9 @@ 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 + artifacts_management_page_feedback_banner: 62, + vscode_web_ide: 63, + vscode_web_ide_callout: 64 } validates :feature_name, diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb index 3e3e424e9c9..2552407fa4c 100644 --- a/app/models/users/group_callout.rb +++ b/app/models/users/group_callout.rb @@ -23,7 +23,8 @@ module Users namespace_storage_limit_banner_alert_threshold: 12, # EE-only namespace_storage_limit_banner_error_threshold: 13, # EE-only usage_quota_trial_alert: 14, # EE-only - preview_usage_quota_free_plan_alert: 15 # EE-only + preview_usage_quota_free_plan_alert: 15, # EE-only + enforcement_at_limit_alert: 16 # EE-only } validates :group, presence: true diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb index f6123c01fd0..b9e4e908ddd 100644 --- a/app/models/users/phone_number_validation.rb +++ b/app/models/users/phone_number_validation.rb @@ -31,11 +31,17 @@ module Users validates :telesign_reference_xid, length: { maximum: 255 } + scope :for_user, -> (user_id) { where(user_id: user_id) } + def self.related_to_banned_user?(international_dial_code, phone_number) joins(:banned_user).where( international_dial_code: international_dial_code, phone_number: phone_number ).exists? end + + def validated? + validated_at.present? + end end end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index ed6f9d161a6..0810c520f7e 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -38,6 +38,18 @@ class WorkItem < Issue end end + def ancestors + hierarchy.ancestors(hierarchy_order: :asc) + end + + def same_type_base_and_ancestors + hierarchy(same_type: true).base_and_ancestors(hierarchy_order: :asc) + end + + def same_type_descendants_depth + hierarchy(same_type: true).max_descendants_depth.to_i + end + private override :parent_link_confidentiality @@ -56,6 +68,13 @@ class WorkItem < Issue Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author) end + + def hierarchy(options = {}) + base = self.class.where(id: id) + base = base.where(work_item_type_id: work_item_type_id) if options[:same_type] + + ::Gitlab::WorkItems::WorkItemHierarchy.new(base, options: options) + end end WorkItem.prepend_mod diff --git a/app/models/work_items/hierarchy_restriction.rb b/app/models/work_items/hierarchy_restriction.rb new file mode 100644 index 00000000000..a253447a8db --- /dev/null +++ b/app/models/work_items/hierarchy_restriction.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module WorkItems + class HierarchyRestriction < ApplicationRecord + self.table_name = 'work_item_hierarchy_restrictions' + + belongs_to :parent_type, class_name: 'WorkItems::Type' + belongs_to :child_type, class_name: 'WorkItems::Type' + + validates :parent_type, presence: true + validates :child_type, presence: true + validates :child_type, uniqueness: { scope: :parent_type_id } + end +end diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb index 13d6db3e08e..33857fb08c2 100644 --- a/app/models/work_items/parent_link.rb +++ b/app/models/work_items/parent_link.rb @@ -12,12 +12,14 @@ module WorkItems validates :work_item_parent, presence: true validates :work_item, presence: true, uniqueness: true - validate :validate_child_type - validate :validate_parent_type + validate :validate_hierarchy_restrictions + validate :validate_cyclic_reference validate :validate_same_project validate :validate_max_children validate :validate_confidentiality + scope :for_parents, ->(parent_ids) { where(work_item_parent_id: parent_ids) } + class << self def has_public_children?(parent_id) joins(:work_item).where(work_item_parent_id: parent_id, 'issues.confidential': false).exists? @@ -33,27 +35,6 @@ module WorkItems private - def validate_child_type - return unless work_item - - unless work_item.task? - errors.add :work_item, _('only Task can be assigned as a child in hierarchy.') - end - end - - def validate_parent_type - return unless work_item_parent - - base_type = work_item_parent.work_item_type.base_type.to_sym - unless PARENT_TYPES.include?(base_type) - parent_names = WorkItems::Type::BASE_TYPES.slice(*WorkItems::ParentLink::PARENT_TYPES) - .values.map { |type| type[:name] } - - errors.add :work_item_parent, _('only %{parent_types} can be parent of Task.') % - { parent_types: parent_names.to_sentence } - end - end - def validate_same_project return if work_item.nil? || work_item_parent.nil? @@ -79,5 +60,40 @@ module WorkItems "parent. Make the work item confidential and try again.") end end + + def validate_hierarchy_restrictions + return unless work_item && work_item_parent + + restriction = ::WorkItems::HierarchyRestriction + .find_by_parent_type_id_and_child_type_id(work_item_parent.work_item_type_id, work_item.work_item_type_id) + + if restriction.nil? + errors.add :work_item, _('is not allowed to add this type of parent') + return + end + + validate_depth(restriction.maximum_depth) + end + + def validate_depth(depth) + return unless depth + return if work_item.work_item_type_id != work_item_parent.work_item_type_id + + if work_item_parent.same_type_base_and_ancestors.count + work_item.same_type_descendants_depth > depth + errors.add :work_item, _('reached maximum depth') + end + end + + def validate_cyclic_reference + return unless work_item_parent&.id && work_item&.id + + if work_item.id == work_item_parent.id + errors.add :work_item, _('is not allowed to point to itself') + end + + if work_item_parent.ancestors.detect { |ancestor| work_item.id == ancestor.id } + errors.add :work_item, _('is already present in ancestors') + end + end end end diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index dc30899d24f..e1f6a13f7a7 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -10,31 +10,86 @@ module WorkItems include CacheMarkdownField + # type name is used in restrictions DB seeder to assure restrictions for + # default types are pre-filled + TYPE_NAMES = { + issue: 'Issue', + incident: 'Incident', + test_case: 'Test Case', + requirement: 'Requirement', + task: 'Task', + objective: 'Objective', + key_result: 'Key Result' + }.freeze + # Base types need to exist on the DB on app startup # This constant is used by the DB seeder # TODO - where to add new icon names created? BASE_TYPES = { - issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 }, - incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 }, - test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only - requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only - task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }, - objective: { name: 'Objective', icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only - key_result: { name: 'Key Result', icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only + issue: { name: TYPE_NAMES[:issue], icon_name: 'issue-type-issue', enum_value: 0 }, + incident: { name: TYPE_NAMES[:incident], icon_name: 'issue-type-incident', enum_value: 1 }, + test_case: { name: TYPE_NAMES[:test_case], icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only + requirement: { name: TYPE_NAMES[:requirement], icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only + task: { name: TYPE_NAMES[:task], icon_name: 'issue-type-task', enum_value: 4 }, + objective: { name: TYPE_NAMES[:objective], icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only + key_result: { name: TYPE_NAMES[:key_result], icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only }.freeze WIDGETS_FOR_TYPE = { - issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate, - Widgets::Milestone], - incident: [Widgets::Description, Widgets::Hierarchy], - test_case: [Widgets::Description], - requirement: [Widgets::Description], - task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate, - Widgets::Milestone], - objective: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::Milestone], - key_result: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::StartAndDueDate] + issue: [ + Widgets::Assignees, + Widgets::Labels, + Widgets::Description, + Widgets::Hierarchy, + Widgets::StartAndDueDate, + Widgets::Milestone, + Widgets::Notes + ], + incident: [ + Widgets::Description, + Widgets::Hierarchy, + Widgets::Notes + ], + test_case: [ + Widgets::Description, + Widgets::Notes + ], + requirement: [ + Widgets::Description, + Widgets::Notes + ], + task: [ + Widgets::Assignees, + Widgets::Labels, + Widgets::Description, + Widgets::Hierarchy, + Widgets::StartAndDueDate, + Widgets::Milestone, + Widgets::Notes + ], + objective: [ + Widgets::Assignees, + Widgets::Labels, + Widgets::Description, + Widgets::Hierarchy, + Widgets::Milestone, + Widgets::Notes + ], + key_result: [ + Widgets::Assignees, + Widgets::Labels, + Widgets::Description, + Widgets::Hierarchy, + Widgets::StartAndDueDate, + Widgets::Notes + ] }.freeze + # A list of types user can change between - both original and new + # type must be included in this list. This is needed for legacy issues + # 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].freeze cache_markdown_field :description, pipeline: :single_line @@ -66,6 +121,7 @@ module WorkItems return found_type if found_type Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types + Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions find_by(namespace_id: nil, base_type: type) end diff --git a/app/models/work_items/widgets/notes.rb b/app/models/work_items/widgets/notes.rb new file mode 100644 index 00000000000..bde94ea8f43 --- /dev/null +++ b/app/models/work_items/widgets/notes.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module WorkItems + module Widgets + class Notes < Base + delegate :notes, to: :work_item + delegate_missing_to :work_item + + def declarative_policy_delegate + work_item + end + end + end +end |