diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /app/models | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/models')
107 files changed, 798 insertions, 576 deletions
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb index 1da8973ff21..3e6ed86d534 100644 --- a/app/models/analytics/cycle_analytics/issue_stage_event.rb +++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb @@ -3,9 +3,14 @@ module Analytics module CycleAnalytics class IssueStageEvent < ApplicationRecord + include StageEventModel extend SuppressCompositePrimaryKeyWarning validates(*%i[stage_event_hash_id issue_id group_id project_id start_event_timestamp], presence: true) + + def self.issuable_id_column + :issue_id + end end end end diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb index d2f899ae933..d0ec3c4e8b9 100644 --- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb +++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb @@ -3,9 +3,14 @@ module Analytics module CycleAnalytics class MergeRequestStageEvent < ApplicationRecord + include StageEventModel extend SuppressCompositePrimaryKeyWarning validates(*%i[stage_event_hash_id merge_request_id group_id project_id start_event_timestamp], presence: true) + + def self.issuable_id_column + :merge_request_id + end end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 5f16b990d01..5a8cbd8d71c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -9,8 +9,6 @@ class ApplicationSetting < ApplicationRecord include Sanitizable ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22' - ignore_column :seat_link_enabled, remove_with: '14.4', remove_after: '2021-09-22' - ignore_column :cloud_license_enabled, remove_with: '14.4', remove_after: '2021-09-22' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ @@ -366,6 +364,10 @@ class ApplicationSetting < ApplicationRecord validates :container_registry_expiration_policies_worker_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :dependency_proxy_ttl_group_policy_worker_capacity, + allow_nil: false, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :invisible_captcha_enabled, inclusion: { in: [true, false], message: _('must be a boolean value') } @@ -481,6 +483,8 @@ class ApplicationSetting < ApplicationRecord validates :throttle_unauthenticated_packages_api_period_in_seconds validates :throttle_unauthenticated_files_api_requests_per_period validates :throttle_unauthenticated_files_api_period_in_seconds + validates :throttle_unauthenticated_deprecated_api_requests_per_period + validates :throttle_unauthenticated_deprecated_api_period_in_seconds validates :throttle_authenticated_api_requests_per_period validates :throttle_authenticated_api_period_in_seconds validates :throttle_authenticated_git_lfs_requests_per_period @@ -491,6 +495,8 @@ class ApplicationSetting < ApplicationRecord validates :throttle_authenticated_packages_api_period_in_seconds validates :throttle_authenticated_files_api_requests_per_period validates :throttle_authenticated_files_api_period_in_seconds + validates :throttle_authenticated_deprecated_api_requests_per_period + validates :throttle_authenticated_deprecated_api_period_in_seconds validates :throttle_protected_paths_requests_per_period validates :throttle_protected_paths_period_in_seconds end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 612fda158d3..7bdea36bb8a 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -159,6 +159,7 @@ module ApplicationSettingImplementation spam_check_endpoint_enabled: false, spam_check_endpoint_url: nil, spam_check_api_key: nil, + suggest_pipeline_enabled: true, terminal_max_session_time: 0, throttle_authenticated_api_enabled: false, throttle_authenticated_api_period_in_seconds: 3600, @@ -175,6 +176,9 @@ module ApplicationSettingImplementation throttle_authenticated_files_api_enabled: false, throttle_authenticated_files_api_period_in_seconds: 15, throttle_authenticated_files_api_requests_per_period: 500, + throttle_authenticated_deprecated_api_enabled: false, + throttle_authenticated_deprecated_api_period_in_seconds: 3600, + throttle_authenticated_deprecated_api_requests_per_period: 3600, throttle_incident_management_notification_enabled: false, throttle_incident_management_notification_per_period: 3600, throttle_incident_management_notification_period_in_seconds: 3600, @@ -193,6 +197,9 @@ module ApplicationSettingImplementation throttle_unauthenticated_files_api_enabled: false, throttle_unauthenticated_files_api_period_in_seconds: 15, throttle_unauthenticated_files_api_requests_per_period: 125, + throttle_unauthenticated_deprecated_api_enabled: false, + throttle_unauthenticated_deprecated_api_period_in_seconds: 3600, + throttle_unauthenticated_deprecated_api_requests_per_period: 1800, time_tracking_limit_to_hours: false, two_factor_grace_period: 48, unique_ips_limit_enabled: false, diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index f17fff742fe..a1c6793607f 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -71,7 +71,7 @@ class AuditEvent < ApplicationRecord end def lazy_author - BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader| + BatchLoader.for(author_id).batch do |author_ids, loader| User.select(:id, :name, :username).where(id: author_ids).find_each do |user| loader.call(user.id, user) end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index d251b0adbd3..c8f6b9aaedb 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -66,5 +66,3 @@ class AwardEmoji < ApplicationRecord awardable.try(:update_upvotes_count) if upvote? end end - -AwardEmoji.prepend_mod_with('AwardEmoji') diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index dee55675304..818ae04ba29 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -4,7 +4,8 @@ # projects to a GitLab instance. It associates the import with the responsible # user. class BulkImport < ApplicationRecord - MINIMUM_GITLAB_MAJOR_VERSION = 14 + MIN_MAJOR_VERSION = 14 + MIN_MINOR_VERSION_FOR_PROJECT = 4 belongs_to :user, optional: false @@ -34,6 +35,14 @@ class BulkImport < ApplicationRecord end end + def source_version_info + Gitlab::VersionInfo.parse(source_version) + end + + def self.min_gl_version_for_project_migration + Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT) + end + def self.all_human_statuses state_machine.states.map(&:human_name) end diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index ab5d248ff8c..ecac4ab95f4 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -20,6 +20,8 @@ class BulkImports::Entity < ApplicationRecord self.table_name = 'bulk_import_entities' + EXPORT_RELATIONS_URL = '/%{resource}/%{full_path}/export_relations' + belongs_to :bulk_import, optional: false belongs_to :parent, class_name: 'BulkImports::Entity', optional: true @@ -81,9 +83,9 @@ class BulkImports::Entity < ApplicationRecord def pipelines @pipelines ||= case source_type when 'group_entity' - BulkImports::Groups::Stage.pipelines + BulkImports::Groups::Stage.new(bulk_import).pipelines when 'project_entity' - BulkImports::Projects::Stage.pipelines + BulkImports::Projects::Stage.new(bulk_import).pipelines end end @@ -102,6 +104,14 @@ class BulkImports::Entity < ApplicationRecord end end + def pluralized_name + source_type.gsub('_entity', '').pluralize + end + + def export_relations_url_path + @export_relations_url_path ||= EXPORT_RELATIONS_URL % { resource: pluralized_name, full_path: encoded_source_full_path } + end + private def validate_parent_is_a_group diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb index 371b58dea03..8d4d31ee92d 100644 --- a/app/models/bulk_imports/export.rb +++ b/app/models/bulk_imports/export.rb @@ -53,7 +53,7 @@ module BulkImports end def relation_definition - config.portable_tree[:include].find { |include| include[relation.to_sym] } + config.relation_definition_for(relation) end def config diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb index ff165830cf1..abf064adaae 100644 --- a/app/models/bulk_imports/export_status.rb +++ b/app/models/bulk_imports/export_status.rb @@ -41,7 +41,7 @@ module BulkImports end def status_endpoint - "/groups/#{entity.encoded_source_full_path}/export_relations/status" + File.join(entity.export_relations_url_path, 'status') end end end diff --git a/app/models/bulk_imports/file_transfer/base_config.rb b/app/models/bulk_imports/file_transfer/base_config.rb index ddea7c3f64c..4d370315ad5 100644 --- a/app/models/bulk_imports/file_transfer/base_config.rb +++ b/app/models/bulk_imports/file_transfer/base_config.rb @@ -22,15 +22,25 @@ module BulkImports end def export_path - strong_memoize(:export_path) do - relative_path = File.join(base_export_path, SecureRandom.hex) - - ::Gitlab::ImportExport.export_path(relative_path: relative_path) - end + @export_path ||= Dir.mktmpdir('bulk_imports') end def portable_relations - import_export_config.dig(:tree, portable_class_sym).keys.map(&:to_s) - skipped_relations + tree_relations + file_relations - skipped_relations + end + + def tree_relation?(relation) + tree_relations.include?(relation) + end + + def file_relation?(relation) + file_relations.include?(relation) + end + + def tree_relation_definition_for(relation) + return unless tree_relation?(relation) + + portable_tree[:include].find { |include| include[relation.to_sym] } end private @@ -44,7 +54,7 @@ module BulkImports end def import_export_config - ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h + @config ||= ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h end def portable_class @@ -63,8 +73,12 @@ module BulkImports raise NotImplementedError end - def base_export_path - raise NotImplementedError + def tree_relations + import_export_config.dig(:tree, portable_class_sym).keys.map(&:to_s) + end + + def file_relations + [] end def skipped_relations diff --git a/app/models/bulk_imports/file_transfer/group_config.rb b/app/models/bulk_imports/file_transfer/group_config.rb index 2266cbb484f..6766c00246b 100644 --- a/app/models/bulk_imports/file_transfer/group_config.rb +++ b/app/models/bulk_imports/file_transfer/group_config.rb @@ -3,16 +3,14 @@ module BulkImports module FileTransfer class GroupConfig < BaseConfig - def base_export_path - portable.full_path - end + SKIPPED_RELATIONS = %w(members).freeze def import_export_yaml ::Gitlab::ImportExport.group_config_file end def skipped_relations - @skipped_relations ||= %w(members) + SKIPPED_RELATIONS end end end diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb index 8a57f51c1c5..9a0434da08a 100644 --- a/app/models/bulk_imports/file_transfer/project_config.rb +++ b/app/models/bulk_imports/file_transfer/project_config.rb @@ -3,16 +3,23 @@ module BulkImports module FileTransfer class ProjectConfig < BaseConfig - def base_export_path - portable.disk_path - end + UPLOADS_RELATION = 'uploads' + + SKIPPED_RELATIONS = %w( + project_members + group_members + ).freeze def import_export_yaml ::Gitlab::ImportExport.config_file end + def file_relations + [UPLOADS_RELATION] + end + def skipped_relations - @skipped_relations ||= %w(project_members group_members) + SKIPPED_RELATIONS end end end diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb index c185470b1c2..9de3239ee0f 100644 --- a/app/models/bulk_imports/tracker.rb +++ b/app/models/bulk_imports/tracker.rb @@ -50,6 +50,8 @@ class BulkImports::Tracker < ApplicationRecord event :start do transition created: :started + # To avoid errors when re-starting a pipeline in case of network errors + transition started: :started end event :finish do diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 97fb8233d34..50bda64d537 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -31,7 +31,7 @@ module Ci next unless bridge.triggers_downstream_pipeline? bridge.run_after_commit do - ::Ci::CreateCrossProjectPipelineWorker.perform_async(bridge.id) + ::Ci::CreateDownstreamPipelineWorker.perform_async(bridge.id) end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e2e24247679..990ef71a457 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -42,6 +42,10 @@ module Ci has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build + # Projects::DestroyService destroys Ci::Pipelines, which use_fast_destroy on :job_artifacts + # before we delete builds. By doing this, the relation should be empty and not fire any + # 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 :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id @@ -55,6 +59,8 @@ module Ci has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build has_one :trace_metadata, class_name: 'Ci::BuildTraceMetadata', inverse_of: :build + has_many :terraform_state_versions, class_name: 'Terraform::StateVersion', dependent: :nullify, inverse_of: :build, foreign_key: :ci_build_id # rubocop:disable Cop/ActiveRecordDependent + accepts_nested_attributes_for :runner_session, update_only: true accepts_nested_attributes_for :job_variables @@ -64,8 +70,8 @@ module Ci delegate :gitlab_deploy_token, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true - ignore_columns :id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22' - ignore_columns :stage_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22' + ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22' + ignore_columns :stage_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22' ## # Since Gitlab 11.5, deployments records started being created right after @@ -192,7 +198,6 @@ module Ci add_authentication_token_field :token, encrypted: :required before_save :ensure_token - before_destroy { unscoped_project } after_save :stick_build_if_status_changed @@ -308,8 +313,10 @@ module Ci end after_transition pending: :running do |build| - Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do - build.deployment&.run + unless build.update_deployment_after_transaction_commit? + Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do + build.deployment&.run + end end build.run_after_commit do @@ -332,8 +339,10 @@ module Ci end after_transition any => [:success] do |build| - Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do - build.deployment&.succeed + unless build.update_deployment_after_transaction_commit? + Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do + build.deployment&.succeed + end end build.run_after_commit do @@ -346,12 +355,14 @@ module Ci next unless build.project next unless build.deployment - begin - Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do - build.deployment.drop! + unless build.update_deployment_after_transaction_commit? + begin + Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do + build.deployment.drop! + end + rescue StandardError => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id) end - rescue StandardError => e - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id) end true @@ -370,14 +381,29 @@ module Ci end after_transition any => [:skipped, :canceled] do |build, transition| - Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do - if transition.to_name == :skipped - build.deployment&.skip - else - build.deployment&.cancel + unless build.update_deployment_after_transaction_commit? + Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do + if transition.to_name == :skipped + build.deployment&.skip + else + build.deployment&.cancel + end end end end + + # Synchronize Deployment Status + # Please note that the data integirty is not assured because we can't use + # a database transaction due to DB decomposition. + after_transition do |build, transition| + next if transition.loopback? + next unless build.project + next unless build.update_deployment_after_transaction_commit? + + build.run_after_commit do + build.deployment&.sync_status_with(build) + end + end end def self.build_matchers(project) @@ -1094,6 +1120,12 @@ module Ci runner&.instance_type? end + def update_deployment_after_transaction_commit? + strong_memoize(:update_deployment_after_transaction_commit) do + Feature.enabled?(:update_deployment_after_transaction_commit, project, default_enabled: :yaml) + end + end + protected def run_status_commit_hooks! @@ -1108,7 +1140,7 @@ module Ci return unless saved_change_to_status? return unless running? - ::Gitlab::Database::LoadBalancing::Sticking.stick(:build, id) + self.class.sticking.stick(:build, id) end def status_commit_hooks @@ -1154,10 +1186,6 @@ module Ci self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil) end - def unscoped_project - @unscoped_project ||= Project.unscoped.find_by(id: project_id) - end - def environment_url options&.dig(:environment, :url) || persisted_environment&.external_url end diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 90237a4be52..0d6d6f7a6a5 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -37,8 +37,8 @@ module Ci job_timeout_source: 4 } - ignore_column :build_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22' - ignore_columns :id_convert_to_bigint, remove_with: '14.3', remove_after: '2021-09-22' + ignore_column :build_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22' + ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22' def update_timeout_state timeout = timeout_with_highest_precedence diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 003659570b3..bf1470ca20f 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -5,8 +5,6 @@ module Ci include BulkInsertSafe include IgnorableColumns - ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22' - belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs validates :build, presence: true diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 45de47116cd..e12c0f82c99 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -6,8 +6,6 @@ module Ci class BuildRunnerSession < Ci::ApplicationRecord include IgnorableColumns - ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22' - TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' DEFAULT_SERVICE_NAME = 'build' DEFAULT_PORT_NAME = 'default_port' diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 7a15d7ba940..6edb5ef4579 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -9,8 +9,6 @@ module Ci include ::Gitlab::OptimisticLocking include IgnorableColumns - ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22' - belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id default_value_for :data_store, :redis_trace_chunks diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index 901b84ceec6..1ffa0e31f99 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -37,8 +37,10 @@ module Ci increment!(:archival_attempts, touch: :last_archival_attempt_at) end - def track_archival!(trace_artifact_id) - update!(trace_artifact_id: trace_artifact_id, archived_at: Time.current) + def track_archival!(trace_artifact_id, checksum) + update!(trace_artifact_id: trace_artifact_id, + checksum: checksum, + archived_at: Time.current) end def archival_attempts_message @@ -49,6 +51,11 @@ module Ci end end + def remote_checksum_valid? + checksum.present? && + checksum == remote_checksum + end + private def backoff diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb index 99118f8090b..c2ab8ca0929 100644 --- a/app/models/ci/job_token/project_scope_link.rb +++ b/app/models/ci/job_token/project_scope_link.rb @@ -5,7 +5,7 @@ module Ci module JobToken - class ProjectScopeLink < ApplicationRecord + class ProjectScopeLink < Ci::ApplicationRecord self.table_name = 'ci_job_token_project_scope_links' belongs_to :source_project, class_name: 'Project' diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index 42cfdc21d66..3a5765aa00c 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -32,12 +32,15 @@ module Ci def all_projects Project.from_union([ Project.id_in(source_project), - Project.where_exists( - Ci::JobToken::ProjectScopeLink - .from_project(source_project) - .where('projects.id = ci_job_token_project_scope_links.target_project_id')) + Project.id_in(target_project_ids) ], remove_duplicates: false) end + + private + + def target_project_ids + Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id) + end end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1a0cec3c935..0041ec5135c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -82,7 +82,8 @@ module Ci # Merge requests for which the current pipeline is running against # the merge request's latest commit. has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest' - + has_many :package_build_infos, class_name: 'Packages::BuildInfo', dependent: :nullify, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent + has_many :package_file_build_infos, class_name: 'Packages::PackageFileBuildInfo', dependent: :nullify, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline has_many :failed_builds, -> { latest.failed }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline @@ -861,11 +862,6 @@ module Ci self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self) end - def execute_hooks - project.execute_hooks(pipeline_data, :pipeline_hooks) if project.has_active_hooks?(:pipeline_hooks) - project.execute_integrations(pipeline_data, :pipeline_hooks) if project.has_active_integrations?(:pipeline_hooks) - end - # All the merge requests for which the current pipeline runs/ran against def all_merge_requests @all_merge_requests ||= @@ -929,9 +925,22 @@ module Ci end def environments_in_self_and_descendants - environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id') + if ::Feature.enabled?(:avoid_cross_joins_environments_in_self_and_descendants, default_enabled: :yaml) + # We limit to 100 unique environments for application safety. + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700 + expanded_environment_names = + builds_in_self_and_descendants.joins(:metadata) + .where.not('ci_builds_metadata.expanded_environment_name' => nil) + .distinct('ci_builds_metadata.expanded_environment_name') + .limit(100) + .pluck(:expanded_environment_name) + + Environment.where(project: project, name: expanded_environment_names) + else + environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id') - Environment.where(id: environment_ids) + Environment.where(id: environment_ids) + end end # With multi-project and parent-child pipelines @@ -1251,12 +1260,6 @@ module Ci messages.build(severity: severity, content: content) end - def pipeline_data - strong_memoize(:pipeline_data) do - Gitlab::DataBuilder::Pipeline.build(self) - end - end - def merge_request_diff_sha return unless merge_request? diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 30d335fd7d5..372df8cc264 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -58,7 +58,8 @@ module Ci after_transition any => ::Ci::Processable.completed_statuses do |processable| next unless processable.with_resource_group? - next unless processable.resource_group.release_resource_from(processable) + + processable.resource_group.release_resource_from(processable) processable.run_after_commit do Ci::ResourceGroups::AssignResourceFromResourceGroupWorker diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb index 8a7456041e6..6d25f747a9d 100644 --- a/app/models/ci/resource_group.rb +++ b/app/models/ci/resource_group.rb @@ -14,6 +14,12 @@ module Ci before_create :ensure_resource + enum process_mode: { + unordered: 0, + oldest_first: 1, + newest_first: 2 + } + ## # NOTE: This is concurrency-safe method that the subquery in the `UPDATE` # works as explicit locking. @@ -25,8 +31,34 @@ module Ci resources.retained_by(processable).update_all(build_id: nil) > 0 end + def upcoming_processables + if unordered? + processables.waiting_for_resource + elsif oldest_first? + processables.waiting_for_resource_or_upcoming + .order(Arel.sql("commit_id ASC, #{sort_by_job_status}")) + elsif newest_first? + processables.waiting_for_resource_or_upcoming + .order(Arel.sql("commit_id DESC, #{sort_by_job_status}")) + else + Ci::Processable.none + end + end + private + # In order to avoid deadlock, we do NOT specify the job execution order in the same pipeline. + # The system processes wherever ready to transition to `pending` status from `waiting_for_resource`. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/202186 for more information. + def sort_by_job_status + <<~SQL + CASE status + WHEN 'waiting_for_resource' THEN 0 + ELSE 1 + END ASC + SQL + end + def ensure_resource # Currently we only support one resource per group, which means # maximum one build can be set to the resource group, thus builds diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 4aa232ad26b..2f718ad7582 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -51,7 +51,7 @@ module Ci has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :runner_projects has_many :runner_namespaces, inverse_of: :runner, autosave: true - has_many :groups, through: :runner_namespaces + has_many :groups, through: :runner_namespaces, disable_joins: true has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build' @@ -246,7 +246,7 @@ module Ci begin transaction do - self.projects << project + self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self) self.save! end rescue ActiveRecord::RecordInvalid => e @@ -280,7 +280,7 @@ module Ci end def belongs_to_more_than_one_project? - self.projects.limit(2).count(:all) > 1 + runner_projects.limit(2).count(:all) > 1 end def assigned_to_group? @@ -309,7 +309,9 @@ module Ci end def only_for?(project) - projects == [project] + ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do + projects == [project] + end end def short_sha @@ -344,7 +346,7 @@ module Ci # intention here is not to execute `Ci::RegisterJobService#execute` on # the primary database. # - ::Gitlab::Database::LoadBalancing::Sticking.stick(:runner, id) + ::Ci::Runner.sticking.stick(:runner, id) SecureRandom.hex.tap do |new_update| ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update, @@ -428,10 +430,8 @@ module Ci end def no_projects - ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do - if projects.any? - errors.add(:runner, 'cannot have projects assigned') - end + if runner_projects.any? + errors.add(:runner, 'cannot have projects assigned') end end @@ -444,14 +444,16 @@ module Ci end def any_project - unless projects.any? + unless runner_projects.any? errors.add(:runner, 'needs to be assigned to at least one project') end end def exactly_one_group - unless groups.one? - errors.add(:runner, 'needs to be assigned to exactly one group') + ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do + unless groups.one? + errors.add(:runner, 'needs to be assigned to exactly one group') + end end end diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb index d1353b97ed9..52a31863fb2 100644 --- a/app/models/ci/runner_namespace.rb +++ b/app/models/ci/runner_namespace.rb @@ -7,7 +7,6 @@ module Ci self.limit_name = 'ci_registered_group_runners' self.limit_scope = :group self.limit_relation = :recent_runners - self.limit_feature_flag = :ci_runner_limits self.limit_feature_flag_for_override = :ci_runner_limits_override belongs_to :runner, inverse_of: :runner_namespaces diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index e1c435e9b1f..148a29a0f8b 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -7,7 +7,6 @@ module Ci self.limit_name = 'ci_registered_project_runners' self.limit_scope = :project self.limit_relation = :recent_runners - self.limit_feature_flag = :ci_runner_limits self.limit_feature_flag_for_override = :ci_runner_limits_override belongs_to :runner, inverse_of: :runner_projects diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 39e26bf2785..131e18adf62 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -8,8 +8,6 @@ module Ci include Presentable include IgnorableColumns - ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22' - enum status: Ci::HasStatus::STATUSES_ENUM belongs_to :project diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb index 74c0cec3b7e..28a711aaf17 100644 --- a/app/models/clusters/agents/group_authorization.rb +++ b/app/models/clusters/agents/group_authorization.rb @@ -10,7 +10,9 @@ module Clusters validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' } - delegate :project, to: :agent + def config_project + agent.project + end end end end diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb index 967cc686045..9f7f653ed65 100644 --- a/app/models/clusters/agents/implicit_authorization.rb +++ b/app/models/clusters/agents/implicit_authorization.rb @@ -6,12 +6,15 @@ module Clusters attr_reader :agent delegate :id, to: :agent, prefix: true - delegate :project, to: :agent def initialize(agent:) @agent = agent end + def config_project + agent.project + end + def config nil end diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb index 1c71a0a432a..f6d19086751 100644 --- a/app/models/clusters/agents/project_authorization.rb +++ b/app/models/clusters/agents/project_authorization.rb @@ -9,6 +9,10 @@ module Clusters belongs_to :project, class_name: '::Project', optional: false validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' } + + def config_project + agent.project + end end end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 993ccb33655..7cef92ce81a 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -72,7 +72,7 @@ module Clusters if cluster.group_type? attributes[:groups] = [group] elsif cluster.project_type? - attributes[:projects] = [project] + attributes[:runner_projects] = [::Ci::RunnerProject.new(project: project)] end attributes diff --git a/app/models/clusters/integrations/elastic_stack.rb b/app/models/clusters/integrations/elastic_stack.rb index 565d268259a..97d73d252b9 100644 --- a/app/models/clusters/integrations/elastic_stack.rb +++ b/app/models/clusters/integrations/elastic_stack.rb @@ -14,6 +14,8 @@ module Clusters validates :cluster, presence: true validates :enabled, inclusion: { in: [true, false] } + scope :enabled, -> { where(enabled: true) } + def available? enabled end diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb index 3f2c47d48e6..d745a49afc1 100644 --- a/app/models/clusters/integrations/prometheus.rb +++ b/app/models/clusters/integrations/prometheus.rb @@ -21,6 +21,8 @@ module Clusters default_value_for(:alert_manager_token) { SecureRandom.hex } + scope :enabled, -> { where(enabled: true) } + after_destroy do run_after_commit do deactivate_project_integrations diff --git a/app/models/commit.rb b/app/models/commit.rb index 6c8b4ae1139..553681ee960 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -133,7 +133,7 @@ class Commit end def lazy(container, oid) - BatchLoader.for({ container: container, oid: oid }).batch(replace_methods: false) do |items, loader| + BatchLoader.for({ container: container, oid: oid }).batch do |items, loader| items_by_container = items.group_by { |i| i[:container] } items_by_container.each do |container, commit_ids| diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8cba3d04502..43427e2ebc7 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -62,6 +62,9 @@ class CommitStatus < Ci::ApplicationRecord scope :updated_before, ->(lookback:, timeout:) { where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout) } + scope :scheduled_at_before, ->(date) { + where('ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?', date) + } # The scope applies `pluck` to split the queries. Use with care. scope :for_project_paths, -> (paths) do diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index 7bb6004ca83..d9e6756ab86 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -27,7 +27,8 @@ module Analytics alias_attribute :custom_stage?, :custom scope :default_stages, -> { where(custom: false) } scope :ordered, -> { order(:relative_position, :id) } - scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered } + scope :with_preloaded_labels, -> { includes(:start_event_label, :end_event_label) } + scope :for_list, -> { with_preloaded_labels.ordered } scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) } before_save :ensure_stage_event_hash_id diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb new file mode 100644 index 00000000000..7462e1e828b --- /dev/null +++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module StageEventModel + extend ActiveSupport::Concern + + class_methods do + def upsert_data(data) + upsert_values = data.map do |row| + row.values_at( + :stage_event_hash_id, + :issuable_id, + :group_id, + :project_id, + :author_id, + :milestone_id, + :start_event_timestamp, + :end_event_timestamp + ) + end + + value_list = Arel::Nodes::ValuesList.new(upsert_values).to_sql + + query = <<~SQL + INSERT INTO #{quoted_table_name} + ( + stage_event_hash_id, + #{connection.quote_column_name(issuable_id_column)}, + group_id, + project_id, + milestone_id, + author_id, + start_event_timestamp, + end_event_timestamp + ) + #{value_list} + ON CONFLICT(stage_event_hash_id, #{issuable_id_column}) + DO UPDATE SET + group_id = excluded.group_id, + project_id = excluded.project_id, + start_event_timestamp = excluded.start_event_timestamp, + end_event_timestamp = excluded.end_event_timestamp, + milestone_id = excluded.milestone_id, + author_id = excluded.author_id + SQL + + result = connection.execute(query) + result.cmd_tuples + end + end + end + end +end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 84a74386ff7..b32502c3ee2 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -18,6 +18,7 @@ module Avatarable prepend ShadowMethods include ObjectStorage::BackgroundMove include Gitlab::Utils::StrongMemoize + include ApplicationHelper validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validates :avatar, file_size: { maximum: MAXIMUM_FILE_SIZE }, if: :avatar_changed? @@ -110,7 +111,7 @@ module Avatarable def retrieve_upload_from_batch(identifier) BatchLoader.for(identifier: identifier, model: self) - .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args| + .batch(key: self.class) do |upload_params, loader, args| model_class = args[:key] paths = upload_params.flat_map do |params| params[:model].upload_paths(params[:identifier]) diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb index 908f0b6a7e2..6c3093ca916 100644 --- a/app/models/concerns/bulk_insert_safe.rb +++ b/app/models/concerns/bulk_insert_safe.rb @@ -51,6 +51,12 @@ module BulkInsertSafe PrimaryKeySetError = Class.new(StandardError) class_methods do + def insert_all_proxy_class + @insert_all_proxy_class ||= Class.new(self) do + attr_readonly :created_at + end + end + def set_callback(name, *args) unless _bulk_insert_callback_allowed?(name, args) raise MethodNotAllowedError, @@ -138,7 +144,7 @@ module BulkInsertSafe when nil false else - raise ArgumentError, "returns needs to be :ids or nil" + returns end # Handle insertions for tables with a composite primary key @@ -153,9 +159,9 @@ module BulkInsertSafe item_batch, validate, &handle_attributes) ActiveRecord::InsertAll - .new(self, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by) + .new(insert_all_proxy_class, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by) .execute - .pluck(primary_key) + .cast_values(insert_all_proxy_class.attribute_types).to_a end end end diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb index 056abafd0ce..9812c62fcc4 100644 --- a/app/models/concerns/checksummable.rb +++ b/app/models/concerns/checksummable.rb @@ -8,8 +8,12 @@ module Checksummable Zlib.crc32(data) end - def hexdigest(path) + def sha256_hexdigest(path) ::Digest::SHA256.file(path).hexdigest end + + def md5_hexdigest(path) + ::Digest::MD5.file(path).hexdigest + end end end diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb index c1299e3d468..8d715279da8 100644 --- a/app/models/concerns/ci/has_status.rb +++ b/app/models/concerns/ci/has_status.rb @@ -95,6 +95,7 @@ module Ci scope :failed_or_canceled, -> { with_status(:failed, :canceled) } scope :complete, -> { with_status(completed_statuses) } scope :incomplete, -> { without_statuses(completed_statuses) } + scope :waiting_for_resource_or_upcoming, -> { with_status(:created, :scheduled, :waiting_for_resource) } scope :cancelable, -> do where(status: [:running, :waiting_for_resource, :preparing, :pending, :created, :scheduled]) diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index ec86746ae54..344f5aa4cd5 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -20,6 +20,7 @@ module Ci delegate :interruptible, to: :metadata, prefix: false, allow_nil: true delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true + delegate :runner_features, to: :metadata, prefix: false, allow_nil: false before_create :ensure_metadata end diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb index 7f46e44697e..1b4cc14f4a2 100644 --- a/app/models/concerns/enums/ci/commit_status.rb +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -27,6 +27,7 @@ module Enums no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data trace_size_exceeded: 19, builds_disabled: 20, + environment_creation_failure: 21, insufficient_bridge_permissions: 1_001, downstream_bridge_project_not_found: 1_002, invalid_bridge_trigger: 1_003, diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index 9218ba47d20..d614d6c4584 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -72,12 +72,10 @@ module HasRepository end def default_branch - @default_branch ||= repository.root_ref || default_branch_from_preferences + @default_branch ||= repository.empty? ? default_branch_from_preferences : repository.root_ref end def default_branch_from_preferences - return unless empty_repo? - (default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence end diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb index 1709b56080e..25a1d855119 100644 --- a/app/models/concerns/integrations/has_data_fields.rb +++ b/app/models/concerns/integrations/has_data_fields.rb @@ -45,7 +45,6 @@ module Integrations included do has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData' has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData' - has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData' has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData' def data_fields diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb index 933e8b5f687..209456f8b67 100644 --- a/app/models/concerns/issue_available_features.rb +++ b/app/models/concerns/issue_available_features.rb @@ -12,7 +12,8 @@ module IssueAvailableFeatures { assignee: %w(issue incident), confidentiality: %w(issue incident), - time_tracking: %w(issue incident) + time_tracking: %w(issue incident), + move_and_clone: %w(issue incident) }.with_indifferent_access end end diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb index 196bec04be6..ff52769fce8 100644 --- a/app/models/concerns/packages/debian/distribution.rb +++ b/app/models/concerns/packages/debian/distribution.rb @@ -96,18 +96,8 @@ module Packages architectures.pluck(:name).sort end - def needs_update? - !file.exists? || time_duration_expired? - end - private - def time_duration_expired? - return false unless valid_time_duration_seconds.present? - - updated_at + valid_time_duration_seconds.seconds + 6.hours < Time.current - end - def unique_codename_and_suite errors.add(:codename, _('has already been taken as Suite')) if codename_exists_as_suite? errors.add(:suite, _('has already been taken as Codename')) if suite_exists_as_codename? diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb index 587f8c35ff7..cf97be21165 100644 --- a/app/models/concerns/restricted_signup.rb +++ b/app/models/concerns/restricted_signup.rb @@ -7,15 +7,49 @@ module RestrictedSignup def validate_admin_signup_restrictions(email) return if allowed_domain?(email) + error_type = fetch_error_type(email) + + return unless error_type.present? + + [ + signup_email_invalid_message, + error_message[created_by_key][error_type] + ].join(' ') + end + + def fetch_error_type(email) if allowlist_present? - return _('domain is not authorized for sign-up.') + :allowlist elsif denied_domain?(email) - return _('is not from an allowed domain.') + :denylist elsif restricted_email?(email) - return _('is not allowed. Try again with a different email address, or contact your GitLab admin.') + :restricted end + end + + def error_message + { + admin: { + allowlist: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check 'Allowed domains for sign-ups'.")).html_safe, + denylist: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check the 'Domain denylist'.")).html_safe, + restricted: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check 'Email restrictions for sign-ups'.")).html_safe, + group_setting: html_escape_once(_("Go to the group’s 'Settings > General' page, and check 'Restrict membership by email domain'.")).html_safe + }, + nonadmin: { + allowlist: error_nonadmin, + denylist: error_nonadmin, + restricted: error_nonadmin, + group_setting: error_nonadmin + } + } + end + + def error_nonadmin + _("Check with your administrator.") + end - nil + def created_by_key + created_by&.can_admin_all_resources? ? :admin : :nonadmin end def denied_domain?(email) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 847abdc1b6d..f382b3624ed 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -41,7 +41,7 @@ module Routable has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - validates :route, presence: true + validates :route, presence: true, unless: -> { is_a?(Namespaces::ProjectNamespace) } scope :with_route, -> { includes(:route) } @@ -185,6 +185,7 @@ module Routable def prepare_route return unless full_path_changed? || full_name_changed? + return if is_a?(Namespaces::ProjectNamespace) route || build_route(source: self) route.path = build_full_path diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb new file mode 100644 index 00000000000..00abe0a06e6 --- /dev/null +++ b/app/models/concerns/ttl_expirable.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module TtlExpirable + extend ActiveSupport::Concern + + included do + validates :status, presence: true + + enum status: { default: 0, expired: 1, processing: 2, error: 3 } + + scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) } + scope :active, -> { where(status: :default) } + + scope :lock_next_by, ->(sort) do + order(sort) + .limit(1) + .lock('FOR UPDATE SKIP LOCKED') + end + end +end diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb index a656856487d..7f96b3901f1 100644 --- a/app/models/concerns/vulnerability_finding_helpers.rb +++ b/app/models/concerns/vulnerability_finding_helpers.rb @@ -2,6 +2,15 @@ module VulnerabilityFindingHelpers extend ActiveSupport::Concern + + # Manually resolvable report types cannot be considered fixed once removed from the + # target branch due to requiring active triage, such as rotation of an exposed token. + REPORT_TYPES_REQUIRING_MANUAL_RESOLUTION = %w[secret_detection].freeze + + def requires_manual_resolution? + REPORT_TYPES_REQUIRING_MANUAL_RESOLUTION.include?(report_type) + end + def matches_signatures(other_signatures, other_uuid) other_signature_types = other_signatures.index_by(&:algorithm_type) diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb index 9bacd9a0edf..aecb47f7a03 100644 --- a/app/models/container_expiration_policy.rb +++ b/app/models/container_expiration_policy.rb @@ -74,6 +74,7 @@ class ContainerExpirationPolicy < ApplicationRecord '7d': _('%{days} days until tags are automatically removed') % { days: 7 }, '14d': _('%{days} days until tags are automatically removed') % { days: 14 }, '30d': _('%{days} days until tags are automatically removed') % { days: 30 }, + '60d': _('%{days} days until tags are automatically removed') % { days: 60 }, '90d': _('%{days} days until tags are automatically removed') % { days: 90 } } end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index aea48a5ec20..ecdac64b31b 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -5,7 +5,7 @@ class CustomEmoji < ApplicationRecord belongs_to :namespace, inverse_of: :custom_emoji - belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' + belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id' belongs_to :creator, class_name: "User", inverse_of: :created_custom_emoji # For now only external emoji are supported. See https://gitlab.com/gitlab-org/gitlab/-/issues/230467 diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb index aaa7e2ae175..c632f8e2efa 100644 --- a/app/models/customer_relations/contact.rb +++ b/app/models/customer_relations/contact.rb @@ -5,8 +5,9 @@ class CustomerRelations::Contact < ApplicationRecord self.table_name = "customer_relations_contacts" - belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id' + belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'group_id' belongs_to :organization, optional: true + has_and_belongs_to_many :issues, join_table: :issue_customer_relations_contacts # rubocop: disable Rails/HasAndBelongsToMany strip_attributes! :phone, :first_name, :last_name diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb index a18d3ab8148..c206d1e05f5 100644 --- a/app/models/customer_relations/organization.rb +++ b/app/models/customer_relations/organization.rb @@ -5,7 +5,7 @@ class CustomerRelations::Organization < ApplicationRecord self.table_name = "customer_relations_organizations" - belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id' + belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'group_id' strip_attributes! :name diff --git a/app/models/dependency_proxy/blob.rb b/app/models/dependency_proxy/blob.rb index 5de6b1cf28f..7ca15652586 100644 --- a/app/models/dependency_proxy/blob.rb +++ b/app/models/dependency_proxy/blob.rb @@ -2,15 +2,14 @@ class DependencyProxy::Blob < ApplicationRecord include FileStoreMounter + include TtlExpirable + include EachBatch belongs_to :group validates :group, presence: true validates :file, presence: true validates :file_name, presence: true - validates :status, presence: true - - enum status: { default: 0, expired: 1 } mount_file_store_uploader DependencyProxy::FileUploader diff --git a/app/models/dependency_proxy/image_ttl_group_policy.rb b/app/models/dependency_proxy/image_ttl_group_policy.rb index 5a1b8cb8f1f..0dfb298a39e 100644 --- a/app/models/dependency_proxy/image_ttl_group_policy.rb +++ b/app/models/dependency_proxy/image_ttl_group_policy.rb @@ -8,4 +8,6 @@ class DependencyProxy::ImageTtlGroupPolicy < ApplicationRecord validates :group, presence: true validates :enabled, inclusion: { in: [true, false] } validates :ttl, numericality: { greater_than: 0 }, allow_nil: true + + scope :enabled, -> { where(enabled: true) } end diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb index 15e5137b50a..b83047efe54 100644 --- a/app/models/dependency_proxy/manifest.rb +++ b/app/models/dependency_proxy/manifest.rb @@ -2,6 +2,8 @@ class DependencyProxy::Manifest < ApplicationRecord include FileStoreMounter + include TtlExpirable + include EachBatch belongs_to :group @@ -9,9 +11,6 @@ class DependencyProxy::Manifest < ApplicationRecord validates :file, presence: true validates :file_name, presence: true validates :digest, presence: true - validates :status, presence: true - - enum status: { default: 0, expired: 1 } mount_file_store_uploader DependencyProxy::FileUploader diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 4a690ccc67e..f91700f764b 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -10,7 +10,8 @@ class Deployment < ApplicationRecord include FastDestroyAll include IgnorableColumns - ignore_column :deployable_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22' + StatusUpdateError = Class.new(StandardError) + StatusSyncError = Class.new(StandardError) belongs_to :project, required: true belongs_to :environment, required: true @@ -48,7 +49,6 @@ class Deployment < ApplicationRecord scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } scope :active, -> { where(status: %i[created running]) } scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) } - scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) } scope :with_api_entity_associations, -> { preload({ deployable: { runner: [], tags: [], user: [], job_artifacts_archive: [] } }) } scope :finished_after, ->(date) { where('finished_at >= ?', date) } @@ -150,6 +150,16 @@ class Deployment < ApplicationRecord success.find_by!(iid: iid) end + # It should be used with caution especially on chaining. + # Fetching any unbounded or large intermediate dataset could lead to loading too many IDs into memory. + # See: https://docs.gitlab.com/ee/development/database/multiple_databases.html#use-disable_joins-for-has_one-or-has_many-through-relations + # For safety we default limit to fetch not more than 1000 records. + def self.builds(limit = 1000) + deployable_ids = where.not(deployable_id: nil).limit(limit).pluck(:deployable_id) + + Ci::Build.where(id: deployable_ids) + end + class << self ## # FastDestroyAll concerns @@ -305,20 +315,23 @@ class Deployment < ApplicationRecord # Changes the status of a deployment and triggers the corresponding state # machine events. def update_status(status) - case status - when 'running' - run - when 'success' - succeed - when 'failed' - drop - when 'canceled' - cancel - when 'skipped' - skip - else - raise ArgumentError, "The status #{status.inspect} is invalid" - end + update_status!(status) + rescue StandardError => e + Gitlab::ErrorTracking.track_exception( + StatusUpdateError.new(e.message), deployment_id: self.id) + + false + end + + def sync_status_with(build) + return false unless ::Deployment.statuses.include?(build.status) + + update_status!(build.status) + rescue StandardError => e + Gitlab::ErrorTracking.track_exception( + StatusSyncError.new(e.message), deployment_id: self.id, build_id: build.id) + + false end def valid_sha @@ -346,6 +359,23 @@ class Deployment < ApplicationRecord private + def update_status!(status) + case status + when 'running' + run! + when 'success' + succeed! + when 'failed' + drop! + when 'canceled' + cancel! + when 'skipped' + skip! + else + raise ArgumentError, "The status #{status.inspect} is invalid" + end + end + def legacy_finished_at self.created_at if success? && !read_attribute(:finished_at) end diff --git a/app/models/environment.rb b/app/models/environment.rb index 48522a23068..31ab426728b 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -28,8 +28,8 @@ class Environment < ApplicationRecord has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment' - has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) } - has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) } + has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true + has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true has_one :upcoming_deployment, -> { running.distinct_on_environment }, 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 @@ -198,14 +198,14 @@ class Environment < ApplicationRecord # Overriding association def last_visible_deployable - return super if association_cached?(:last_visible_deployable) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) + return super if association_cached?(:last_visible_deployable) last_visible_deployment&.deployable end # Overriding association def last_visible_pipeline - return super if association_cached?(:last_visible_pipeline) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) + return super if association_cached?(:last_visible_pipeline) last_visible_deployable&.pipeline end @@ -260,10 +260,9 @@ class Environment < ApplicationRecord end def cancel_deployment_jobs! - jobs = active_deployments.with_deployable - jobs.each do |deployment| - Gitlab::OptimisticLocking.retry_lock(deployment.deployable, name: 'environment_cancel_deployment_jobs') do |deployable| - deployable.cancel! if deployable&.cancelable? + active_deployments.builds.each do |build| + Gitlab::OptimisticLocking.retry_lock(build, name: 'environment_cancel_deployment_jobs') do |build| + build.cancel! if build&.cancelable? end rescue StandardError => e Gitlab::ErrorTracking.track_exception(e, environment_id: id, deployment_id: deployment.id) diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 3be7af2e4bf..07c0983f239 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -100,13 +100,11 @@ class EnvironmentStatus def self.build_environments_status(mr, user, pipeline) return [] unless pipeline - ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/340781') do - pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment| - next unless Ability.allowed?(user, :read_environment, environment) + pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment| + next unless Ability.allowed?(user, :read_environment, environment) - EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha) - end.compact - end + EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha) + end.compact end private_class_method :build_environments_status end diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb index 39ecc487806..2d6a4694def 100644 --- a/app/models/error_tracking/error.rb +++ b/app/models/error_tracking/error.rb @@ -7,6 +7,14 @@ class ErrorTracking::Error < ApplicationRecord has_many :events, class_name: 'ErrorTracking::ErrorEvent' + has_one :first_event, + -> { order(id: :asc) }, + class_name: 'ErrorTracking::ErrorEvent' + + has_one :last_event, + -> { order(id: :desc) }, + class_name: 'ErrorTracking::ErrorEvent' + scope :for_status, -> (status) { where(status: status) } validates :project, presence: true @@ -90,7 +98,10 @@ class ErrorTracking::Error < ApplicationRecord status: status, tags: { level: nil, logger: nil }, external_url: external_url, - external_base_url: external_base_url + external_base_url: external_base_url, + integrated: true, + first_release_version: first_event&.release, + last_release_version: last_event&.release ) end @@ -106,6 +117,6 @@ class ErrorTracking::Error < ApplicationRecord # For compatibility with sentry integration def external_base_url - Gitlab::Routing.url_helpers.root_url + Gitlab::Routing.url_helpers.project_url(project) end end diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb index 4de13de7e2e..686518a39fb 100644 --- a/app/models/error_tracking/error_event.rb +++ b/app/models/error_tracking/error_event.rb @@ -22,6 +22,10 @@ class ErrorTracking::ErrorEvent < ApplicationRecord ) end + def release + payload.dig('release') + end + private def build_stacktrace diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index dd5ce9f7387..25f812645b1 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -46,6 +46,11 @@ module ErrorTracking after_save :clear_reactive_cache! + # When a user enables the integrated error tracking + # we want to immediately provide them with a first + # working client key so they have a DSN for Sentry SDK. + after_save :create_client_key! + def sentry_enabled enabled && !integrated_client? end @@ -54,6 +59,12 @@ module ErrorTracking integrated end + def gitlab_dsn + strong_memoize(:gitlab_dsn) do + client_key&.sentry_dsn + end + end + def api_url=(value) super clear_memoization(:api_url_slugs) @@ -236,5 +247,19 @@ module ErrorTracking errors.add(:project, 'is a required field') end end + + def client_key + # Project can have multiple client keys. + # However for UI simplicity we render the first active one for user. + # In future we should make it possible to manage client keys from UI. + # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596 + project.error_tracking_client_keys.active.first + end + + def create_client_key! + if enabled? && integrated_client? && !client_key + project.error_tracking_client_keys.create! + end + end end end diff --git a/app/models/group.rb b/app/models/group.rb index a667a908707..c5e119451e3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -192,9 +192,15 @@ class Group < Namespace # Returns the ids of the passed group models where the `emails_disabled` # column is set to true anywhere in the ancestor hierarchy. def ids_with_disabled_email(groups) - innner_query = Gitlab::ObjectHierarchy - .new(Group.where('id = namespaces_with_emails_disabled.id')) - .base_and_ancestors + inner_groups = Group.where('id = namespaces_with_emails_disabled.id') + + inner_ancestors = if Feature.enabled?(:linear_group_ancestor_scopes, default_enabled: :yaml) + inner_groups.self_and_ancestors + else + Gitlab::ObjectHierarchy.new(inner_groups).base_and_ancestors + end + + inner_query = inner_ancestors .where(emails_disabled: true) .select('1') .limit(1) @@ -202,7 +208,7 @@ class Group < Namespace group_ids = Namespace .from('(SELECT * FROM namespaces) as namespaces_with_emails_disabled') .where(namespaces_with_emails_disabled: { id: groups }) - .where('EXISTS (?)', innner_query) + .where('EXISTS (?)', inner_query) .pluck(:id) Set.new(group_ids) @@ -701,9 +707,9 @@ class Group < Namespace raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state) case state - when 'disabled_and_unoverridable' then disable_shared_runners! # also disallows override - when 'disabled_with_override' then disable_shared_runners_and_allow_override! - when 'enabled' then enable_shared_runners! # set both to true + when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override + when SR_DISABLED_WITH_OVERRIDE then disable_shared_runners_and_allow_override! + when SR_ENABLED then enable_shared_runners! # set both to true end end diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb index 9565dae08b5..0bf9e805aa8 100644 --- a/app/models/instance_configuration.rb +++ b/app/models/instance_configuration.rb @@ -22,7 +22,12 @@ class InstanceConfiguration private def ssh_algorithms_hashes - SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact + SSH_ALGORITHMS.select { |algo| ssh_algorithm_enabled?(algo) }.map { |algo| ssh_algorithm_hashes(algo) }.compact + end + + def ssh_algorithm_enabled?(algorithm) + algorithm_key_restriction = application_settings["#{algorithm.downcase}_key_restriction"] + algorithm_key_restriction.nil? || algorithm_key_restriction != ApplicationSetting::FORBIDDEN_KEY_VALUE end def host @@ -98,6 +103,11 @@ class InstanceConfiguration requests_per_period: application_settings[:throttle_authenticated_packages_api_requests_per_period], period_in_seconds: application_settings[:throttle_authenticated_packages_api_period_in_seconds] }, + authenticated_git_lfs_api: { + enabled: application_settings[:throttle_authenticated_git_lfs_enabled], + requests_per_period: application_settings[:throttle_authenticated_git_lfs_requests_per_period], + period_in_seconds: application_settings[:throttle_authenticated_git_lfs_period_in_seconds] + }, issue_creation: application_setting_limit_per_minute(:issues_create_limit), note_creation: application_setting_limit_per_minute(:notes_create_limit), project_export: application_setting_limit_per_minute(:project_export_limit), diff --git a/app/models/integrations/open_project.rb b/app/models/integrations/open_project.rb deleted file mode 100644 index e4cfb24151a..00000000000 --- a/app/models/integrations/open_project.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Integrations - class OpenProject < BaseIssueTracker - validates :url, public_url: true, presence: true, if: :activated? - validates :api_url, public_url: true, allow_blank: true, if: :activated? - validates :token, presence: true, if: :activated? - validates :project_identifier_code, presence: true, if: :activated? - - data_field :url, :api_url, :token, :closed_status_id, :project_identifier_code - - def data_fields - open_project_tracker_data || self.build_open_project_tracker_data - end - - def self.to_param - 'open_project' - end - end -end diff --git a/app/models/integrations/open_project_tracker_data.rb b/app/models/integrations/open_project_tracker_data.rb deleted file mode 100644 index b3f2618b94f..00000000000 --- a/app/models/integrations/open_project_tracker_data.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Integrations - class OpenProjectTrackerData < ApplicationRecord - include BaseDataFields - - # When the Open Project is fresh installed, the default closed status id is "13" based on current version: v8. - DEFAULT_CLOSED_STATUS_ID = "13" - - attr_encrypted :url, encryption_options - attr_encrypted :api_url, encryption_options - attr_encrypted :token, encryption_options - - def closed_status_id - super || DEFAULT_CLOSED_STATUS_ID - end - end -end diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index ad6a9164d00..e3e180ae959 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -15,13 +15,8 @@ module Integrations end def help - 'This service sends notifications about projects events to a Unify Circuit conversation.<br /> - To set up this service: - <ol> - <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448" target="_blank" rel="noopener noreferrer">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li> - <li>Paste the <strong>Webhook URL</strong> into the field below.</li> - <li>Select events below to enable notifications.</li> - </ol>' + docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer' + s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end def event_field(event) @@ -37,7 +32,7 @@ module Integrations def default_fields [ - { type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true }, + { type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } ] diff --git a/app/models/issue.rb b/app/models/issue.rb index e0b0c352c22..9c568414ec2 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -81,6 +81,7 @@ class Issue < ApplicationRecord 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 :prometheus_alerts, through: :prometheus_alert_events + has_and_belongs_to_many :customer_relations_contacts, join_table: :issue_customer_relations_contacts, class_name: 'CustomerRelations::Contact' # rubocop: disable Rails/HasAndBelongsToMany accepts_nested_attributes_for :issuable_severity, update_only: true accepts_nested_attributes_for :sentry_issue @@ -107,8 +108,6 @@ class Issue < ApplicationRecord scope :order_due_date_asc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'ASC')) } scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) } scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) } - scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) } - scope :order_relative_position_desc, -> { reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')) } scope :order_closed_date_desc, -> { reorder(closed_at: :desc) } scope :order_created_at_desc, -> { reorder(created_at: :desc) } scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') } @@ -127,6 +126,7 @@ class Issue < ApplicationRecord project: [:route, { namespace: :route }]) } scope :with_issue_type, ->(types) { where(issue_type: types) } + scope :without_issue_type, ->(types) { where.not(issue_type: types) } scope :public_only, -> { without_hidden.where(confidential: false) @@ -166,6 +166,8 @@ class Issue < ApplicationRecord scope :by_project_id_and_iid, ->(composites) do where_composite(%i[project_id iid], composites) end + scope :with_null_relative_position, -> { where(relative_position: nil) } + scope :with_non_null_relative_position, -> { where.not(relative_position: nil) } after_commit :expire_etag_cache, unless: :importing? after_save :ensure_metrics, unless: :importing? @@ -266,8 +268,8 @@ class Issue < ApplicationRecord 'due_date' => -> { order_due_date_asc.with_order_id_desc }, 'due_date_asc' => -> { order_due_date_asc.with_order_id_desc }, 'due_date_desc' => -> { order_due_date_desc.with_order_id_desc }, - 'relative_position' => -> { order_relative_position_asc.with_order_id_desc }, - 'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc } + 'relative_position' => -> { order_by_relative_position }, + 'relative_position_asc' => -> { order_by_relative_position } } ) end @@ -277,7 +279,7 @@ class Issue < ApplicationRecord when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc when 'due_date_desc' then order_due_date_desc.with_order_id_desc - when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc + when 'relative_position', 'relative_position_asc' then order_by_relative_position when 'severity_asc' then order_severity_asc.with_order_id_desc when 'severity_desc' then order_severity_desc.with_order_id_desc else @@ -285,13 +287,8 @@ class Issue < ApplicationRecord end end - # `with_cte` argument allows sorting when using CTE queries and prevents - # errors in postgres when using CTE search optimisation - def self.order_by_position_and_priority(with_cte: false) - order = Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_highest_priority, column_order_id_desc]) - - order_labels_priority(with_cte: with_cte) - .reorder(order) + def self.order_by_relative_position + reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc])) end def self.column_order_relative_position @@ -306,25 +303,6 @@ class Issue < ApplicationRecord ) end - def self.column_order_highest_priority - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'highest_priority', - column_expression: Arel.sql('highest_priorities.label_priority'), - order_expression: Gitlab::Database.nulls_last_order('highest_priorities.label_priority', 'ASC'), - reversed_order_expression: Gitlab::Database.nulls_last_order('highest_priorities.label_priority', 'DESC'), - order_direction: :asc, - nullable: :nulls_last, - distinct: false - ) - end - - def self.column_order_id_desc - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: arel_table[:id].desc - ) - end - def self.column_order_id_asc Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( attribute_name: 'id', @@ -541,6 +519,10 @@ class Issue < ApplicationRecord issue_type_supports?(:time_tracking) end + def supports_move_and_clone? + issue_type_supports?(:move_and_clone) + end + def email_participants_emails issue_email_participants.pluck(:email) end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 53e7d52c558..9765ac6f2e9 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -49,7 +49,7 @@ class LfsObject < ApplicationRecord end def self.calculate_oid(path) - self.hexdigest(path) + self.sha256_hexdigest(path) end end diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb index a39d88b2e49..ca5a2800a03 100644 --- a/app/models/loose_foreign_keys/deleted_record.rb +++ b/app/models/loose_foreign_keys/deleted_record.rb @@ -2,48 +2,4 @@ class LooseForeignKeys::DeletedRecord < ApplicationRecord extend SuppressCompositePrimaryKeyWarning - include PartitionedTable - - partitioned_by :created_at, strategy: :monthly, retain_for: 3.months, retain_non_empty_partitions: true - - scope :ordered_by_primary_keys, -> { order(:created_at, :deleted_table_name, :deleted_table_primary_key_value) } - - def self.load_batch(batch_size) - ordered_by_primary_keys - .limit(batch_size) - .to_a - end - - # Because the table has composite primary keys, the delete_all or delete methods are not going to work. - # This method implements deletion that benefits from the primary key index, example: - # - # > DELETE - # > FROM "loose_foreign_keys_deleted_records" - # > WHERE (created_at, - # > deleted_table_name, - # > deleted_table_primary_key_value) IN - # > (SELECT created_at::TIMESTAMP WITH TIME ZONE, - # > deleted_table_name, - # > deleted_table_primary_key_value - # > FROM (VALUES (LIST_OF_VALUES)) AS primary_key_values (created_at, deleted_table_name, deleted_table_primary_key_value)) - def self.delete_records(records) - values = records.pluck(:created_at, :deleted_table_name, :deleted_table_primary_key_value) - - primary_keys = connection.primary_keys(table_name).join(', ') - - primary_keys_with_type_cast = [ - Arel.sql('created_at::timestamp with time zone'), - Arel.sql('deleted_table_name'), - Arel.sql('deleted_table_primary_key_value') - ] - - value_list = Arel::Nodes::ValuesList.new(values) - - # (SELECT primary keys FROM VALUES) - inner_query = Arel::SelectManager.new - inner_query.from("#{Arel::Nodes::Grouping.new([value_list]).as('primary_key_values').to_sql} (#{primary_keys})") - inner_query.projections = primary_keys_with_type_cast - - where(Arel::Nodes::Grouping.new([Arel.sql(primary_keys)]).in(inner_query)).delete_all - end end diff --git a/app/models/member.rb b/app/models/member.rb index beb4c05f2a6..21fd4aebd7b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -50,6 +50,11 @@ class Member < ApplicationRecord }, if: :project_bot? + scope :with_invited_user_state, -> do + joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email') + .select('members.*', 'invited_user.state as invited_user_state') + end + scope :in_hierarchy, ->(source) do groups = source.root_ancestor.self_and_descendants group_members = Member.default_scoped.where(source: groups) @@ -178,7 +183,13 @@ class Member < ApplicationRecord after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met? after_save :log_invitation_token_cleanup - after_commit :refresh_member_authorized_projects, unless: :importing? + after_commit on: [:create, :update], unless: :importing? do + refresh_member_authorized_projects(blocking: true) + end + + after_commit on: [:destroy], unless: :importing? do + refresh_member_authorized_projects(blocking: false) + end default_value_for :notification_level, NotificationSetting.levels[:global] @@ -395,8 +406,8 @@ class Member < ApplicationRecord # transaction has been committed, resulting in the job either throwing an # error or not doing any meaningful work. # rubocop: disable CodeReuse/ServiceClass - def refresh_member_authorized_projects - UserProjectAccessChangedService.new(user_id).execute + def refresh_member_authorized_projects(blocking:) + UserProjectAccessChangedService.new(user_id).execute(blocking: blocking) end # rubocop: enable CodeReuse/ServiceClass @@ -442,6 +453,14 @@ class Member < ApplicationRecord errors.add(:user, error) if error end + def signup_email_invalid_message + if source_type == 'Project' + _("is not allowed for this project.") + else + _("is not allowed for this group.") + end + end + def update_highest_role? return unless user_id.present? diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index a13133c90e9..9062a405218 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -43,15 +43,17 @@ class GroupMember < Member # Because source_type is `Namespace`... def real_source_type - 'Group' + Group.sti_name end def notifiable_options { group: group } end + private + override :refresh_member_authorized_projects - def refresh_member_authorized_projects + def refresh_member_authorized_projects(blocking:) # Here, `destroyed_by_association` will be present if the # GroupMember is being destroyed due to the `dependent: :destroy` # callback on Group. In this case, there is no need to refresh the @@ -63,8 +65,6 @@ class GroupMember < Member super end - private - def access_level_inclusion return if access_level.in?(Gitlab::Access.all_values) diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 72cb831cc88..eec46b3493e 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -90,24 +90,28 @@ class ProjectMember < Member { project: project } end + private + override :refresh_member_authorized_projects - def refresh_member_authorized_projects + def refresh_member_authorized_projects(blocking:) return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh) return unless user # rubocop:disable CodeReuse/ServiceClass - AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute + if blocking + AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute + else + AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.perform_async(project.id, user.id) + end # Until we compare the inconsistency rates of the new, specialized service and # the old approach, we still run AuthorizedProjectsWorker # but with some delay and lower urgency as a safety net. UserProjectAccessChangedService.new(user_id) - .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY) + .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY) # rubocop:enable CodeReuse/ServiceClass end - private - def send_invite run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index db49ec6f412..15862fb2bfa 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1111,15 +1111,23 @@ class MergeRequest < ApplicationRecord can_be_merged? && !should_be_rebased? end + # rubocop: disable CodeReuse/ServiceClass def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) return false unless open? return false if work_in_progress? return false if broken? - return false unless skip_ci_check || mergeable_ci_state? return false unless skip_discussions_check || mergeable_discussions_state? - true + if Feature.enabled?(:improved_mergeability_checks, self.project, default_enabled: :yaml) + additional_checks = MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: { skip_ci_check: skip_ci_check }) + additional_checks.execute.all?(&:success?) + else + return false unless skip_ci_check || mergeable_ci_state? + + true + end end + # rubocop: enable CodeReuse/ServiceClass def ff_merge_possible? project.repository.ancestor?(target_branch_sha, diff_head_sha) @@ -1658,6 +1666,10 @@ class MergeRequest < ApplicationRecord service_class.new(project, current_user, id: id, report_type: report_type).execute(comparison_base_pipeline(identifier), actual_head_pipeline) end + def recent_diff_head_shas(limit = 100) + merge_request_diffs.recent(limit).pluck(:head_commit_sha) + end + def all_commits MergeRequestDiffCommit .where(merge_request_diff: merge_request_diffs.recent) @@ -1857,7 +1869,7 @@ class MergeRequest < ApplicationRecord override :ensure_metrics def ensure_metrics - if Feature.enabled?(:use_upsert_query_for_mr_metrics) + if Feature.enabled?(:use_upsert_query_for_mr_metrics, default_enabled: :yaml) MergeRequest::Metrics.record!(self) else # Backward compatibility: some merge request metrics records will not have target_project_id filled in. @@ -1918,20 +1930,6 @@ class MergeRequest < ApplicationRecord end end - def lazy_upvotes_count - BatchLoader.for(id).batch(default_value: 0) do |ids, loader| - counts = AwardEmoji - .where(awardable_id: ids) - .upvotes - .group(:awardable_id) - .count - - counts.each do |id, count| - loader.call(id, count) - end - end - end - private def set_draft_status diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index d2b3ca753b1..bd94c0ad30e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -66,7 +66,7 @@ class MergeRequestDiff < ApplicationRecord joins(:merge_request).where(merge_requests: { target_project_id: project_id }) end - scope :recent, -> { order(id: :desc).limit(100) } + scope :recent, -> (limit = 100) { order(id: :desc).limit(limit) } scope :files_in_database, -> do where(stored_externally: [false, nil]).where(arel_table[:files_count].gt(0)) diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb index 3383dda20c9..d3d3f973398 100644 --- a/app/models/metrics/dashboard/annotation.rb +++ b/app/models/metrics/dashboard/annotation.rb @@ -32,19 +32,19 @@ module Metrics def ending_at_after_starting_at return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at - errors.add(:ending_at, s_("Metrics::Dashboard::Annotation|can't be before starting_at time")) + errors.add(:ending_at, s_("MetricsDashboardAnnotation|can't be before starting_at time")) end def single_ownership return if cluster.nil? ^ environment.nil? - errors.add(:base, s_("Metrics::Dashboard::Annotation|Annotation can't belong to both a cluster and an environment at the same time")) + errors.add(:base, s_("MetricsDashboardAnnotation|Annotation can't belong to both a cluster and an environment at the same time")) end def orphaned_annotation return if cluster.present? || environment.present? - errors.add(:base, s_("Metrics::Dashboard::Annotation|Annotation must belong to a cluster or an environment")) + errors.add(:base, s_("MetricsDashboardAnnotation|Annotation must belong to a cluster or an environment")) end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 0c160cedb4d..e6406293c66 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -28,7 +28,10 @@ class Namespace < ApplicationRecord # Android repo (15) + some extra backup. NUMBER_OF_ANCESTORS_ALLOWED = 20 - SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze + SR_DISABLED_AND_UNOVERRIDABLE = 'disabled_and_unoverridable' + SR_DISABLED_WITH_OVERRIDE = 'disabled_with_override' + SR_ENABLED = 'enabled' + SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_ENABLED].freeze URL_MAX_LENGTH = 255 cache_markdown_field :description, pipeline: :description @@ -44,6 +47,8 @@ class Namespace < ApplicationRecord # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. + # TODO: can this be moved into the UserNamespace class? + # evaluate in issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070 belongs_to :owner, class_name: "User" belongs_to :parent, class_name: "Namespace" @@ -63,21 +68,31 @@ class Namespace < ApplicationRecord length: { maximum: 255 } validates :description, length: { maximum: 255 } + validates :path, presence: true, - length: { maximum: URL_MAX_LENGTH }, - namespace_path: true + length: { maximum: URL_MAX_LENGTH } + + validates :path, namespace_path: true, if: ->(n) { !n.project_namespace? } + # Project path validator is used for project namespaces for now to assure + # compatibility with project paths + # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341764 + validates :path, project_path: true, if: ->(n) { n.project_namespace? } # Introduce minimal path length of 2 characters. # Allow change of other attributes without forcing users to # rename their user or group. At the same time prevent changing # the path without complying with new 2 chars requirement. # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/225214 - validates :path, length: { minimum: 2 }, if: :path_changed? + # + # For ProjectNamespace we don't check minimal path length to keep + # compatibility with existing project restrictions. + # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341764 + validates :path, length: { minimum: 2 }, if: :enforce_minimum_path_length? validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true } - validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type) } + validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type, default_enabled: :yaml) } validate :nesting_level_allowed validate :changing_shared_runners_enabled_is_allowed validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed @@ -93,7 +108,7 @@ class Namespace < ApplicationRecord # Legacy Storage specific hooks - after_update :move_dir, if: :saved_change_to_path_or_parent? + 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: -> { @@ -101,7 +116,12 @@ class Namespace < ApplicationRecord saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id? } - scope :for_user, -> { where(type: nil) } + # TODO: change to `type: Namespaces::UserNamespace.sti_name` when + # working on issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070 + scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) } + # TODO: this can be simplified with `type != 'Project'` when working on issue + # https://gitlab.com/gitlab-org/gitlab/-/issues/341070 + scope :without_project_namespaces, -> { where("type IS DISTINCT FROM ?", Namespaces::ProjectNamespace.sti_name) } scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) } scope :include_route, -> { includes(:route) } scope :by_parent, -> (parent) { where(parent_id: parent) } @@ -138,14 +158,12 @@ class Namespace < ApplicationRecord class << self def sti_class_for(type_name) case type_name - when 'Group' + when Group.sti_name Group - when 'Project' + when Namespaces::ProjectNamespace.sti_name Namespaces::ProjectNamespace - when 'User' - # TODO: We create a normal Namespace until - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68894 is ready - Namespace + when Namespaces::UserNamespace.sti_name + Namespaces::UserNamespace else Namespace end @@ -247,27 +265,27 @@ class Namespace < ApplicationRecord end def kind - return 'group' if group? - return 'project' if project? + return 'group' if group_namespace? + return 'project' if project_namespace? 'user' # defaults to user end - def group? + def group_namespace? type == Group.sti_name end - def project? + def project_namespace? type == Namespaces::ProjectNamespace.sti_name end - def user? + def user_namespace? # That last bit ensures we're considered a user namespace as a default - type.nil? || type == Namespaces::UserNamespace.sti_name || !(group? || project?) + type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?) end def owner_required? - user? + user_namespace? end def find_fork_of(project) @@ -314,7 +332,7 @@ class Namespace < ApplicationRecord # that belongs to this namespace def all_projects if Feature.enabled?(:recursive_approach_for_all_projects, default_enabled: :yaml) - namespace = user? ? self : self_and_descendant_ids + namespace = user_namespace? ? self : self_and_descendant_ids Project.where(namespace: namespace) else Project.inside_path(full_path) @@ -416,7 +434,7 @@ class Namespace < ApplicationRecord def changing_shared_runners_enabled_is_allowed return unless new_record? || changes.has_key?(:shared_runners_enabled) - if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable' + if shared_runners_enabled && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE errors.add(:shared_runners_enabled, _('cannot be enabled because parent group has shared Runners disabled')) end end @@ -428,30 +446,30 @@ class Namespace < ApplicationRecord errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled')) end - if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable' + if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be enabled because parent group does not allow it')) end end def shared_runners_setting if shared_runners_enabled - 'enabled' + SR_ENABLED else if allow_descendants_override_disabled_shared_runners - 'disabled_with_override' + SR_DISABLED_WITH_OVERRIDE else - 'disabled_and_unoverridable' + SR_DISABLED_AND_UNOVERRIDABLE end end end def shared_runners_setting_higher_than?(other_setting) - if other_setting == 'enabled' + if other_setting == SR_ENABLED false - elsif other_setting == 'disabled_with_override' - shared_runners_setting == 'enabled' - elsif other_setting == 'disabled_and_unoverridable' - shared_runners_setting == 'enabled' || shared_runners_setting == 'disabled_with_override' + elsif other_setting == SR_DISABLED_WITH_OVERRIDE + shared_runners_setting == SR_ENABLED + elsif other_setting == SR_DISABLED_AND_UNOVERRIDABLE + shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE else raise ArgumentError end @@ -536,21 +554,21 @@ class Namespace < ApplicationRecord def validate_parent_type unless has_parent? - if project? + if project_namespace? errors.add(:parent_id, _('must be set for a project namespace')) end return end - if parent.project? + if parent.project_namespace? errors.add(:parent_id, _('project namespace cannot be the parent of another namespace')) end - if user? + if user_namespace? errors.add(:parent_id, _('cannot not be used for user namespace')) - elsif group? - errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user? + elsif group_namespace? + errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user_namespace? end end @@ -575,6 +593,10 @@ class Namespace < ApplicationRecord project.track_project_repository end end + + def enforce_minimum_path_length? + path_changed? && !project_namespace? + end end Namespace.prepend_mod_with('Namespace') diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb index 73061b78637..99e32537595 100644 --- a/app/models/namespace/root_storage_statistics.rb +++ b/app/models/namespace/root_storage_statistics.rb @@ -57,7 +57,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord end def attributes_from_personal_snippets - return {} unless namespace.user? + return {} unless namespace.user_namespace? from_personal_snippets.take.slice(SNIPPETS_SIZE_STAT_NAME) end diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb index 517d68b118d..22b7a0a3b2b 100644 --- a/app/models/namespaces/user_namespace.rb +++ b/app/models/namespaces/user_namespace.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # TODO: currently not created/mapped in the database, will be done in another issue -# https://gitlab.com/gitlab-org/gitlab/-/issues/337102 +# https://gitlab.com/gitlab-org/gitlab/-/issues/341070 module Namespaces class UserNamespace < Namespace def self.sti_name diff --git a/app/models/note.rb b/app/models/note.rb index a8f5c305d9b..37473518892 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -149,7 +149,7 @@ class Note < ApplicationRecord scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) } before_validation :nullify_blank_type, :nullify_blank_line_code - after_save :keep_around_commit, if: :for_project_noteable?, unless: :importing? + 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 @@ -355,8 +355,6 @@ class Note < ApplicationRecord end def noteable_author?(noteable) - return false unless ::Feature.enabled?(:show_author_on_note, project) - noteable.author == self.author end diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 46810749b18..7db396bcad5 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -19,13 +19,10 @@ module Operations default_value_for :active, true default_value_for :version, :new_version_flag - # scopes exists only for the first version - has_many :scopes, class_name: 'Operations::FeatureFlagScope' # strategies exists only for the second version has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy' has_many :feature_flag_issues has_many :issues, through: :feature_flag_issues - has_one :default_scope, -> { where(environment_scope: '*') }, class_name: 'Operations::FeatureFlagScope' validates :project, presence: true validates :name, @@ -37,10 +34,7 @@ module Operations } validates :name, uniqueness: { scope: :project_id } validates :description, allow_blank: true, length: 0..255 - validate :first_default_scope, on: :create, if: :has_scopes? - validate :version_associations - accepts_nested_attributes_for :scopes, allow_destroy: true accepts_nested_attributes_for :strategies, allow_destroy: true scope :ordered, -> { order(:name) } @@ -56,7 +50,7 @@ module Operations class << self def preload_relations - preload(:scopes, strategies: :scopes) + preload(strategies: :scopes) end def for_unleash_client(project, environment) @@ -104,13 +98,6 @@ module Operations Ability.issues_readable_by_user(issues, current_user) end - def execute_hooks(current_user) - run_after_commit do - feature_flag_data = Gitlab::DataBuilder::FeatureFlag.build(self, current_user) - project.execute_hooks(feature_flag_data, :feature_flag_hooks) - end - end - def hook_attrs { id: id, @@ -119,27 +106,5 @@ module Operations active: active } end - - private - - def version_associations - if new_version_flag? && scopes.any? - errors.add(:version_associations, 'version 2 feature flags may not have scopes') - end - end - - def first_default_scope - unless scopes.first.environment_scope == '*' - errors.add(:default_scope, 'has to be the first element') - end - end - - def build_default_scope - scopes.build(environment_scope: '*', active: self.active) - end - - def has_scopes? - scopes.any? - end end end diff --git a/app/models/operations/feature_flag_scope.rb b/app/models/operations/feature_flag_scope.rb deleted file mode 100644 index 9068ca0f588..00000000000 --- a/app/models/operations/feature_flag_scope.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -# All of the legacy flags have been removed in 14.1, including all of the -# `operations_feature_flag_scopes` rows. Therefore, this model and the database -# table are unused and should be removed. - -module Operations - class FeatureFlagScope < ApplicationRecord - prepend HasEnvironmentScope - include Gitlab::Utils::StrongMemoize - - self.table_name = 'operations_feature_flag_scopes' - - belongs_to :feature_flag - - validates :environment_scope, uniqueness: { - scope: :feature_flag, - message: "(%{value}) has already been taken" - } - - validates :environment_scope, - if: :default_scope?, on: :update, - inclusion: { in: %w(*), message: 'cannot be changed from default scope' } - - validates :strategies, feature_flag_strategies: true - - before_destroy :prevent_destroy_default_scope, if: :default_scope? - - scope :ordered, -> { order(:id) } - scope :enabled, -> { where(active: true) } - scope :disabled, -> { where(active: false) } - - def self.with_name_and_description - joins(:feature_flag) - .select(FeatureFlag.arel_table[:name], FeatureFlag.arel_table[:description]) - end - - def self.for_unleash_client(project, environment) - select_columns = [ - 'DISTINCT ON (operations_feature_flag_scopes.feature_flag_id) operations_feature_flag_scopes.id', - '(operations_feature_flags.active AND operations_feature_flag_scopes.active) AS active', - 'operations_feature_flag_scopes.strategies', - 'operations_feature_flag_scopes.environment_scope', - 'operations_feature_flag_scopes.created_at', - 'operations_feature_flag_scopes.updated_at' - ] - - select(select_columns) - .with_name_and_description - .where(feature_flag_id: project.operations_feature_flags.select(:id)) - .order(:feature_flag_id) - .on_environment(environment) - .reverse_order - end - - private - - def default_scope? - environment_scope_was == '*' - end - - def prevent_destroy_default_scope - raise ActiveRecord::ReadOnlyRecord, "default scope cannot be destroyed" - end - end -end diff --git a/app/models/packages/composer/cache_file.rb b/app/models/packages/composer/cache_file.rb index ecd7596b989..5222101d171 100644 --- a/app/models/packages/composer/cache_file.rb +++ b/app/models/packages/composer/cache_file.rb @@ -9,15 +9,13 @@ module Packages mount_file_store_uploader Packages::Composer::CacheUploader - belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' + belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id' belongs_to :namespace validates :namespace, presence: true scope :with_namespace, ->(namespace) { where(namespace: namespace) } scope :with_sha, ->(sha) { where(file_sha256: sha) } - scope :expired, -> { where("delete_at <= ?", Time.current) } - scope :without_namespace, -> { where(namespace_id: nil) } end end end diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb index 1771003d1f9..dfa4ab6df82 100644 --- a/app/models/packages/helm/file_metadatum.rb +++ b/app/models/packages/helm/file_metadatum.rb @@ -12,7 +12,7 @@ module Packages validates :channel, presence: true, - length: { maximum: 63 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.helm_channel_regex } validates :metadata, diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index c932d0bf800..0c5a155d48a 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -129,18 +129,15 @@ class PagesDomain < ApplicationRecord store = OpenSSL::X509::Store.new store.set_default_paths - # This forces to load all intermediate certificates stored in `certificate` - Tempfile.open('certificate_chain') do |f| - f.write(certificate) - f.flush - store.add_file(f.path) - end - - store.verify(x509) + store.verify(x509, untrusted_ca_certs_bundle) rescue OpenSSL::X509::StoreError false end + def untrusted_ca_certs_bundle + ::Gitlab::X509::Certificate.load_ca_certs_bundle(certificate) + end + def expired? return false unless x509 diff --git a/app/models/preloaders/merge_requests_preloader.rb b/app/models/preloaders/merge_requests_preloader.rb deleted file mode 100644 index cefe8408cab..00000000000 --- a/app/models/preloaders/merge_requests_preloader.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Preloaders - class MergeRequestsPreloader - attr_reader :merge_requests - - def initialize(merge_requests) - @merge_requests = merge_requests - end - - def execute - preloader = ActiveRecord::Associations::Preloader.new - preloader.preload(merge_requests, { target_project: [:project_feature] }) - merge_requests.each do |merge_request| - merge_request.lazy_upvotes_count - end - end - end -end diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb index d2026d3b333..52baa3be6c4 100644 --- a/app/models/product_analytics_event.rb +++ b/app/models/product_analytics_event.rb @@ -20,8 +20,6 @@ class ProductAnalyticsEvent < ApplicationRecord where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1) } - scope :by_category_and_action, ->(category, action) { where(se_category: category, se_action: action) } - def self.count_by_graph(graph, days) group(graph).timerange(days).count end diff --git a/app/models/project.rb b/app/models/project.rb index 74ffeef797e..6eb19b4462c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -98,6 +98,7 @@ class Project < ApplicationRecord before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } before_save :ensure_runners_token + before_save :ensure_project_namespace_in_sync after_save :update_project_statistics, if: :saved_change_to_namespace_id? @@ -128,26 +129,9 @@ class Project < ApplicationRecord after_initialize :use_hashed_storage after_create :check_repository_absence! - # Required during the `ActsAsTaggableOn::Tag -> Topic` migration - # TODO: remove 'acts_as_ordered_taggable_on' and ':topics_acts_as_taggable' in the further process of the migration - # https://gitlab.com/gitlab-org/gitlab/-/issues/335946 - acts_as_ordered_taggable_on :topics - has_many :topics_acts_as_taggable, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") }, - class_name: 'ActsAsTaggableOn::Tag', - through: :topic_taggings, - source: :tag - has_many :project_topics, -> { order(:id) }, class_name: 'Projects::ProjectTopic' has_many :topics, through: :project_topics, class_name: 'Projects::Topic' - # Required during the `ActsAsTaggableOn::Tag -> Topic` migration - # TODO: remove 'topics' in the further process of the migration - # https://gitlab.com/gitlab-org/gitlab/-/issues/335946 - alias_method :topics_new, :topics - def topics - self.topics_acts_as_taggable + self.topics_new - end - attr_accessor :old_path_with_namespace attr_accessor :template_name attr_writer :pipeline_status @@ -159,11 +143,11 @@ class Project < ApplicationRecord # Relations belongs_to :pool_repository belongs_to :creator, class_name: 'User' - belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' + belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id' belongs_to :namespace # Sync deletion via DB Trigger to ensure we do not have # a project without a project_namespace (or vice-versa) - belongs_to :project_namespace, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project + belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project alias_method :parent, :namespace alias_attribute :parent_id, :namespace_id @@ -233,6 +217,7 @@ class Project < ApplicationRecord has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :export_jobs, class_name: 'ProjectExportJob' + has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :project has_one :project_repository, inverse_of: :project has_one :tracing_setting, class_name: 'ProjectTracingSetting' has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting' @@ -652,15 +637,8 @@ class Project < ApplicationRecord scope :with_topic, ->(topic_name) do topic = Projects::Topic.find_by_name(topic_name) - acts_as_taggable_on_topic = ActsAsTaggableOn::Tag.find_by_name(topic_name) - - return none unless topic || acts_as_taggable_on_topic - - relations = [] - relations << where(id: topic.project_topics.select(:project_id)) if topic - relations << where(id: acts_as_taggable_on_topic.taggings.select(:taggable_id)) if acts_as_taggable_on_topic - Project.from_union(relations) + topic ? where(id: topic.project_topics.select(:project_id)) : none end enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } @@ -678,7 +656,7 @@ class Project < ApplicationRecord mount_uploader :bfg_object_map, AttachmentUploader def self.with_api_entity_associations - preload(:project_feature, :route, :topics, :topics_acts_as_taggable, :group, :timelogs, namespace: [:route, :owner]) + preload(:project_feature, :route, :topics, :group, :timelogs, namespace: [:route, :owner]) end def self.with_web_entity_associations @@ -851,7 +829,7 @@ class Project < ApplicationRecord end def group_ids - joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) + joins(:namespace).where(namespaces: { type: Group.sti_name }).select(:namespace_id) end # Returns ids of projects with issuables available for given user @@ -1200,7 +1178,7 @@ class Project < ApplicationRecord end def import? - external_import? || forked? || gitlab_project_import? || jira_import? || bare_repository_import? + external_import? || forked? || gitlab_project_import? || jira_import? || bare_repository_import? || gitlab_project_migration? end def external_import? @@ -1223,6 +1201,10 @@ class Project < ApplicationRecord import_type == 'gitlab_project' end + def gitlab_project_migration? + import_type == 'gitlab_project_migration' + end + def gitea_import? import_type == 'gitea' end @@ -1327,11 +1309,21 @@ class Project < ApplicationRecord def changing_shared_runners_enabled_is_allowed return unless new_record? || changes.has_key?(:shared_runners_enabled) - if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable' + if shared_runners_setting_conflicting_with_group? errors.add(:shared_runners_enabled, _('cannot be enabled because parent group does not allow it')) end end + def shared_runners_setting_conflicting_with_group? + shared_runners_enabled && group&.shared_runners_setting == Namespace::SR_DISABLED_AND_UNOVERRIDABLE + end + + def reconcile_shared_runners_setting! + if shared_runners_setting_conflicting_with_group? + self.shared_runners_enabled = false + end + end + def to_param if persisted? && errors.include?(:path) path_was @@ -1814,7 +1806,7 @@ class Project < ApplicationRecord def open_issues_count(current_user = nil) return Projects::OpenIssuesCountService.new(self, current_user).count unless current_user.nil? - BatchLoader.for(self).batch(replace_methods: false) do |projects, loader| + BatchLoader.for(self).batch do |projects, loader| issues_count_per_project = ::Projects::BatchOpenIssuesCountService.new(projects).refresh_cache_and_retrieve_data issues_count_per_project.each do |project, count| @@ -2279,7 +2271,7 @@ class Project < ApplicationRecord # rubocop: disable CodeReuse/ServiceClass def forks_count - BatchLoader.for(self).batch(replace_methods: false) do |projects, loader| + BatchLoader.for(self).batch do |projects, loader| fork_count_per_project = ::Projects::BatchForksCountService.new(projects).refresh_cache_and_retrieve_data fork_count_per_project.each do |project, count| @@ -2418,7 +2410,7 @@ class Project < ApplicationRecord end def mark_primary_write_location - ::Gitlab::Database::LoadBalancing::Sticking.mark_primary_write_location(:project, self.id) + self.class.sticking.mark_primary_write_location(:project, self.id) end def toggle_ci_cd_settings!(settings_attribute) @@ -2677,10 +2669,6 @@ class Project < ApplicationRecord ProjectStatistics.increment_statistic(self, statistic, delta) end - def merge_requests_author_approval - !!read_attribute(:merge_requests_author_approval) - end - def ci_forward_deployment_enabled? return false unless ci_cd_settings @@ -2734,15 +2722,9 @@ class Project < ApplicationRecord @topic_list = @topic_list.split(',') if @topic_list.instance_of?(String) @topic_list = @topic_list.map(&:strip).uniq.reject(&:empty?) - if @topic_list != self.topic_list || self.topics_acts_as_taggable.any? - self.topics_new.delete_all + if @topic_list != self.topic_list + self.topics.delete_all self.topics = @topic_list.map { |topic| Projects::Topic.find_or_create_by(name: topic) } - - # Remove old topics (ActsAsTaggableOn::Tag) - # Required during the `ActsAsTaggableOn::Tag -> Topic` migration - # TODO: remove in the further process of the migration - # https://gitlab.com/gitlab-org/gitlab/-/issues/335946 - self.topic_taggings.clear end @topic_list = nil @@ -2912,6 +2894,15 @@ class Project < ApplicationRecord def online_runners_with_tags @online_runners_with_tags ||= active_runners.with_tags.online end + + def ensure_project_namespace_in_sync + if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present? + project_namespace.name = name + project_namespace.path = path + project_namespace.parent = namespace + project_namespace.visibility_level = visibility_level + end + end end Project.prepend_mod_with('Project') diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index b2559636f32..24d892290a6 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class ProjectSetting < ApplicationRecord - include IgnorableColumns - - ignore_column :allow_editing_commit_messages, remove_with: '14.4', remove_after: '2021-09-10' - belongs_to :project, inverse_of: :project_setting enum squash_option: { diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 387732cf151..99cec647a98 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -31,7 +31,6 @@ class ProjectStatistics < ApplicationRecord scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) } - scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) } def total_repository_size repository_size + lfs_objects_size @@ -70,7 +69,7 @@ class ProjectStatistics < ApplicationRecord end def update_lfs_objects_size - self.lfs_objects_size = project.lfs_objects.sum(:size) + self.lfs_objects_size = LfsObject.joins(:lfs_objects_projects).where(lfs_objects_projects: { project_id: project.id }).sum(:size) end def update_uploads_size diff --git a/app/models/projects/project_topic.rb b/app/models/projects/project_topic.rb index d4b456ef482..7021a48646a 100644 --- a/app/models/projects/project_topic.rb +++ b/app/models/projects/project_topic.rb @@ -3,6 +3,6 @@ module Projects class ProjectTopic < ApplicationRecord belongs_to :project - belongs_to :topic + belongs_to :topic, counter_cache: :total_projects_count end end diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb index a17aa550edb..f3352ecc5ee 100644 --- a/app/models/projects/topic.rb +++ b/app/models/projects/topic.rb @@ -1,10 +1,30 @@ # frozen_string_literal: true +require 'carrierwave/orm/activerecord' + module Projects class Topic < ApplicationRecord + include Avatarable + include Gitlab::SQL::Pattern + validates :name, presence: true, uniqueness: true, length: { maximum: 255 } + validates :description, length: { maximum: 1024 } has_many :project_topics, class_name: 'Projects::ProjectTopic' has_many :projects, through: :project_topics + + scope :order_by_total_projects_count, -> { order(total_projects_count: :desc).order(id: :asc) } + scope :reorder_by_similarity, -> (search) do + order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [ + { column: arel_table['name'] } + ]) + reorder(order_expression.desc, arel_table['total_projects_count'].desc, arel_table['id']) + end + + class << self + def search(query) + fuzzy_search(query, [:name]) + end + end end end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 3d32144e0f8..b4e2d17c3e5 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -10,6 +10,8 @@ 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.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) diff --git a/app/models/release.rb b/app/models/release.rb index 0dd71c6ebfb..eac6346cc60 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -33,6 +33,7 @@ class Release < ApplicationRecord includes(:author, :evidences, :milestones, :links, :sorted_links, project: [:project_feature, :route, { namespace: :route }]) } + scope :with_milestones, -> { joins(:milestone_releases) } scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) } scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) } scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) } diff --git a/app/models/repository.rb b/app/models/repository.rb index f20b306c806..119d874a6e1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -732,7 +732,7 @@ class Repository end def tags_sorted_by(value) - return raw_repository.tags(sort_by: value) if Feature.enabled?(:gitaly_tags_finder, project, default_enabled: :yaml) + return raw_repository.tags(sort_by: value) if Feature.enabled?(:tags_finder_gitaly, project, default_enabled: :yaml) tags_ruby_sort(value) end @@ -1054,10 +1054,10 @@ class Repository end def squash(user, merge_request, message) - raw.squash(user, merge_request.id, start_sha: merge_request.diff_start_sha, - end_sha: merge_request.diff_head_sha, - author: merge_request.author, - message: message) + raw.squash(user, start_sha: merge_request.diff_start_sha, + end_sha: merge_request.diff_head_sha, + author: merge_request.author, + message: message) end def submodule_links diff --git a/app/models/upload.rb b/app/models/upload.rb index 0a4acdfc7e3..c1a3df82457 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -18,6 +18,8 @@ class Upload < ApplicationRecord before_save :calculate_checksum!, if: :foreground_checksummable? after_commit :schedule_checksum, if: :needs_checksum? + after_commit :update_project_statistics, on: [:create, :destroy], if: :project? + # 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 } @@ -67,7 +69,7 @@ class Upload < ApplicationRecord self.checksum = nil return unless needs_checksum? - self.checksum = self.class.hexdigest(absolute_path) + self.checksum = self.class.sha256_hexdigest(absolute_path) end # Initialize the associated Uploader class with current model @@ -161,6 +163,14 @@ class Upload < ApplicationRecord def mount_point super&.to_sym end + + def project? + model_type == "Project" + end + + def update_project_statistics + ProjectCacheWorker.perform_async(model_id, [], [:uploads_size]) + end end Upload.prepend_mod_with('Upload') diff --git a/app/models/user.rb b/app/models/user.rb index a4c8d606911..25a2588a6a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -112,7 +112,14 @@ class User < ApplicationRecord # # Namespace for personal projects - has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent + # TODO: change to `:namespace, -> { where(type: Namespaces::UserNamespace.sti_name}, class_name: 'Namespaces::UserNamespace'...` + # when working on issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070 + has_one :namespace, + -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) }, + dependent: :destroy, # rubocop:disable Cop/ActiveRecordDependent + foreign_key: :owner_id, + inverse_of: :owner, + autosave: true # rubocop:disable Cop/ActiveRecordDependent # Profile has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -229,9 +236,9 @@ class User < ApplicationRecord validates :first_name, length: { maximum: 127 } validates :last_name, length: { maximum: 127 } validates :email, confirmation: true - validates :notification_email, devise_email: true, allow_blank: true, if: ->(user) { user.notification_email != user.email } + validates :notification_email, devise_email: true, allow_blank: true validates :public_email, uniqueness: true, devise_email: true, allow_blank: true - validates :commit_email, devise_email: true, allow_blank: true, if: ->(user) { user.commit_email != user.email && user.commit_email != Gitlab::PrivateCommitEmail::TOKEN } + validates :commit_email, devise_email: true, allow_blank: true, unless: ->(user) { user.commit_email == Gitlab::PrivateCommitEmail::TOKEN } validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } @@ -316,6 +323,7 @@ class User < ApplicationRecord delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true + delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true accepts_nested_attributes_for :user_preference, update_only: true accepts_nested_attributes_for :user_detail, update_only: true @@ -449,11 +457,12 @@ class User < ApplicationRecord scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) } scope :with_no_activity, -> { active.where(last_activity_on: nil) } scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) } + scope :get_ids_by_username, -> (username) { where(username: username).pluck(:id) } def preferred_language read_attribute('preferred_language') || I18n.default_locale.to_s.presence_in(Gitlab::I18n.available_locales) || - 'en' + default_preferred_language end def active_for_authentication? @@ -728,7 +737,7 @@ class User < ApplicationRecord end def find_by_full_path(path, follow_redirects: false) - namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects) + namespace = Namespace.user_namespaces.find_by_full_path(path, follow_redirects: follow_redirects) namespace&.owner end @@ -1434,7 +1443,10 @@ class User < ApplicationRecord namespace.path = username if username_changed? namespace.name = name if name_changed? else - namespace = build_namespace(path: username, name: name) + # TODO: we should no longer need the `type` parameter once we can make the + # the `has_one :namespace` association use the correct class. + # issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070 + namespace = build_namespace(path: username, name: name, type: ::Namespaces::UserNamespace.sti_name) namespace.build_namespace_settings end end @@ -2003,6 +2015,11 @@ class User < ApplicationRecord private + # To enable JiHu repository to modify the default language options + def default_preferred_language + 'en' + end + def notification_email_verified return if notification_email.blank? || temp_oauth_email? @@ -2094,10 +2111,14 @@ class User < ApplicationRecord errors.add(:email, error) if error end + def signup_email_invalid_message + _('is not allowed for sign-up.') + end + def check_username_format return if username.blank? || Mime::EXTENSION_LOOKUP.keys.none? { |type| username.end_with?(".#{type}") } - errors.add(:username, _('ending with a file extension is not allowed.')) + errors.add(:username, _('ending with a reserved file extension is not allowed.')) end def groups_with_developer_maintainer_project_access diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 04bc29755f8..b990aedd4f8 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -36,7 +36,8 @@ class UserCallout < ApplicationRecord trial_status_reminder_d3: 35, # EE-only security_configuration_devops_alert: 36, # EE-only profile_personal_access_token_expiry: 37, # EE-only - terraform_notification_dismissed: 38 + terraform_notification_dismissed: 38, + security_newsletter_callout: 39 } validates :feature_name, diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index c41cff67864..6b0ed89c683 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -3,7 +3,10 @@ class UserDetail < ApplicationRecord extend ::Gitlab::Utils::Override include IgnorableColumns - ignore_columns %i[bio_html cached_markdown_version], remove_with: '13.6', remove_after: '2021-10-22' + + ignore_columns %i[bio_html cached_markdown_version], remove_with: '14.5', remove_after: '2021-10-22' + + REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze belongs_to :user @@ -14,6 +17,8 @@ class UserDetail < ApplicationRecord before_save :prevent_nil_bio + enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true + private def prevent_nil_bio diff --git a/app/models/user_highest_role.rb b/app/models/user_highest_role.rb index 4853fc3d248..dd5c85a5a87 100644 --- a/app/models/user_highest_role.rb +++ b/app/models/user_highest_role.rb @@ -3,7 +3,13 @@ class UserHighestRole < ApplicationRecord belongs_to :user, optional: false - validates :highest_access_level, allow_nil: true, inclusion: { in: Gitlab::Access.all_values } + validates :highest_access_level, allow_nil: true, inclusion: { in: ->(_) { self.allowed_values } } scope :with_highest_access_level, -> (highest_access_level) { where(highest_access_level: highest_access_level) } + + def self.allowed_values + Gitlab::Access.all_values + end end + +UserHighestRole.prepend_mod diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 337ae7125f3..7687430cfd1 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -23,7 +23,6 @@ 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 :timezone, value: Time.zone.tzinfo.name, 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 diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 5e255acd882..a4cc43d1f13 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -7,5 +7,18 @@ module Users self.table_name = 'user_credit_card_validations' belongs_to :user + + validates :holder_name, length: { maximum: 26 } + validates :last_digits, allow_nil: true, numericality: { + greater_than_or_equal_to: 0, less_than_or_equal_to: 9999 + } + + def similar_records + self.class.where( + expiration_date: expiration_date, + last_digits: last_digits, + holder_name: holder_name + ).order(credit_card_validated_at: :desc).includes(:user) + end end end |