diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-19 23:18:09 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-19 23:18:09 +0000 |
commit | 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch) | |
tree | dc4d20fe6064752c0bd323187252c77e0a89144b /app/models | |
parent | 9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff) | |
download | gitlab-ce-6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde.tar.gz |
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'app/models')
115 files changed, 1640 insertions, 935 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 9f634e70ff4..7dbc95c251b 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -83,21 +83,21 @@ class ActiveSession is_impersonated: request.session[:impersonator_id].present? ) - redis.pipelined do - redis.setex( + redis.pipelined do |pipeline| + pipeline.setex( key_name(user.id, session_private_id), expiry, active_user_session.dump ) # Deprecated legacy format - temporary to support mixed deployments - redis.setex( + pipeline.setex( key_name_v1(user.id, session_private_id), expiry, Marshal.dump(active_user_session) ) - redis.sadd( + pipeline.sadd( lookup_key_name(user.id), session_private_id ) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 579f2c38ae6..edb9a2053b1 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -10,11 +10,7 @@ class ApplicationSetting < ApplicationRecord ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22' ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22' - ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22' ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18' - ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22' - ignore_column :enforce_ssh_key_expiration, remove_with: '15.2', remove_after: '2022-07-22' - ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ @@ -221,6 +217,10 @@ class ApplicationSetting < ApplicationRecord numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: ::Gitlab::Pages::MAX_SIZE / 1.megabyte } + validates :max_pages_custom_domains_per_project, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :jobs_per_stage_page_size, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -406,6 +406,10 @@ class ApplicationSetting < ApplicationRecord validates :invisible_captcha_enabled, inclusion: { in: [true, false], message: _('must be a boolean value') } + validates :invitation_flow_enforcement, + allow_nil: false, + inclusion: { in: [true, false], message: _('must be a boolean value') } + Gitlab::SSHPublicKey.supported_types.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -621,6 +625,10 @@ class ApplicationSetting < ApplicationRecord validates :inactive_projects_send_warning_email_after_months, numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months } + validates :cube_api_base_url, + addressable_url: { allow_localhost: true, allow_local_network: false }, + allow_blank: true + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, @@ -658,6 +666,7 @@ class ApplicationSetting < ApplicationRecord attr_encrypted :database_grafana_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :arkose_labs_public_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) + attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm validates :disable_feed_token, inclusion: { in: [true, false], message: _('must be a boolean value') } diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 3fda8693a58..323d759510e 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -75,9 +75,9 @@ module Ci def self.clone_accessors %i[pipeline project ref tag options name - allow_failure stage stage_id stage_idx + allow_failure stage stage_idx yaml_variables when description needs_attributes - scheduling_type].freeze + scheduling_type ci_stage partition_id].freeze end def inherit_status_from_downstream!(pipeline) @@ -183,6 +183,10 @@ module Ci false end + def prevent_rollback_deployment? + false + end + def expanded_environment_name end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bf8817e6e78..4e58f877217 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -11,7 +11,7 @@ module Ci include Presentable include Importable include Ci::HasRef - include HasDeploymentName + include Ci::TrackEnvironmentUsage extend ::Gitlab::Utils::Override @@ -34,7 +34,7 @@ module Ci DEPLOYMENT_NAMES = %w[deploy release rollout].freeze - has_one :deployment, as: :deployable, class_name: 'Deployment' + has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id @@ -194,7 +194,7 @@ module Ci after_save :stick_build_if_status_changed after_create unless: :importing? do |build| - run_after_commit { build.feature_flagged_execute_hooks } + run_after_commit { build.execute_hooks } end class << self @@ -214,10 +214,11 @@ module Ci def clone_accessors %i[pipeline project ref tag options name - allow_failure stage stage_id stage_idx trigger_request + allow_failure stage stage_idx trigger_request yaml_variables when environment coverage_regex description tag_list protected needs_attributes - job_variables_attributes resource_group scheduling_type].freeze + job_variables_attributes resource_group scheduling_type + ci_stage partition_id].freeze end end @@ -285,7 +286,7 @@ module Ci build.run_after_commit do BuildQueueWorker.perform_async(id) - build.feature_flagged_execute_hooks + build.execute_hooks end end @@ -313,7 +314,7 @@ module Ci build.run_after_commit do build.ensure_persistent_ref - build.feature_flagged_execute_hooks + build.execute_hooks end end @@ -442,6 +443,15 @@ module Ci manual? && starts_environment? && deployment&.blocked? end + def prevent_rollback_deployment? + strong_memoize(:prevent_rollback_deployment) do + Feature.enabled?(:prevent_outdated_deployment_jobs, project) && + starts_environment? && + project.ci_forward_deployment_enabled? && + deployment&.older_than_last_successful_deployment? + end + end + def schedulable? self.when == 'delayed' && options[:start_in].present? end @@ -703,25 +713,7 @@ module Ci end def has_test_reports? - job_artifacts.test_reports.exists? - end - - def has_old_trace? - old_trace.present? - end - - def trace=(data) - raise NotImplementedError - end - - def old_trace - read_attribute(:trace) - end - - def erase_old_trace! - return unless has_old_trace? - - update_column(:trace, nil) + job_artifacts.of_report_type(:test).exists? end def ensure_trace_metadata! @@ -780,14 +772,6 @@ module Ci pending? && !any_runners_online? end - def feature_flagged_execute_hooks - if Feature.enabled?(:execute_build_hooks_inline, project) - execute_hooks - else - BuildHooksWorker.perform_async(self) - end - end - def execute_hooks return unless project return if user&.blocked? @@ -823,41 +807,6 @@ module Ci end end - def erase_erasable_artifacts! - if project.refreshing_build_artifacts_size? - Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh( - method: 'Ci::Build#erase_erasable_artifacts!', - project_id: project_id - ) - end - - destroyed_artifacts = job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll - - Gitlab::Ci::Artifacts::Logger.log_deleted(destroyed_artifacts, 'Ci::Build#erase_erasable_artifacts!') - - destroyed_artifacts - end - - def erase(opts = {}) - return false unless erasable? - - if project.refreshing_build_artifacts_size? - Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh( - method: 'Ci::Build#erase', - project_id: project_id - ) - end - - # TODO: We should use DestroyBatchService here - # See https://gitlab.com/gitlab-org/gitlab/-/issues/369132 - destroyed_artifacts = job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll - - Gitlab::Ci::Artifacts::Logger.log_deleted(destroyed_artifacts, 'Ci::Build#erase') - - erase_trace! - update_erased!(opts[:erased_by]) - end - def erasable? complete? && (artifacts? || has_job_artifacts? || has_trace?) end @@ -1004,15 +953,11 @@ module Ci end def collect_test_reports!(test_reports) - test_reports.get_suite(test_suite_name).tap do |test_suite| - each_report(Ci::JobArtifact.file_types_for_report(:test)) do |file_type, blob| - Gitlab::Ci::Parsers.fabricate!(file_type).parse!( - blob, - test_suite, - job: self - ) - end + each_report(Ci::JobArtifact.file_types_for_report(:test)) do |file_type, blob| + Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_reports, job: self) end + + test_reports end def collect_accessibility_reports!(accessibility_report) @@ -1154,18 +1099,6 @@ module Ci .include?(exit_code) end - def track_deployment_usage - Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) if user_id.present? && count_user_deployment? - end - - def track_verify_usage - Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_verify_environment_job', user_id) if user_id.present? && count_user_verification? - end - - def count_user_verification? - has_environment? && environment_action == 'verify' - end - def each_report(report_types) job_artifacts_for_types(report_types).each do |report_artifact| report_artifact.each_blob do |blob| @@ -1189,6 +1122,14 @@ module Ci job_artifacts.map(&:file_type) end + def test_suite_name + if matrix_build? + name + else + group_name + end + end + protected def run_status_commit_hooks! @@ -1199,14 +1140,6 @@ module Ci private - def test_suite_name - if matrix_build? - name - else - group_name - end - end - def matrix_build? options.dig(:parallel, :matrix).present? end @@ -1245,14 +1178,6 @@ module Ci job_artifacts.select { |artifact| artifact.file_type.in?(report_types) } end - def erase_trace! - trace.erase! - end - - def update_erased!(user = nil) - self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil) - end - def environment_url options&.dig(:environment, :url) || persisted_environment&.external_url end @@ -1298,7 +1223,7 @@ module Ci end def observe_report_types - return unless ::Gitlab.com? && Feature.enabled?(:report_artifact_build_completed_metrics_on_build_completion) + return unless ::Gitlab.com? report_types = options&.dig(:artifacts, :reports)&.keys || [] diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 5fc21ba3f28..3bdf2f90acb 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -9,7 +9,6 @@ module Ci include Presentable include ChronicDurationAttribute include Gitlab::Utils::StrongMemoize - include IgnorableColumns self.table_name = 'ci_builds_metadata' @@ -39,8 +38,6 @@ module Ci job_timeout_source: 4 } - ignore_columns :runner_features, remove_with: '15.1', remove_after: '2022-05-22' - def update_timeout_state timeout = timeout_with_highest_precedence diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb index befa935e750..e810bb3f229 100644 --- a/app/models/ci/freeze_period_status.rb +++ b/app/models/ci/freeze_period_status.rb @@ -13,32 +13,16 @@ module Ci end def within_freeze_period?(period) - # previous_freeze_end, ..., previous_freeze_start, ..., NOW, ..., next_freeze_end, ..., next_freeze_start - # Current time is within a freeze period if - # it falls between a previous freeze start and next freeze end - start_freeze = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone) - end_freeze = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone) - - previous_freeze_start = previous_time(start_freeze) - previous_freeze_end = previous_time(end_freeze) - next_freeze_start = next_time(start_freeze) - next_freeze_end = next_time(end_freeze) - - previous_freeze_end < previous_freeze_start && - previous_freeze_start <= time_zone_now && - time_zone_now <= next_freeze_end && - next_freeze_end < next_freeze_start - end + start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone) + end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone) - private + start_freeze = start_freeze_cron.previous_time_from(time_zone_now) + end_freeze = end_freeze_cron.next_time_from(start_freeze) - def previous_time(cron_parser) - cron_parser.previous_time_from(time_zone_now) + start_freeze <= time_zone_now && time_zone_now <= end_freeze end - def next_time(cron_parser) - cron_parser.next_time_from(time_zone_now) - end + private def time_zone_now @time_zone_now ||= Time.zone.now diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 71d33f0bb63..922806a21c3 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -2,6 +2,7 @@ module Ci class JobArtifact < Ci::ApplicationRecord + include Ci::Partitionable include IgnorableColumns include AfterCommitQueue include ObjectStorage::BackgroundMove @@ -9,6 +10,7 @@ module Ci include UsageStatistics include Sortable include Artifactable + include Lockable include FileStoreMounter include EachBatch include Gitlab::Utils::StrongMemoize @@ -22,8 +24,7 @@ module Ci accessibility: %w[accessibility], coverage: %w[cobertura], codequality: %w[codequality], - terraform: %w[terraform], - sbom: %w[cyclonedx] + terraform: %w[terraform] }.freeze DEFAULT_FILE_NAMES = { @@ -54,7 +55,7 @@ module Ci requirements: 'requirements.json', coverage_fuzzing: 'gl-coverage-fuzzing.json', api_fuzzing: 'gl-api-fuzzing-report.json', - cyclonedx: 'gl-sbom.cdx.zip' + cyclonedx: 'gl-sbom.cdx.json' }.freeze INTERNAL_TYPES = { @@ -72,6 +73,7 @@ module Ci cobertura: :gzip, cluster_applications: :gzip, # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094 lsif: :zip, + cyclonedx: :gzip, # Security reports and license scanning reports are raw artifacts # because they used to be fetched by the frontend, but this is not the case anymore. @@ -94,8 +96,7 @@ module Ci terraform: :raw, requirements: :raw, coverage_fuzzing: :raw, - api_fuzzing: :raw, - cyclonedx: :zip + api_fuzzing: :raw }.freeze DOWNLOADABLE_TYPES = %w[ @@ -134,14 +135,16 @@ module Ci mount_file_store_uploader JobArtifactUploader, skip_store_file: true + before_save :set_size, if: :file_changed? after_save :store_file_in_transaction!, unless: :store_after_commit? after_commit :store_file_after_transaction!, on: [:create, :update], if: :store_after_commit? + validates :job, presence: true validates :file_format, presence: true, unless: :trace?, on: :create validate :validate_file_format!, unless: :trace?, on: :create - before_save :set_size, if: :file_changed? update_project_statistics project_statistics_name: :build_artifacts_size + partitionable scope: :job scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } @@ -160,12 +163,6 @@ module Ci where(file_type: types) end - REPORT_FILE_TYPES.each do |report_type, file_types| - scope "#{report_type}_reports", -> do - with_file_types(file_types) - end - end - scope :all_reports, -> do with_file_types(REPORT_TYPES.keys.map(&:to_s)) end @@ -229,25 +226,20 @@ module Ci hashed_path: 2 } - # `locked` will be populated from the source of truth on Ci::Pipeline - # in order to clean up expired job artifacts in a performant way. - # The values should be the same as `Ci::Pipeline.lockeds` with the - # additional value of `unknown` to indicate rows that have not - # yet been populated from the parent Ci::Pipeline - enum locked: { - unlocked: 0, - artifacts_locked: 1, - unknown: 2 - }, _prefix: :artifact - def validate_file_format! unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym errors.add(:base, _('Invalid file format with specified file type')) end end + def self.of_report_type(report_type) + file_types = file_types_for_report(report_type) + + with_file_types(file_types) + end + def self.file_types_for_report(report_type) - REPORT_FILE_TYPES.fetch(report_type) + REPORT_FILE_TYPES.fetch(report_type) { raise ArgumentError, "Unrecognized report type: #{report_type}" } end def self.associated_file_types_for(file_type) diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index 3a5765aa00c..26a49d6a730 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -30,10 +30,7 @@ module Ci end def all_projects - Project.from_union([ - Project.id_in(source_project), - Project.id_in(target_project_ids) - ], remove_duplicates: false) + Project.from_union(target_projects, remove_duplicates: false) end private @@ -41,6 +38,13 @@ module Ci def target_project_ids Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id) end + + def target_projects + [ + Project.id_in(source_project), + Project.id_in(target_project_ids) + ] + end end end end diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb index e8f08db597f..5ea51fbe0a7 100644 --- a/app/models/ci/namespace_mirror.rb +++ b/app/models/ci/namespace_mirror.rb @@ -43,20 +43,6 @@ module Ci upsert({ namespace_id: event.namespace_id, traversal_ids: traversal_ids }, unique_by: :namespace_id) - - # It won't be necessary once we remove `sync_traversal_ids`. - # More info: https://gitlab.com/gitlab-org/gitlab/-/issues/347541 - sync_children_namespaces!(event.namespace_id, traversal_ids) - end - - private - - def sync_children_namespaces!(namespace_id, traversal_ids) - by_group_and_descendants(namespace_id) - .where.not(namespace_id: namespace_id) - .update_all( - "traversal_ids = ARRAY[#{sanitize_sql(traversal_ids.join(','))}]::int[] || traversal_ids[array_position(traversal_ids, #{sanitize_sql(namespace_id)}) + 1:]" - ) end end end diff --git a/app/models/ci/partition.rb b/app/models/ci/partition.rb new file mode 100644 index 00000000000..d773038df01 --- /dev/null +++ b/app/models/ci/partition.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Ci + class Partition < Ci::ApplicationRecord + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a94330270e2..1e328c3c573 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -2,6 +2,7 @@ module Ci class Pipeline < Ci::ApplicationRecord + include Ci::Partitionable include Ci::HasStatus include Importable include AfterCommitQueue @@ -31,7 +32,7 @@ module Ci sha_attribute :source_sha sha_attribute :target_sha - + partitionable scope: ->(_) { Ci::Pipeline.current_partition_value } # Ci::CreatePipelineService returns Ci::Pipeline so this is the only place # where we can pass additional information from the service. This accessor # is used for storing the processed metadata for linting purposes. @@ -296,6 +297,12 @@ module Ci end end + after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| + pipeline.run_after_commit do + ::Ci::JobArtifacts::TrackArtifactReportWorker.perform_async(pipeline.id) + end + end + after_transition any => ::Ci::Pipeline.stopped_statuses do |pipeline| pipeline.run_after_commit do pipeline.persistent_ref.delete @@ -422,6 +429,10 @@ module Ci end def self.jobs_count_in_alive_pipelines + created_after(24.hours.ago).alive.joins(:statuses).count + end + + def self.builds_count_in_alive_pipelines created_after(24.hours.ago).alive.joins(:builds).count end @@ -472,8 +483,12 @@ module Ci @auto_devops_pipelines_completed_total ||= Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines') end + def self.current_partition_value + 100 + end + def uses_needs? - builds.where(scheduling_type: :dag).any? + processables.where(scheduling_type: :dag).any? end def stages_count @@ -605,7 +620,7 @@ module Ci if cascade_to_children # cancel any bridges that could spin up new child pipelines - cancel_jobs(bridges_in_self_and_descendants.cancelable, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id) + cancel_jobs(bridges_in_self_and_project_descendants.cancelable, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id) cancel_children(auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id, execute_async: execute_async) end end @@ -937,26 +952,26 @@ module Ci ).base_and_descendants.select(:id) end - def build_with_artifacts_in_self_and_descendants(name) - builds_in_self_and_descendants + def build_with_artifacts_in_self_and_project_descendants(name) + builds_in_self_and_project_descendants .ordered_by_pipeline # find job in hierarchical order .with_downloadable_artifacts .find_by_name(name) end - def builds_in_self_and_descendants - Ci::Build.latest.where(pipeline: self_and_descendants) + def builds_in_self_and_project_descendants + Ci::Build.latest.where(pipeline: self_and_project_descendants) end - def bridges_in_self_and_descendants - Ci::Bridge.latest.where(pipeline: self_and_descendants) + def bridges_in_self_and_project_descendants + Ci::Bridge.latest.where(pipeline: self_and_project_descendants) end - def environments_in_self_and_descendants(deployment_status: nil) + def environments_in_self_and_project_descendants(deployment_status: nil) # 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) + builds_in_self_and_project_descendants.joins(:metadata) .where.not('ci_builds_metadata.expanded_environment_name' => nil) .distinct('ci_builds_metadata.expanded_environment_name') .limit(100) @@ -971,17 +986,22 @@ module Ci end # With multi-project and parent-child pipelines - def all_pipelines_in_hierarchy + def self_and_downstreams + object_hierarchy.base_and_descendants + end + + # With multi-project and parent-child pipelines + def upstream_and_all_downstreams object_hierarchy.all_objects end # With only parent-child pipelines - def self_and_ancestors + def self_and_project_ancestors object_hierarchy(project_condition: :same).base_and_ancestors end # With only parent-child pipelines - def self_and_descendants + def self_and_project_descendants object_hierarchy(project_condition: :same).base_and_descendants end @@ -990,8 +1010,8 @@ module Ci object_hierarchy(project_condition: :same).descendants end - def self_and_descendants_complete? - self_and_descendants.all?(&:complete?) + def self_and_project_descendants_complete? + self_and_project_descendants.all?(&:complete?) end # Follow the parent-child relationships and return the top-level parent @@ -1006,7 +1026,12 @@ module Ci # Follow the upstream pipeline relationships, regardless of multi-project or # parent-child, and return the top-level ancestor. def upstream_root - object_hierarchy.base_and_ancestors(hierarchy_order: :desc).first + @upstream_root ||= object_hierarchy.base_and_ancestors(hierarchy_order: :desc).first + end + + # Applies to all parent-child and multi-project pipelines + def complete_hierarchy_count + upstream_root.self_and_downstreams.count end def bridge_triggered? @@ -1052,11 +1077,11 @@ module Ci end def latest_test_report_builds - latest_report_builds(Ci::JobArtifact.test_reports).preload(:project, :metadata) + latest_report_builds(Ci::JobArtifact.of_report_type(:test)).preload(:project, :metadata) end - def latest_report_builds_in_self_and_descendants(reports_scope = ::Ci::JobArtifact.all_reports) - builds_in_self_and_descendants.with_artifacts(reports_scope) + def latest_report_builds_in_self_and_project_descendants(reports_scope = ::Ci::JobArtifact.all_reports) + builds_in_self_and_project_descendants.with_artifacts(reports_scope) end def builds_with_coverage @@ -1068,10 +1093,14 @@ module Ci end def has_reports?(reports_scope) + latest_report_builds(reports_scope).exists? + end + + def complete_and_has_reports?(reports_scope) if Feature.enabled?(:mr_show_reports_immediately, project, type: :development) latest_report_builds(reports_scope).exists? else - complete? && latest_report_builds(reports_scope).exists? + complete? && has_reports?(reports_scope) end end @@ -1084,7 +1113,7 @@ module Ci end def can_generate_codequality_reports? - has_reports?(Ci::JobArtifact.codequality_reports) + complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality)) end def test_report_summary @@ -1103,7 +1132,7 @@ module Ci def accessibility_reports Gitlab::Ci::Reports::AccessibilityReports.new.tap do |accessibility_reports| - latest_report_builds(Ci::JobArtifact.accessibility_reports).each do |build| + latest_report_builds(Ci::JobArtifact.of_report_type(:accessibility)).each do |build| build.collect_accessibility_reports!(accessibility_reports) end end @@ -1111,7 +1140,7 @@ module Ci def codequality_reports Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports| - latest_report_builds(Ci::JobArtifact.codequality_reports).each do |build| + latest_report_builds(Ci::JobArtifact.of_report_type(:codequality)).each do |build| build.collect_codequality_reports!(codequality_reports) end end @@ -1119,7 +1148,7 @@ module Ci def terraform_reports ::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports| - latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build| + latest_report_builds(::Ci::JobArtifact.of_report_type(:terraform)).each do |build| build.collect_terraform_reports!(terraform_reports) end end @@ -1307,7 +1336,7 @@ module Ci def has_test_reports? strong_memoize(:has_test_reports) do - has_reports?(::Ci::JobArtifact.test_reports) + has_reports?(::Ci::JobArtifact.of_report_type(:test)) end end diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb index cdc3d69f754..6d22a875aab 100644 --- a/app/models/ci/pipeline_artifact.rb +++ b/app/models/ci/pipeline_artifact.rb @@ -7,6 +7,7 @@ module Ci include UpdateProjectStatistics include Artifactable include FileStoreMounter + include Lockable include Presentable FILE_SIZE_LIMIT = 10.megabytes.freeze @@ -52,7 +53,7 @@ module Ci find_by(file_type: file_type) end - def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:) + def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:, locked: :unknown) transaction do pipeline.pipeline_artifacts.find_by_file_type(file_type)&.destroy! @@ -62,7 +63,8 @@ module Ci size: size, file: file, file_format: REPORT_TYPES[file_type], - expire_at: EXPIRATION_DATE.from_now + expire_at: EXPIRATION_DATE.from_now, + locked: locked ) end rescue ActiveRecord::ActiveRecordError => err diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 3dca77af051..6e4418bc360 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -2,13 +2,16 @@ module Ci class PipelineVariable < Ci::ApplicationRecord + include Ci::Partitionable include Ci::HasVariable belongs_to :pipeline + partitionable scope: :pipeline + alias_attribute :secret_value, :value - validates :key, presence: true + validates :key, :pipeline, presence: true def hook_attrs { key: key, value: value } diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index a2ff49077be..09dc9d4bce1 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -3,6 +3,7 @@ module Ci class Processable < ::CommitStatus include Gitlab::Utils::StrongMemoize + include FromUnion extend ::Gitlab::Utils::Override has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 6c3754d84d0..28d9edcc135 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -8,15 +8,12 @@ module Ci include ChronicDurationAttribute include FromUnion include TokenAuthenticatable - include IgnorableColumns include FeatureGate include Gitlab::Utils::StrongMemoize include TaggableQueries include Presentable include EachBatch - ignore_column :semver, remove_with: '15.4', remove_after: '2022-08-22' - add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced? enum access_level: { @@ -351,6 +348,12 @@ module Ci end end + def owner_project + return unless project_type? + + runner_projects.order(:id).first.project + end + def belongs_to_one_project? runner_projects.count == 1 end @@ -359,14 +362,6 @@ module Ci runner_projects.limit(2).count(:all) > 1 end - def assigned_to_group? - runner_namespaces.any? - end - - def assigned_to_project? - runner_projects.any? - end - def match_build_if_online?(build) active? && online? && matches_build?(build) end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index f03d1e96a4b..46a9e3f6494 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -2,22 +2,31 @@ module Ci class Stage < Ci::ApplicationRecord + include Ci::Partitionable include Importable include Ci::HasStatus include Gitlab::OptimisticLocking include Presentable + partitionable scope: :pipeline + enum status: Ci::HasStatus::STATUSES_ENUM belongs_to :project belongs_to :pipeline - has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id - has_many :latest_statuses, -> { ordered.latest }, class_name: 'CommitStatus', foreign_key: :stage_id - has_many :retried_statuses, -> { ordered.retried }, class_name: 'CommitStatus', foreign_key: :stage_id - has_many :processables, class_name: 'Ci::Processable', foreign_key: :stage_id - has_many :builds, foreign_key: :stage_id - has_many :bridges, foreign_key: :stage_id + has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id, inverse_of: :ci_stage + has_many :latest_statuses, -> { ordered.latest }, + class_name: 'CommitStatus', + foreign_key: :stage_id, + inverse_of: :ci_stage + has_many :retried_statuses, -> { ordered.retried }, + class_name: 'CommitStatus', + foreign_key: :stage_id, + inverse_of: :ci_stage + has_many :processables, class_name: 'Ci::Processable', foreign_key: :stage_id, inverse_of: :ci_stage + has_many :builds, foreign_key: :stage_id, inverse_of: :ci_stage + has_many :bridges, foreign_key: :stage_id, inverse_of: :ci_stage scope :ordered, -> { order(position: :asc) } scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) } diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index c4db4754c52..1092b9c9564 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -6,6 +6,8 @@ module Ci include Limitable include IgnorableColumns + TRIGGER_TOKEN_PREFIX = 'glptt-' + ignore_column :ref, remove_with: '15.4', remove_after: '2022-08-22' self.limit_name = 'pipeline_triggers' @@ -22,7 +24,7 @@ module Ci before_validation :set_default_values def set_default_values - self.token = SecureRandom.hex(15) if self.token.blank? + self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank? end def last_trigger_request @@ -34,7 +36,7 @@ module Ci end def short_token - token[0...4] if token.present? + token.delete_prefix(TRIGGER_TOKEN_PREFIX)[0...4] if token.present? end def can_access_project? diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 3a8c314efe4..27550616002 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -16,14 +16,10 @@ module Clusters include ::Clusters::Concerns::ApplicationData include AfterCommitQueue include UsageStatistics - include IgnorableColumns default_value_for :ingress_type, :nginx default_value_for :version, VERSION - ignore_column :modsecurity_enabled, remove_with: '14.2', remove_after: '2021-07-22' - ignore_column :modsecurity_mode, remove_with: '14.2', remove_after: '2021-07-22' - enum ingress_type: { nginx: 1 } diff --git a/app/models/commit.rb b/app/models/commit.rb index bd60f02b532..54de45ebba7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -133,6 +133,22 @@ class Commit def parent_class ::Project end + + def build_from_sidekiq_hash(project, hash) + hash = hash.dup + date_suffix = '_date' + + # When processing Sidekiq payloads various timestamps are stored as Strings. + # Commit in turn expects Time-like instances upon input, so we have to + # manually parse these values. + hash.each do |key, value| + if key.to_s.end_with?(date_suffix) && value.is_a?(String) + hash[key] = Time.zone.parse(value) + end + end + + from_hash(hash, project) + end end attr_accessor :raw diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index afe4927ee73..05a258e6e26 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class CommitStatus < Ci::ApplicationRecord + include Ci::Partitionable include Ci::HasStatus include Importable include AfterCommitQueue @@ -11,13 +12,14 @@ class CommitStatus < Ci::ApplicationRecord include IgnorableColumns self.table_name = 'ci_builds' - - ignore_column :token, remove_with: '15.4', remove_after: '2022-08-22' + partitionable scope: :pipeline + ignore_column :trace, remove_with: '15.6', remove_after: '2022-10-22' belongs_to :user belongs_to :project belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' + belongs_to :ci_stage, class_name: 'Ci::Stage', foreign_key: :stage_id has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build @@ -318,6 +320,10 @@ class CommitStatus < Ci::ApplicationRecord Gitlab::EtagCaching::Store.new.touch(job_path) end + def stage_name + ci_stage&.name + end + private def unrecoverable_failure? diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable.rb index 8240f9bd6ea..1566c53217d 100644 --- a/app/models/concerns/approvable_base.rb +++ b/app/models/concerns/approvable.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module ApprovableBase +module Approvable extend ActiveSupport::Concern include FromUnion @@ -27,12 +27,11 @@ module ApprovableBase scope :not_approved_by_users_with_usernames, -> (usernames) do users = User.where(username: usernames).select(:id) - self_table = self.arel_table app_table = Approval.arel_table where( Approval.where(approvals: { user_id: users }) - .where(app_table[:merge_request_id].eq(self_table[:id])) + .where(app_table[:merge_request_id].eq(arel_table[:id])) .select('true') .arel.exists.not ) @@ -48,7 +47,7 @@ module ApprovableBase def approved_by?(user) return false unless user - approved_by_users.include?(user) + approvals.where(user: user).any? end def can_be_approved_by?(user) @@ -59,3 +58,5 @@ module ApprovableBase user && approved_by?(user) && user.can?(:approve_merge_request, self) end end + +Approvable.prepend_mod diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index ee8e98ec1bf..3fdbd6a8789 100644 --- a/app/models/concerns/ci/artifactable.rb +++ b/app/models/concerns/ci/artifactable.rb @@ -10,8 +10,17 @@ module Ci STORE_COLUMN = :file_store NotSupportedAdapterError = Class.new(StandardError) FILE_FORMAT_ADAPTERS = { + # While zip is a streamable file format, performing streaming + # reads requires that each entry in the zip has certain headers + # present at the front of the entry. These headers are OPTIONAL + # according to the file format specification. GitLab Runner uses + # Go's `archive/zip` to create zip archives, which does not include + # these headers. Go maintainers have expressed that they don't intend + # to support them: https://github.com/golang/go/issues/23301#issuecomment-363240781 + # + # If you need GitLab to be able to read Artifactables, store them in + # raw or gzip format instead of zip. gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream, - zip: Gitlab::Ci::Build::Artifacts::Adapters::ZipStream, raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream }.freeze diff --git a/app/models/concerns/ci/has_deployment_name.rb b/app/models/concerns/ci/has_deployment_name.rb deleted file mode 100644 index 887653e846e..00000000000 --- a/app/models/concerns/ci/has_deployment_name.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Ci - module HasDeploymentName - extend ActiveSupport::Concern - - def count_user_deployment? - deployment_name? - end - - def deployment_name? - self.class::DEPLOYMENT_NAMES.any? { |n| name.downcase.include?(n) } - end - end -end diff --git a/app/models/concerns/ci/lockable.rb b/app/models/concerns/ci/lockable.rb new file mode 100644 index 00000000000..31ba93775e2 --- /dev/null +++ b/app/models/concerns/ci/lockable.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Ci + module Lockable + extend ActiveSupport::Concern + + included do + # `locked` will be populated from the source of truth on Ci::Pipeline + # in order to clean up expired job artifacts in a performant way. + # The values should be the same as `Ci::Pipeline.lockeds` with the + # additional value of `unknown` to indicate rows that have not + # yet been populated from the parent Ci::Pipeline + enum locked: { + unlocked: 0, + artifacts_locked: 1, + unknown: 2 + }, _prefix: :artifact + end + end +end diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index 8c3a05c23f0..71b26b70bbf 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -34,7 +34,7 @@ module Ci end def ensure_metadata - metadata || build_metadata(project: project) + metadata || build_metadata(project: project, partition_id: partition_id) end def degenerated? diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb new file mode 100644 index 00000000000..710ee1ba64f --- /dev/null +++ b/app/models/concerns/ci/partitionable.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Ci + ## + # This module implements a way to set the `partion_id` value on a dependent + # resource from a parent record. + # Usage: + # + # class PipelineVariable < Ci::ApplicationRecord + # include Ci::Partitionable + # + # belongs_to :pipeline + # partitionable scope: :pipeline + # # Or + # partitionable scope: ->(record) { record.partition_value } + # + # + module Partitionable + extend ActiveSupport::Concern + include ::Gitlab::Utils::StrongMemoize + + included do + before_validation :set_partition_id, on: :create + validates :partition_id, presence: true + + def set_partition_id + return if partition_id_changed? && partition_id.present? + return unless partition_scope_value + + self.partition_id = partition_scope_value + end + end + + class_methods do + private + + def partitionable(scope:) + define_method(:partition_scope_value) do + strong_memoize(:partition_scope_value) do + record = scope.to_proc.call(self) + record.respond_to?(:partition_id) ? record.partition_id : record + end + end + end + end + end +end diff --git a/app/models/concerns/ci/track_environment_usage.rb b/app/models/concerns/ci/track_environment_usage.rb new file mode 100644 index 00000000000..45d9cdeeb59 --- /dev/null +++ b/app/models/concerns/ci/track_environment_usage.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Ci + module TrackEnvironmentUsage + extend ActiveSupport::Concern + + def track_deployment_usage + return unless user_id.present? && count_user_deployment? + + Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) + end + + def track_verify_environment_usage + return unless user_id.present? && verifies_environment? + + Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_verify_environment_job', user_id) + end + + def verifies_environment? + has_environment? && environment_action == 'verify' + end + + def count_user_deployment? + deployment_name? + end + + def deployment_name? + self.class::DEPLOYMENT_NAMES.any? { |n| name.downcase.include?(n) } + end + end +end diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb index 65cf3246d11..64d178b7507 100644 --- a/app/models/concerns/counter_attribute.rb +++ b/app/models/concerns/counter_attribute.rb @@ -65,6 +65,10 @@ module CounterAttribute def counter_attribute_after_flush(&callback) after_flush_callbacks << callback end + + def counter_attribute_enabled?(attribute) + counter_attributes.include?(attribute) + end end # This method must only be called by FlushCounterIncrementsWorker @@ -103,16 +107,14 @@ module CounterAttribute end def delayed_increment_counter(attribute, increment) + raise ArgumentError, "#{attribute} is not a counter attribute" unless counter_attribute_enabled?(attribute) + return if increment == 0 run_after_commit_or_now do - if counter_attribute_enabled?(attribute) - increment_counter(attribute, increment) + increment_counter(attribute, increment) - FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute) - else - legacy_increment!(attribute, increment) - end + FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute) end true @@ -157,7 +159,7 @@ module CounterAttribute end def counter_attribute_enabled?(attribute) - self.class.counter_attributes.include?(attribute) + self.class.counter_attribute_enabled?(attribute) end private @@ -168,10 +170,6 @@ module CounterAttribute end end - def legacy_increment!(attribute, increment) - increment!(attribute, increment) - end - def unsafe_update_counters(id, increments) self.class.update_counters(id, increments) end diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb index ecb120d8013..9de2da5aac3 100644 --- a/app/models/concerns/enums/ci/commit_status.rb +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -19,7 +19,7 @@ module Enums unmet_prerequisites: 10, scheduler_failure: 11, data_integrity_failure: 12, - forward_deployment_failure: 13, + forward_deployment_failure: 13, # Deprecated in favor of failed_outdated_deployment_job. user_blocked: 14, project_deleted: 15, ci_quota_exceeded: 16, @@ -29,6 +29,7 @@ module Enums builds_disabled: 20, environment_creation_failure: 21, deployment_rejected: 22, + failed_outdated_deployment_job: 23, protected_environment_failure: 1_000, insufficient_bridge_permissions: 1_001, downstream_bridge_project_not_found: 1_002, @@ -39,7 +40,8 @@ module Enums downstream_pipeline_creation_failed: 1_007, secrets_provider_not_found: 1_008, reached_max_descendant_pipelines_depth: 1_009, - ip_restriction_failure: 1_010 + ip_restriction_failure: 1_010, + reached_max_pipeline_hierarchy_size: 1_011 } end end diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb index 71c86bab136..a8227363a22 100644 --- a/app/models/concerns/enums/internal_id.rb +++ b/app/models/concerns/enums/internal_id.rb @@ -16,7 +16,8 @@ module Enums alert_management_alerts: 8, sprints: 9, # iterations design_management_designs: 10, - incident_management_oncall_schedules: 11 + incident_management_oncall_schedules: 11, + ml_experiments: 12 } end end diff --git a/app/models/concerns/from_set_operator.rb b/app/models/concerns/from_set_operator.rb index ce3a83e9fa1..56b788eb1ab 100644 --- a/app/models/concerns/from_set_operator.rb +++ b/app/models/concerns/from_set_operator.rb @@ -10,7 +10,9 @@ module FromSetOperator raise "Trying to redefine method '#{method(method_name)}'" if methods.include?(method_name) - define_method(method_name) do |members, remove_duplicates: true, remove_order: true, alias_as: table_name| + define_method(method_name) do |*members, remove_duplicates: true, remove_order: true, alias_as: table_name| + members = flatten_ar_array(members) + operator_sql = if members.any? operator.new(members, remove_duplicates: remove_duplicates, remove_order: remove_order).to_sql @@ -20,5 +22,26 @@ module FromSetOperator from(Arel.sql("(#{operator_sql}) #{alias_as}")) end + + # Array#flatten with ActiveRecord::Relation items will load the ActiveRecord::Relation. + # Therefore we need to roll our own flatten method. + unless method_defined?(:flatten_ar_array) # rubocop:disable Style/GuardClause + define_method :flatten_ar_array do |ary| + arrays = ary.dup + result = [] + + until arrays.empty? + item = arrays.shift + if item.is_a?(Array) + arrays.concat(item.dup) + else + result.push(item) + end + end + + result + end + private :flatten_ar_array + end end end diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb index 142e62bb501..1ecddc015ab 100644 --- a/app/models/concerns/integrations/slack_mattermost_notifier.rb +++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb @@ -21,13 +21,13 @@ module Integrations ) responses.each do |response| - unless response.success? - log_error('SlackMattermostNotifier HTTP error response', - request_host: response.request.uri.host, - response_code: response.code, - response_body: response.body - ) - end + next if response.success? + + log_error('SlackMattermostNotifier HTTP error response', + request_host: response.request.uri.host, + response_code: response.code, + response_body: response.body + ) end end diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb index 18ec1c253e1..412b1da55da 100644 --- a/app/models/concerns/merge_request_reviewer_state.rb +++ b/app/models/concerns/merge_request_reviewer_state.rb @@ -6,20 +6,11 @@ module MergeRequestReviewerState included do enum state: { unreviewed: 0, - reviewed: 1, - attention_requested: 2 + reviewed: 1 } validates :state, presence: true, inclusion: { in: self.states.keys } - - belongs_to :updated_state_by, class_name: 'User', foreign_key: :updated_state_by_user_id - - def attention_requested_by - return unless attention_requested? - - updated_state_by - end end end diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb index 813827478da..335fcec2611 100644 --- a/app/models/concerns/pg_full_text_searchable.rb +++ b/app/models/concerns/pg_full_text_searchable.rb @@ -108,6 +108,7 @@ module PgFullTextSearchable # This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs # See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920 search_term = remove_url_scheme(search_term) + search_term = ActiveSupport::Inflector.transliterate(search_term) joins(:search_data).where( Arel::Nodes::InfixOperation.new( diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 7613691bc2e..2976b6f02a7 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -86,6 +86,10 @@ module ProjectFeaturesCompatibility write_feature_attribute_string(:operations_access_level, value) end + def monitor_access_level=(value) + write_feature_attribute_string(:monitor_access_level, value) + end + def security_and_compliance_access_level=(value) write_feature_attribute_string(:security_and_compliance_access_level, value) end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 65fb62a814f..eccb004b503 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -43,6 +43,33 @@ module Sortable } end + def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:) + reversed_direction = direction == :asc ? :desc : :asc + + # rubocop: disable GitlabSecurity/PublicSend + order = ::Gitlab::Pagination::Keyset::Order.build( + [ + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: attribute_name, + column_expression: column, + order_expression: column.send(direction).send(nullable), + reversed_order_expression: column.send(reversed_direction).send(nullable), + order_direction: direction, + distinct: false, + add_to_projections: true, + nullable: nullable + ), + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: arel_table['id'].desc + ) + ] + ) + # rubocop: enable GitlabSecurity/PublicSend + + order.apply_cursor_conditions(scope).reorder(order) + end + private def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index e10452c1081..14520b2da26 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -263,10 +263,10 @@ class ContainerRepository < ApplicationRecord .with_migration_import_started_at_nil_or_before(before_timestamp) union = ::Gitlab::SQL::Union.new([ - stale_pre_importing, - stale_pre_import_done, - stale_importing - ]) + stale_pre_importing, + stale_pre_import_done, + stale_importing + ]) from("(#{union.to_sql}) #{ContainerRepository.table_name}") end @@ -598,6 +598,7 @@ class ContainerRepository < ApplicationRecord tags_response_body.map do |raw_tag| tag = ContainerRegistry::Tag.new(self, raw_tag['name']) tag.force_created_at_from_iso8601(raw_tag['created_at']) + tag.updated_at = raw_tag['updated_at'] tag end end diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb index f6455da890b..16c741d340f 100644 --- a/app/models/customer_relations/contact.rb +++ b/app/models/customer_relations/contact.rb @@ -79,22 +79,23 @@ class CustomerRelations::Contact < ApplicationRecord end def self.sort_by_name - order(Gitlab::Pagination::Keyset::Order.build([ - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'last_name', - order_expression: arel_table[:last_name].asc, - distinct: false - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'first_name', - order_expression: arel_table[:first_name].asc, - distinct: false - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: arel_table[:id].asc - ) - ])) + order(Gitlab::Pagination::Keyset::Order.build( + [ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'last_name', + order_expression: arel_table[:last_name].asc, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'first_name', + order_expression: arel_table[:first_name].asc, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: arel_table[:id].asc + ) + ])) end def self.find_ids_by_emails(group, emails) @@ -117,22 +118,14 @@ class CustomerRelations::Contact < ApplicationRecord JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email) WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id SQL - connection.execute(sanitize_sql([ - update_query, - old_group_id: group.root_ancestor.id, - new_group_id: group.id - ])) + connection.execute(sanitize_sql([update_query, old_group_id: group.root_ancestor.id, new_group_id: group.id])) dupes_query = <<~SQL DELETE FROM #{table_name} AS existing_contacts USING #{table_name} AS new_contacts WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email) SQL - connection.execute(sanitize_sql([ - dupes_query, - old_group_id: group.root_ancestor.id, - new_group_id: group.id - ])) + connection.execute(sanitize_sql([dupes_query, old_group_id: group.root_ancestor.id, new_group_id: group.id])) where(group: group).update_all(group_id: group.root_ancestor.id) end diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb index 705e84250c9..5eda9b4bf15 100644 --- a/app/models/customer_relations/organization.rb +++ b/app/models/customer_relations/organization.rb @@ -23,6 +23,9 @@ class CustomerRelations::Organization < ApplicationRecord validates :description, length: { maximum: 1024 } validate :validate_root_group + scope :order_scope_asc, ->(field) { order(arel_table[field].asc.nulls_last) } + scope :order_scope_desc, ->(field) { order(arel_table[field].desc.nulls_last) } + # Searches for organizations with a matching name or description. # # This method uses ILIKE on PostgreSQL @@ -38,6 +41,14 @@ class CustomerRelations::Organization < ApplicationRecord where(state: state) end + def self.sort_by_field(field, direction) + if direction == :asc + order_scope_asc(field) + else + order_scope_desc(field) + end + end + def self.sort_by_name order(name: :asc) end @@ -55,28 +66,30 @@ class CustomerRelations::Organization < ApplicationRecord JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name) WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id SQL - connection.execute(sanitize_sql([ - update_query, - old_group_id: group.root_ancestor.id, - new_group_id: group.id - ])) + connection.execute(sanitize_sql([update_query, old_group_id: group.root_ancestor.id, new_group_id: group.id])) dupes_query = <<~SQL DELETE FROM #{table_name} AS existing_organizations USING #{table_name} AS new_organizations WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name) SQL - connection.execute(sanitize_sql([ - dupes_query, - old_group_id: group.root_ancestor.id, - new_group_id: group.id - ])) + connection.execute(sanitize_sql([dupes_query, old_group_id: group.root_ancestor.id, new_group_id: group.id])) where(group: group).update_all(group_id: group.root_ancestor.id) end + def self.counts_by_state + default_state_counts.merge(group(:state).count) + end + private + def self.default_state_counts + states.keys.each_with_object({}) do |key, memo| + memo[key] = 0 + end + end + def validate_root_group return if group&.root? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index a3213a59bed..dafcbc593be 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -18,7 +18,7 @@ class Deployment < ApplicationRecord belongs_to :environment, optional: false belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true belongs_to :user - belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations + belongs_to :deployable, polymorphic: true, optional: true, inverse_of: :deployment # rubocop:disable Cop/PolymorphicAssociations has_many :deployment_merge_requests has_many :merge_requests, @@ -36,6 +36,7 @@ class Deployment < ApplicationRecord delegate :name, to: :environment, prefix: true delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true + scope :for_iid, -> (project, iid) { where(project: project, iid: iid) } scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment_name, -> (project, name) do where('deployments.environment_id = (?)', @@ -58,9 +59,11 @@ class Deployment < ApplicationRecord scope :finished_before, ->(date) { where('finished_at < ?', date) } scope :ordered, -> { order(finished_at: :desc) } + scope :ordered_as_upcoming, -> { order(id: :desc) } VISIBLE_STATUSES = %i[running success failed canceled blocked].freeze FINISHED_STATUSES = %i[success failed canceled].freeze + UPCOMING_STATUSES = %i[created blocked running].freeze state_machine :status, initial: :created do event :run do @@ -220,6 +223,10 @@ class Deployment < ApplicationRecord Ci::Build.where(id: deployable_ids) end + def build + deployable if deployable.is_a?(::Ci::Build) + end + class << self ## # FastDestroyAll concerns @@ -310,6 +317,16 @@ class Deployment < ApplicationRecord project.repository.ancestor?(ancestor_sha, sha) end + def older_than_last_successful_deployment? + last_deployment_id = environment.last_deployment&.id + + return false unless last_deployment_id.present? + + return false if self.id == last_deployment_id + + self.id < last_deployment_id + end + def update_merge_request_metrics! return unless environment.production? && success? @@ -436,6 +453,12 @@ class Deployment < ApplicationRecord deployable.environment_tier_from_options end + # default tag limit is 100, 0 means no limit + def tags(limit: 100) + project.repository.tag_names_contains(sha, limit: limit) + end + strong_memoize_attr :tags + private def update_status!(status) diff --git a/app/models/environment.rb b/app/models/environment.rb index 1950431446b..4b98cd02e3b 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -13,6 +13,7 @@ class Environment < ApplicationRecord self.reactive_cache_work_type = :external_dependency belongs_to :project, optional: false + belongs_to :merge_request, optional: true use_fast_destroy :all_deployments nullify_if_blank :external_url @@ -30,6 +31,16 @@ class Environment < ApplicationRecord has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment has_one :last_visible_deployment, -> { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment' + Deployment::FINISHED_STATUSES.each do |status| + has_one :"last_#{status}_deployment", -> { where(status: status).ordered }, + class_name: 'Deployment', inverse_of: :environment + end + + Deployment::UPCOMING_STATUSES.each do |status| + has_one :"last_#{status}_deployment", -> { where(status: status).ordered_as_upcoming }, + class_name: 'Deployment', inverse_of: :environment + end + has_one :upcoming_deployment, -> { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment @@ -58,6 +69,7 @@ class Environment < ApplicationRecord allow_nil: true validate :safe_external_url + validate :merge_request_not_changed delegate :manual_actions, :other_manual_actions, to: :last_deployment, allow_nil: true delegate :auto_rollback_enabled?, to: :project @@ -84,11 +96,12 @@ class Environment < ApplicationRecord # Search environments which have names like the given query. # Do not set a large limit unless you've confirmed that it works on gitlab.com scale. scope :for_name_like, -> (query, limit: 5) do - where(arel_table[:name].matches("#{sanitize_sql_like query}%")).limit(limit) + where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit) end scope :for_project, -> (project) { where(project_id: project) } scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) } + scope :for_type, -> (type) { where(environment_type: type) } scope :unfoldered, -> { where(environment_type: nil) } scope :with_rank, -> do select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)') @@ -431,9 +444,13 @@ class Environment < ApplicationRecord return unless value parser = ::Gitlab::Ci::Build::DurationParser.new(value) - return if parser.seconds_from_now.nil? + + return if parser.seconds_from_now.nil? && auto_stop_at.nil? self.auto_stop_at = parser.seconds_from_now + rescue ChronicDuration::DurationParseError => ex + Gitlab::ErrorTracking.track_exception(ex, project_id: self.project_id, environment_id: self.id) + raise ex end def rollout_status @@ -509,6 +526,12 @@ class Environment < ApplicationRecord self.tier ||= guess_tier end + def merge_request_not_changed + if merge_request_id_changed? && persisted? + errors.add(:merge_request, 'merge_request cannot be changed') + end + end + # Guessing the tier of the environment if it's not explicitly specified by users. # See https://en.wikipedia.org/wiki/Deployment_environment for industry standard deployment environments def guess_tier diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 43b2c7899a1..d06d0a99948 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -100,7 +100,7 @@ class EnvironmentStatus def self.build_environments_status(mr, user, pipeline) return [] unless pipeline - pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment| + pipeline.environments_in_self_and_project_descendants.includes(:project).available.map do |environment| next unless Ability.allowed?(user, :read_environment, environment) EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha) diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 4953f24755c..12d73ef0d72 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -23,6 +23,7 @@ module ErrorTracking self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } self.reactive_cache_work_type = :external_dependency + self.reactive_cache_hard_limit = ErrorTracking::SentryClient::RESPONSE_SIZE_LIMIT self.table_name = 'project_error_tracking_settings' @@ -103,9 +104,18 @@ module ErrorTracking api_host end + def sentry_response_limit_enabled? + Feature.enabled?(:error_tracking_sentry_limit, project) + end + + def reactive_cache_limit_enabled? + sentry_response_limit_enabled? + end + def sentry_client strong_memoize(:sentry_client) do - ::ErrorTracking::SentryClient.new(api_url, token) + ::ErrorTracking::SentryClient + .new(api_url, token, validate_size_guarded_by_feature_flag: sentry_response_limit_enabled?) end end @@ -127,14 +137,14 @@ module ErrorTracking def issue_details(opts = {}) with_reactive_cache('issue_details', opts.stringify_keys) do |result| - ensure_issue_belongs_to_project!(result[:issue].project_id) + ensure_issue_belongs_to_project!(result[:issue].project_id) if result[:issue] result end end def issue_latest_event(opts = {}) with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result| - ensure_issue_belongs_to_project!(result[:latest_event].project_id) + ensure_issue_belongs_to_project!(result[:latest_event].project_id) if result[:latest_event] result end end diff --git a/app/models/group.rb b/app/models/group.rb index 55455d85531..1445e71b0bc 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -153,7 +153,7 @@ class Group < Namespace after_create :post_create_hook after_destroy :post_destroy_hook - after_save :update_two_factor_requirement + after_commit :update_two_factor_requirement after_update :path_changed_hook, if: :saved_change_to_path? after_create -> { create_or_load_association(:group_feature) } @@ -186,6 +186,27 @@ class Group < Namespace where(project_creation_level: permitted_levels) end + scope :shared_into_ancestors, -> (group) do + joins(:shared_group_links) + .where(group_group_links: { shared_group_id: group.self_and_ancestors }) + end + + # WARNING: This method should never be used on its own + # please do make sure the number of rows you are filtering is small + # enough for this query + # + # It's a replacement for `public_or_visible_to_user` that correctly + # supports subgroup permissions + scope :accessible_to_user, -> (user) do + if user + Preloaders::GroupPolicyPreloader.new(self, user).execute + + select { |group| user.can?(:read_group, group) } + else + public_to_user + end + end + class << self def sort_by_attribute(method) if method == 'storage_size_desc' @@ -614,11 +635,11 @@ class Group < Namespace # 4. They belong to an ancestor group def direct_and_indirect_users User.from_union([ - User - .where(id: direct_and_indirect_members.select(:user_id)) - .reorder(nil), - project_users_with_descendants - ]) + User + .where(id: direct_and_indirect_members.select(:user_id)) + .reorder(nil), + project_users_with_descendants + ]) end # Returns all users (also inactive) that are members of the group because: @@ -628,11 +649,11 @@ class Group < Namespace # 4. They belong to an ancestor group def direct_and_indirect_users_with_inactive User.from_union([ - User - .where(id: direct_and_indirect_members_with_inactive.select(:user_id)) - .reorder(nil), - project_users_with_descendants - ]) + User + .where(id: direct_and_indirect_members_with_inactive.select(:user_id)) + .reorder(nil), + project_users_with_descendants + ]) end def users_count @@ -672,14 +693,6 @@ class Group < Namespace } end - def ci_variables_for(ref, project, environment: nil) - cache_key = "ci_variables_for:group:#{self&.id}:project:#{project&.id}:ref:#{ref}:environment:#{environment}" - - ::Gitlab::SafeRequestStore.fetch(cache_key) do - uncached_ci_variables_for(ref, project, environment: environment) - end - end - def member(user) if group_members.loaded? group_members.find { |gm| gm.user_id == user.id } @@ -890,6 +903,18 @@ class Group < Namespace end end + def packages_policy_subject + if Feature.enabled?(:read_package_policy_rule, self) + ::Packages::Policies::Group.new(self) + else + self + end + end + + def update_two_factor_requirement_for_members + direct_and_indirect_members.find_each(&:update_two_factor_requirement) + end + private def feature_flag_enabled_for_self_or_ancestor?(feature_flag) @@ -912,7 +937,7 @@ class Group < Namespace def update_two_factor_requirement return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period? - direct_and_indirect_members.find_each(&:update_two_factor_requirement) + Groups::UpdateTwoFactorRequirementForMembersWorker.perform_async(self.id) end def path_changed_hook @@ -1031,26 +1056,6 @@ class Group < Namespace def enable_shared_runners! update!(shared_runners_enabled: true) end - - def uncached_ci_variables_for(ref, project, environment: nil) - list_of_ids = if root_ancestor.use_traversal_ids? - [self] + ancestors(hierarchy_order: :asc) - else - [self] + ancestors - end - - variables = Ci::GroupVariable.where(group: list_of_ids) - variables = variables.unprotected unless project.protected_for?(ref) - - variables = if environment - variables.on_environment(environment) - else - variables.where(environment_scope: '*') - end - - variables = variables.group_by(&:group_id) - list_of_ids.reverse.flat_map { |group| variables[group.id] }.compact - end end Group.prepend_mod_with('Group') diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb index 8dd245a6ab5..7005c8593bd 100644 --- a/app/models/group_group_link.rb +++ b/app/models/group_group_link.rb @@ -19,6 +19,10 @@ class GroupGroupLink < ApplicationRecord where(group_access: [Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER]) end + scope :with_owner_access, -> do + where(group_access: [Gitlab::Access::OWNER]) + end + scope :groups_accessible_via, -> (shared_with_group_ids) do links = where(shared_with_group_id: shared_with_group_ids) # a group share also gives you access to the descendants of the group being shared, diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index 24e5f193a32..3fc3f193f19 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -25,7 +25,7 @@ class WebHookLog < ApplicationRecord before_save :redact_author_email def self.recent - where('created_at >= ?', 2.days.ago.beginning_of_day) + where(created_at: 2.days.ago.beginning_of_day..Time.zone.now) .order(created_at: :desc) end diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb index d30d6906e14..dd0d3c6585d 100644 --- a/app/models/incident_management/timeline_event.rb +++ b/app/models/incident_management/timeline_event.rb @@ -20,6 +20,6 @@ module IncidentManagement validates :action, presence: true, length: { maximum: 128 } validates :note, :note_html, presence: true, length: { maximum: 10_000 } - scope :order_occurred_at_asc, -> { reorder(occurred_at: :asc) } + scope :order_occurred_at_asc_id_asc, -> { reorder(occurred_at: :asc, id: :asc) } end end diff --git a/app/models/integration.rb b/app/models/integration.rb index 6d755016380..aecf9529a14 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -401,9 +401,9 @@ class Integration < ApplicationRecord .or(where(type: integration.type, instance: true)).select(:id) from_union([ - where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants), - where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants)) - ]) + where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants), + where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants)) + ]) end def activated? diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index bb0fb6b9079..4479725a33b 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -10,7 +10,7 @@ module Integrations URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/" SUPPORTED_EVENTS = %w[ - pipeline job + pipeline job archive_trace ].freeze TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze @@ -38,14 +38,6 @@ module Integrations SUPPORTED_EVENTS end - def supported_events - events = super - - return events + ['archive_trace'] if Feature.enabled?(:datadog_integration_logs_collection, parent) - - events - end - def self.default_test_event 'pipeline' end @@ -77,7 +69,7 @@ module Integrations end def fields - f = [ + [ { type: 'text', name: 'datadog_site', @@ -110,21 +102,15 @@ module Integrations linkClose: '</a>'.html_safe }, required: true - } - ] - - if Feature.enabled?(:datadog_integration_logs_collection, parent) - f.append({ + }, + { type: 'checkbox', name: 'archive_trace_events', title: s_('Logs'), checkbox_label: s_('Enable logs collection'), help: s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.'), required: false - }) - end - - f += [ + }, { type: 'text', name: 'datadog_service', @@ -161,8 +147,6 @@ module Integrations } } ] - - f end override :hook_url diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index ec8a12e4760..d0389b82410 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -6,6 +6,24 @@ module Integrations class Discord < BaseChatNotification ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze + undef :notify_only_broken_pipelines + + field :webhook, + section: SECTION_TYPE_CONNECTION, + placeholder: 'https://discordapp.com/api/webhooks/…', + help: 'URL to the webhook for the Discord channel.', + required: true + + field :notify_only_broken_pipelines, + type: 'checkbox', + section: SECTION_TYPE_CONFIGURATION + + field :branches_to_be_notified, + type: 'select', + section: SECTION_TYPE_CONFIGURATION, + title: -> { s_('Integrations|Branches for which notifications are to be sent') }, + choices: -> { branch_choices } + def title s_("DiscordService|Discord Notifications") end @@ -18,6 +36,10 @@ module Integrations "discord" end + def fields + self.class.fields + build_event_channels + end + def help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer' s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } @@ -31,30 +53,6 @@ module Integrations %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] end - def default_fields - [ - { - type: 'text', - section: SECTION_TYPE_CONNECTION, - name: 'webhook', - placeholder: 'https://discordapp.com/api/webhooks/…', - help: 'URL to the webhook for the Discord channel.' - }, - { - type: 'checkbox', - section: SECTION_TYPE_CONFIGURATION, - name: 'notify_only_broken_pipelines' - }, - { - type: 'select', - section: SECTION_TYPE_CONFIGURATION, - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - } - ] - end - def sections [ { diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index df112ad6ca8..6e7f31aa030 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -47,8 +47,31 @@ module Integrations private def notify(message, opts) + url = webhook.dup + + key = parse_thread_key(message) + url = Gitlab::Utils.add_url_parameters(url, { threadKey: key }) if key + simple_text = parse_simple_text_message(message) - ::HangoutsChat::Sender.new(webhook).simple(simple_text) + ::HangoutsChat::Sender.new(url).simple(simple_text) + end + + # Returns an appropriate key for threading messages in google chat + def parse_thread_key(message) + case message + when Integrations::ChatMessage::NoteMessage + message.target + when Integrations::ChatMessage::IssueMessage + "issue #{Issue.reference_prefix}#{message.issue_iid}" + when Integrations::ChatMessage::MergeMessage + "merge request #{MergeRequest.reference_prefix}#{message.merge_request_iid}" + when Integrations::ChatMessage::PushMessage + "push #{message.project_name}_#{message.ref}" + when Integrations::ChatMessage::PipelineMessage + "pipeline #{message.pipeline_id}" + when Integrations::ChatMessage::WikiPageMessage + "wiki_page #{message.wiki_page_url}" + end end def parse_simple_text_message(message) diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb index 03913a71d47..58eabcfd378 100644 --- a/app/models/integrations/harbor.rb +++ b/app/models/integrations/harbor.rb @@ -24,6 +24,10 @@ module Integrations s_("HarborIntegration|After the Harbor integration is activated, global variables '$HARBOR_USERNAME', '$HARBOR_HOST', '$HARBOR_OCI', '$HARBOR_PASSWORD', '$HARBOR_URL' and '$HARBOR_PROJECT' will be created for CI/CD use.") end + def hostname + Gitlab::Utils.parse_url(url).hostname + end + class << self def to_param name.demodulize.downcase diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb index 8bc296e0320..f5b6595fff2 100644 --- a/app/models/integrations/shimo.rb +++ b/app/models/integrations/shimo.rb @@ -9,8 +9,6 @@ module Integrations required: true def render? - return false unless Feature.enabled?(:shimo_integration, project) - valid? && activated? end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index b502d5e354d..d141061062a 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -143,10 +143,7 @@ class InternalId < ApplicationRecord def track_greatest(new_value) InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage) - function = Arel::Nodes::NamedFunction.new('GREATEST', [ - arel_table[:last_value], - new_value.to_i - ]) + function = Arel::Nodes::NamedFunction.new('GREATEST', [arel_table[:last_value], new_value.to_i]) next_iid = update_record!(subject, scope, usage, function) return next_iid if next_iid diff --git a/app/models/issue.rb b/app/models/issue.rb index df8ee34b3c3..153747c75df 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -254,31 +254,6 @@ class Issue < ApplicationRecord alias_method :with_state, :with_state_id alias_method :with_states, :with_state_ids - def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:) - reversed_direction = direction == :asc ? :desc : :asc - - # rubocop: disable GitlabSecurity/PublicSend - order = ::Gitlab::Pagination::Keyset::Order.build([ - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: attribute_name, - column_expression: column, - order_expression: column.send(direction).send(nullable), - reversed_order_expression: column.send(reversed_direction).send(nullable), - order_direction: direction, - distinct: false, - add_to_projections: true, - nullable: nullable - ), - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: arel_table['id'].desc - ) - ]) - # rubocop: enable GitlabSecurity/PublicSend - - order.apply_cursor_conditions(scope).order(order) - end - override :order_upvotes_desc def order_upvotes_desc reorder(upvotes_count: :desc) @@ -293,16 +268,6 @@ class Issue < ApplicationRecord def pg_full_text_search(search_term) super.where('issue_search_data.project_id = issues.project_id') end - - override :full_search - def full_search(query, matched_columns: nil, use_minimum_char_limit: true) - return super if query.match?(IssuableFinder::FULL_TEXT_SEARCH_TERM_REGEX) - - super.where( - 'issues.title NOT SIMILAR TO :pattern OR issues.description NOT SIMILAR TO :pattern', - pattern: IssuableFinder::FULL_TEXT_SEARCH_TERM_PATTERN - ) - end end def next_object_by_relative_position(ignoring: nil, order: :asc) @@ -406,8 +371,6 @@ class Issue < ApplicationRecord attribute_name: 'relative_position', column_expression: arel_table[:relative_position], order_expression: Issue.arel_table[:relative_position].asc.nulls_last, - reversed_order_expression: Issue.arel_table[:relative_position].desc.nulls_last, - order_direction: :asc, nullable: :nulls_last, distinct: false ) @@ -695,11 +658,11 @@ class Issue < ApplicationRecord return unless persisted? if confidential? && WorkItems::ParentLink.has_public_children?(id) - errors.add(:confidential, _('confidential parent can not be used if there are non-confidential children.')) + errors.add(:base, _('A confidential issue cannot have a parent that already has non-confidential children.')) end if !confidential? && WorkItems::ParentLink.has_confidential_parent?(id) - errors.add(:confidential, _('associated parent is confidential and can not have non-confidential children.')) + errors.add(:base, _('A non-confidential issue cannot have a confidential parent.')) end end @@ -722,7 +685,7 @@ class Issue < ApplicationRecord end def record_create_action - Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author) + Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author, project: project) end # Returns `true` if this Issue is visible to everybody. diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb index 8befe9a9230..0a2d3ba0749 100644 --- a/app/models/jira_connect_installation.rb +++ b/app/models/jira_connect_installation.rb @@ -24,4 +24,10 @@ class JiraConnectInstallation < ApplicationRecord def client Atlassian::JiraConnect::Client.new(base_url, shared_secret) end + + def oauth_authorization_url + return Gitlab.config.gitlab.url if instance_url.blank? || Feature.disabled?(:jira_connect_oauth_self_managed) + + instance_url + end end diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb index 94444f4b6d3..f28e8f81b40 100644 --- a/app/models/loose_foreign_keys/deleted_record.rb +++ b/app/models/loose_foreign_keys/deleted_record.rb @@ -12,7 +12,7 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel next_partition_if: -> (active_partition) do oldest_record_in_partition = LooseForeignKeys::DeletedRecord .select(:id, :created_at) - .for_partition(active_partition) + .for_partition(active_partition.value) .order(:id) .limit(1) .take @@ -22,7 +22,7 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel end, detach_partition_if: -> (partition) do !LooseForeignKeys::DeletedRecord - .for_partition(partition) + .for_partition(partition.value) .status_pending .exists? end diff --git a/app/models/member.rb b/app/models/member.rb index 0cd1e022617..c5351d5447b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -60,6 +60,7 @@ class Member < ApplicationRecord if: :project_bot? validate :access_level_inclusion validate :validate_member_role_access_level + validate :validate_access_level_locked_for_member_role, on: :update scope :with_invited_user_state, -> do joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email') @@ -73,10 +74,7 @@ class Member < ApplicationRecord projects = source.root_ancestor.all_projects project_members = Member.default_scoped.where(source: projects).select(*Member.cached_column_list) - Member.default_scoped.from_union([ - group_members, - project_members - ]).merge(self) + Member.default_scoped.from_union([group_members, project_members]).merge(self) end scope :excluding_users, ->(user_ids) do @@ -186,14 +184,85 @@ class Member < ApplicationRecord unscoped.from(distinct_members, :members) end - scope :order_name_asc, -> { left_join_users.reorder(User.arel_table[:name].asc.nulls_last) } - scope :order_name_desc, -> { left_join_users.reorder(User.arel_table[:name].desc.nulls_last) } - scope :order_recent_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].desc.nulls_last) } - scope :order_oldest_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].asc.nulls_last) } - scope :order_recent_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].desc.nulls_last) } - scope :order_oldest_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].asc.nulls_first) } - scope :order_recent_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].desc.nulls_last) } - scope :order_oldest_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].asc.nulls_first) } + scope :order_name_asc, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_full_name', + column: User.arel_table[:name], + direction: :asc, + nullable: :nulls_last + ) + end + + scope :order_name_desc, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_full_name', + column: User.arel_table[:name], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_sign_in, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_sign_in_at', + column: User.arel_table[:last_sign_in_at], + direction: :asc, + nullable: :nulls_last + ) + end + + scope :order_recent_sign_in, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_sign_in_at', + column: User.arel_table[:last_sign_in_at], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_last_activity, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_activity_on', + column: User.arel_table[:last_activity_on], + direction: :asc, + nullable: :nulls_first + ) + end + + scope :order_recent_last_activity, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_activity_on', + column: User.arel_table[:last_activity_on], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_created_user, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_created_at', + column: User.arel_table[:created_at], + direction: :asc, + nullable: :nulls_first + ) + end + + scope :order_recent_created_user, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_created_at', + column: User.arel_table[:created_at], + direction: :desc, + nullable: :nulls_last + ) + end scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) } @@ -438,6 +507,14 @@ class Member < ApplicationRecord end end + def validate_access_level_locked_for_member_role + return unless member_role_id + + if access_level_changed? + errors.add(:access_level, _("cannot be changed since member is associated with a custom role")) + end + end + def send_invite # override in subclass end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 3c06e1aa983..a57cb97e936 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -20,7 +20,7 @@ class MergeRequest < ApplicationRecord include IgnorableColumns include MilestoneEventable include StateEventable - include ApprovableBase + include Approvable include IdInOrdered include Todoable @@ -67,6 +67,8 @@ class MergeRequest < ApplicationRecord has_one :merge_head_diff, -> { merge_head }, inverse_of: :merge_request, class_name: 'MergeRequestDiff' has_one :cleanup_schedule, inverse_of: :merge_request + has_one :predictions, inverse_of: :merge_request + delegate :suggested_reviewers, to: :predictions belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff' manual_inverse_association :latest_merge_request_diff, :merge_request @@ -116,6 +118,7 @@ class MergeRequest < ApplicationRecord has_many :draft_notes has_many :reviews, inverse_of: :merge_request + has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request KNOWN_MERGE_PARAMS = [ :auto_merge_strategy, @@ -343,23 +346,24 @@ class MergeRequest < ApplicationRecord column_expression = MergeRequest::Metrics.arel_table[metric] column_expression_with_direction = direction == 'ASC' ? column_expression.asc : column_expression.desc - order = Gitlab::Pagination::Keyset::Order.build([ - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: "merge_request_metrics_#{metric}", - column_expression: column_expression, - order_expression: column_expression_with_direction.nulls_last, - reversed_order_expression: column_expression_with_direction.reverse.nulls_first, - order_direction: direction, - nullable: :nulls_last, - distinct: false, - add_to_projections: true - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'merge_request_metrics_id', - order_expression: MergeRequest::Metrics.arel_table[:id].desc, - add_to_projections: true - ) - ]) + order = Gitlab::Pagination::Keyset::Order.build( + [ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: "merge_request_metrics_#{metric}", + column_expression: column_expression, + order_expression: column_expression_with_direction.nulls_last, + reversed_order_expression: column_expression_with_direction.reverse.nulls_first, + order_direction: direction, + nullable: :nulls_last, + distinct: false, + add_to_projections: true + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'merge_request_metrics_id', + order_expression: MergeRequest::Metrics.arel_table[:id].desc, + add_to_projections: true + ) + ]) order.apply_cursor_conditions(join_metrics).order(order) end @@ -417,17 +421,6 @@ class MergeRequest < ApplicationRecord ) end - scope :attention, ->(user) do - # rubocop: disable Gitlab/Union - union = Gitlab::SQL::Union.new([ - MergeRequestReviewer.select(:merge_request_id).where(user_id: user.id, state: MergeRequestReviewer.states[:attention_requested]), - MergeRequestAssignee.select(:merge_request_id).where(user_id: user.id, state: MergeRequestAssignee.states[:attention_requested]) - ]) - # rubocop: enable Gitlab/Union - - with(Gitlab::SQL::CTE.new(:reviewers_and_assignees, union).to_arel).where('merge_requests.id in (select merge_request_id from reviewers_and_assignees)') - end - def self.total_time_to_merge join_metrics .merge(MergeRequest::Metrics.with_valid_time_to_merge) @@ -1187,41 +1180,13 @@ class MergeRequest < ApplicationRecord ] end - def detailed_merge_status - if cannot_be_merged_rechecking? || preparing? || checking? - return :checking - elsif unchecked? - return :unchecked - end - - checks = execute_merge_checks - - if checks.success? - :mergeable - else - checks.failure_reason - end - end - - # rubocop: disable CodeReuse/ServiceClass def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) - if Feature.enabled?(:improved_mergeability_checks, self.project) - additional_checks = execute_merge_checks(params: { - skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check - }) - additional_checks.execute.success? - else - return false unless open? - return false if draft? - return false if broken? - return false unless skip_discussions_check || mergeable_discussions_state? - return false unless skip_ci_check || mergeable_ci_state? - - true - end + additional_checks = execute_merge_checks(params: { + skip_ci_check: skip_ci_check, + skip_discussions_check: skip_discussions_check + }) + additional_checks.success? end - # rubocop: enable CodeReuse/ServiceClass def ff_merge_possible? project.repository.ancestor?(target_branch_sha, diff_head_sha) @@ -1318,7 +1283,6 @@ class MergeRequest < ApplicationRecord # running `ReferenceExtractor` on each of them separately. # This optimization does not apply to issues from external sources. def cache_merge_request_closes_issues!(current_user = self.author) - return unless project.issues_enabled? return if closed? || merged? transaction do @@ -1489,7 +1453,7 @@ class MergeRequest < ApplicationRecord end def environments_in_head_pipeline(deployment_status: nil) - actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none + actual_head_pipeline&.environments_in_self_and_project_descendants(deployment_status: deployment_status) || Environment.none end def fetch_ref! @@ -1589,7 +1553,7 @@ class MergeRequest < ApplicationRecord end def has_test_reports? - actual_head_pipeline&.has_reports?(Ci::JobArtifact.test_reports) + actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)) end def predefined_variables @@ -1619,7 +1583,7 @@ class MergeRequest < ApplicationRecord end def has_accessibility_reports? - actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports) + actual_head_pipeline.present? && actual_head_pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:accessibility)) end def has_coverage_reports? @@ -1627,7 +1591,7 @@ class MergeRequest < ApplicationRecord end def has_terraform_reports? - actual_head_pipeline&.has_reports?(Ci::JobArtifact.terraform_reports) + actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform)) end def compare_accessibility_reports @@ -1667,7 +1631,7 @@ class MergeRequest < ApplicationRecord end def has_codequality_reports? - actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports) + actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality)) end def compare_codequality_reports @@ -1717,11 +1681,11 @@ class MergeRequest < ApplicationRecord end def has_sast_reports? - !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports) + !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:sast)) end def has_secret_detection_reports? - !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports) + !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:secret_detection)) end def compare_sast_reports(current_user) @@ -2019,6 +1983,12 @@ class MergeRequest < ApplicationRecord false # Overridden in EE end + def execute_merge_checks(params: {}) + # rubocop: disable CodeReuse/ServiceClass + MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute + # rubocop: enable CodeReuse/ServiceClass + end + private attr_accessor :skip_fetch_ref @@ -2072,12 +2042,6 @@ class MergeRequest < ApplicationRecord def report_type_enabled?(report_type) !!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type) end - - def execute_merge_checks(params: {}) - # rubocop: disable CodeReuse/ServiceClass - MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute - # rubocop: enable CodeReuse/ServiceClass - end end MergeRequest.prepend_mod_with('MergeRequest') diff --git a/app/models/merge_request/predictions.rb b/app/models/merge_request/predictions.rb new file mode 100644 index 00000000000..ef9e00b5f74 --- /dev/null +++ b/app/models/merge_request/predictions.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class MergeRequest::Predictions < ApplicationRecord # rubocop:disable Style/ClassAndModuleChildren + belongs_to :merge_request, inverse_of: :predictions + + validates :suggested_reviewers, json_schema: { filename: 'merge_request_predictions_suggested_reviewers' } +end diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb index fd8e5860040..be3a1d42eac 100644 --- a/app/models/merge_request_assignee.rb +++ b/app/models/merge_request_assignee.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class MergeRequestAssignee < ApplicationRecord - include MergeRequestReviewerState + include IgnorableColumns + ignore_column %i[state updated_state_by_user_id], remove_with: '15.6', remove_after: '2022-10-22' belongs_to :merge_request, touch: true belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees @@ -11,6 +12,6 @@ class MergeRequestAssignee < ApplicationRecord scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) } def cache_key - [model_name.cache_key, id, state, assignee.cache_key] + [model_name.cache_key, id, assignee.cache_key] end end diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb index 4abf0fa09f0..4b5b71481d3 100644 --- a/app/models/merge_request_reviewer.rb +++ b/app/models/merge_request_reviewer.rb @@ -2,6 +2,8 @@ class MergeRequestReviewer < ApplicationRecord include MergeRequestReviewerState + include IgnorableColumns + ignore_column :updated_state_by_user_id, remove_with: '15.6', remove_after: '2022-10-22' belongs_to :merge_request belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index e181217f01c..29e1ba88528 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -2,11 +2,24 @@ module Ml class Candidate < ApplicationRecord + enum status: { running: 0, scheduled: 1, finished: 2, failed: 3, killed: 4 } + validates :iid, :experiment, presence: true + validates :status, inclusion: { in: statuses.keys } belongs_to :experiment, class_name: 'Ml::Experiment' belongs_to :user has_many :metrics, class_name: 'Ml::CandidateMetric' has_many :params, class_name: 'Ml::CandidateParam' + + default_value_for(:iid) { SecureRandom.uuid } + + class << self + def with_project_id_and_iid(project_id, iid) + return unless project_id.present? && iid.present? + + joins(:experiment).find_by(experiment: { project_id: project_id }, iid: iid) + end + end end end diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index 7ef9c70ba7e..e4e9baac4c8 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -2,11 +2,33 @@ module Ml class Experiment < ApplicationRecord - validates :name, :iid, :project, presence: true - validates :iid, :name, uniqueness: { scope: :project, message: "should be unique in the project" } + include AtomicInternalId + + validates :name, :project, presence: true + validates :name, uniqueness: { scope: :project, message: "should be unique in the project" } belongs_to :project belongs_to :user has_many :candidates, class_name: 'Ml::Candidate' + + has_internal_id :iid, scope: :project + + def artifact_location + 'not_implemented' + end + + class << self + def by_project_id_and_iid(project_id, iid) + find_by(project_id: project_id, iid: iid) + end + + def by_project_id_and_name(project_id, name) + find_by(project_id: project_id, name: name) + end + + def has_record?(project_id, name) + where(project_id: project_id, name: name).exists? + end + end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 06f49f16d66..0ffd5c446d3 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -43,6 +43,8 @@ class Namespace < ApplicationRecord # The first date in https://docs.gitlab.com/ee/user/usage_quotas.html#namespace-storage-limit-enforcement-schedule # Determines when we start enforcing namespace storage MIN_STORAGE_ENFORCEMENT_DATE = Date.new(2022, 10, 19) + # https://gitlab.com/gitlab-org/gitlab/-/issues/367531 + MIN_STORAGE_ENFORCEMENT_USAGE = 5.gigabytes cache_markdown_field :description, pipeline: :description @@ -59,7 +61,7 @@ class Namespace < ApplicationRecord has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace' has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' has_many :pending_builds, class_name: 'Ci::PendingBuild' - has_one :onboarding_progress + has_one :onboarding_progress, class_name: 'Onboarding::Progress' # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. @@ -126,8 +128,9 @@ class Namespace < ApplicationRecord delegate :avatar_url, to: :owner, allow_nil: true delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=, to: :namespace_settings, allow_nil: true + delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=, + to: :namespace_settings - after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? } after_save :reload_namespace_details after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } @@ -136,6 +139,7 @@ class Namespace < ApplicationRecord before_update :sync_share_with_group_lock_with_parent, if: :parent_changed? after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? } after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? } + after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear # Legacy Storage specific hooks @@ -172,13 +176,17 @@ class Namespace < ApplicationRecord end scope :sorted_by_similarity_and_parent_id_desc, -> (search) do - order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [ - { column: arel_table["path"], multiplier: 1 }, - { column: arel_table["name"], multiplier: 0.7 } - ]) + order_expression = Gitlab::Database::SimilarityScore.build_expression( + search: search, + rules: [ + { column: arel_table["path"], multiplier: 1 }, + { column: arel_table["name"], multiplier: 0.7 } + ]) reorder(order_expression.desc, Namespace.arel_table['parent_id'].desc.nulls_last, Namespace.arel_table['id'].desc) end + scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) } + # Make sure that the name is same as strong_memoize name in root_ancestor # method attr_writer :root_ancestor, :emails_disabled_memoized @@ -362,7 +370,7 @@ class Namespace < ApplicationRecord end def any_project_with_shared_runners_enabled? - projects.with_shared_runners.any? + projects.with_shared_runners_enabled.any? end def user_ids_for_project_authorizations diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 595e34821af..6a87fba57ac 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -4,6 +4,11 @@ class NamespaceSetting < ApplicationRecord include CascadingNamespaceSettingAttribute include Sanitizable include ChronicDurationAttribute + include IgnorableColumns + + ignore_columns %i[exclude_from_free_user_cap include_for_free_user_cap_preview], + remove_with: '15.5', + remove_after: '2022-09-23' cascading_attr :delayed_project_removal @@ -53,8 +58,18 @@ class NamespaceSetting < ApplicationRecord namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy end + def show_diff_preview_in_email? + return show_diff_preview_in_email unless namespace.has_parent? + + all_ancestors_allow_diff_preview_in_email? + end + private + def all_ancestors_allow_diff_preview_in_email? + !self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists? + end + def normalize_default_branch_name self.default_branch_name = default_branch_name.presence end diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 687fa6a5334..16a9c20dfdc 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -47,6 +47,8 @@ module Namespaces # This uses rails internal before_commit API to sync traversal_ids on namespace create, right before transaction is committed. # This helps reduce the time during which the root namespace record is locked to ensure updated traversal_ids are valid before_commit :sync_traversal_ids, on: [:create] + + define_model_callbacks :sync_traversal_ids end class_methods do @@ -208,10 +210,12 @@ module Namespaces # # NOTE: self.traversal_ids will be stale. Reload for a fresh record. def sync_traversal_ids - # Clear any previously memoized root_ancestor as our ancestors have changed. - clear_memoization(:root_ancestor) + run_callbacks :sync_traversal_ids do + # Clear any previously memoized root_ancestor as our ancestors have changed. + clear_memoization(:root_ancestor) - Namespace::TraversalHierarchy.for_namespace(self).sync_traversal_ids! + Namespace::TraversalHierarchy.for_namespace(self).sync_traversal_ids! + end end # Lock the root of the hierarchy we just left, and lock the root of the hierarchy diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb index 81ac026d7ff..843de9bce33 100644 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ b/app/models/namespaces/traversal/linear_scopes.rb @@ -41,24 +41,13 @@ module Namespaces def self_and_descendants(include_self: true) return super unless use_traversal_ids_for_descendants_scopes? - if Feature.enabled?(:traversal_ids_btree) - self_and_descendants_with_comparison_operators(include_self: include_self) - else - records = self_and_descendants_with_duplicates_with_array_operator(include_self: include_self) - distinct = records.select('DISTINCT on(namespaces.id) namespaces.*') - distinct.normal_select - end + self_and_descendants_with_comparison_operators(include_self: include_self) end def self_and_descendant_ids(include_self: true) return super unless use_traversal_ids_for_descendants_scopes? - if Feature.enabled?(:traversal_ids_btree) - self_and_descendants_with_comparison_operators(include_self: include_self).as_ids - else - self_and_descendants_with_duplicates_with_array_operator(include_self: include_self) - .select('DISTINCT namespaces.id') - end + self_and_descendants(include_self: include_self).as_ids end def self_and_hierarchy @@ -181,20 +170,6 @@ module Namespaces Arel::Nodes::NamedFunction.new('unnest', args) end - def self_and_descendants_with_duplicates_with_array_operator(include_self: true) - base_ids = select(:id) - - records = unscoped - .from("namespaces, (#{base_ids.to_sql}) base") - .where('namespaces.traversal_ids @> ARRAY[base.id]') - - if include_self - records - else - records.where('namespaces.id <> base.id') - end - end - def superset_cte(base_name) superset_sql = <<~SQL SELECT d1.traversal_ids diff --git a/app/models/note.rb b/app/models/note.rb index 1715f7cdc3b..daac489757b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -164,6 +164,9 @@ 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 + # Syncs `confidential` with `internal` as we rename the column. + # https://gitlab.com/gitlab-org/gitlab/-/issues/367923 + before_create :set_internal_flag 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? @@ -813,6 +816,10 @@ class Note < ApplicationRecord def noteable_can_have_confidential_note? for_issue? end + + def set_internal_flag + self.internal = confidential if confidential + end end Note.prepend_mod_with('Note') diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index b3eaed154e2..caa24377791 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -38,6 +38,7 @@ class NotificationRecipient return !unsubscribed? if @type == :subscription return false unless suitable_notification_level? + return false if email_blocked? # check this last because it's expensive # nobody should receive notifications if they've specifically unsubscribed @@ -95,6 +96,15 @@ class NotificationRecipient end end + def email_blocked? + return false if Feature.disabled?(:block_emails_with_failures) + + recipient_email = user.notification_email_for(@group) + + Gitlab::ApplicationRateLimiter.peek(:permanent_email_failure, scope: recipient_email) || + Gitlab::ApplicationRateLimiter.peek(:temporary_email_failure, scope: recipient_email) + end + def has_access? DeclarativePolicy.subject_scope do break false unless user.can?(:receive_notifications) diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index 7d71e15d3c5..eac99e8d441 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -26,4 +26,13 @@ class OauthAccessToken < Doorkeeper::AccessToken super end + + # Override Doorkeeper::AccessToken.matching_token_for since we + # have `reuse_access_tokens` disabled and we also hash tokens. + # This ensures we don't accidentally return a hashed token value. + def self.matching_token_for(application, resource_owner, scopes) + return if Feature.enabled?(:hash_oauth_tokens) + + super + end end diff --git a/app/models/onboarding/completion.rb b/app/models/onboarding/completion.rb new file mode 100644 index 00000000000..49fdb102209 --- /dev/null +++ b/app/models/onboarding/completion.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Onboarding + class Completion + include Gitlab::Utils::StrongMemoize + include Gitlab::Experiment::Dsl + + ACTION_ISSUE_IDS = { + pipeline_created: 7, + trial_started: 2, + required_mr_approvals_enabled: 11, + code_owners_enabled: 10 + }.freeze + + ACTION_PATHS = [ + :issue_created, + :git_write, + :merge_request_created, + :user_added + ].freeze + + def initialize(namespace, current_user = nil) + @namespace = namespace + @current_user = current_user + end + + def percentage + return 0 unless onboarding_progress + + attributes = onboarding_progress.attributes.symbolize_keys + + total_actions = action_columns.count + completed_actions = action_columns.count { |column| attributes[column].present? } + + (completed_actions.to_f / total_actions * 100).round + end + + private + + def onboarding_progress + strong_memoize(:onboarding_progress) do + ::Onboarding::Progress.find_by(namespace: namespace) + end + end + + def action_columns + strong_memoize(:action_columns) do + tracked_actions.map { |action_key| ::Onboarding::Progress.column_name(action_key) } + end + end + + def tracked_actions + ACTION_ISSUE_IDS.keys + ACTION_PATHS + deploy_section_tracked_actions + end + + def deploy_section_tracked_actions + experiment( + :security_actions_continuous_onboarding, + namespace: namespace, + user: current_user, + sticky_to: current_user + ) do |e| + e.control { [:security_scan_enabled] } + e.candidate { [:license_scanning_run, :secure_dependency_scanning_run, :secure_dast_run] } + end.run + end + + attr_reader :namespace, :current_user + end +end diff --git a/app/models/onboarding/learn_gitlab.rb b/app/models/onboarding/learn_gitlab.rb new file mode 100644 index 00000000000..d7a189ed6e2 --- /dev/null +++ b/app/models/onboarding/learn_gitlab.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Onboarding + class LearnGitlab + PROJECT_NAME = 'Learn GitLab' + PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial' + BOARD_NAME = 'GitLab onboarding' + LABEL_NAME = 'Novice' + + def initialize(current_user) + @current_user = current_user + end + + def available? + project && board && label + end + + def project + @project ||= current_user.projects.find_by_name([PROJECT_NAME, PROJECT_NAME_ULTIMATE_TRIAL]) + end + + def board + return unless project + + @board ||= project.boards.find_by_name(BOARD_NAME) + end + + def label + return unless project + + @label ||= project.labels.find_by_name(LABEL_NAME) + end + + private + + attr_reader :current_user + end +end diff --git a/app/models/onboarding/progress.rb b/app/models/onboarding/progress.rb new file mode 100644 index 00000000000..ecc78418256 --- /dev/null +++ b/app/models/onboarding/progress.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module Onboarding + class Progress < ApplicationRecord + self.table_name = 'onboarding_progresses' + + belongs_to :namespace, optional: false + + validate :namespace_is_root_namespace + + ACTIONS = [ + :git_pull, + :git_write, + :merge_request_created, + :pipeline_created, + :user_added, + :trial_started, + :subscription_created, + :required_mr_approvals_enabled, + :code_owners_enabled, + :scoped_label_created, + :security_scan_enabled, + :issue_created, + :issue_auto_closed, + :repository_imported, + :repository_mirrored, + :secure_dependency_scanning_run, + :secure_container_scanning_run, + :secure_dast_run, + :secure_secret_detection_run, + :secure_coverage_fuzzing_run, + :secure_api_fuzzing_run, + :secure_cluster_image_scanning_run, + :license_scanning_run + ].freeze + + scope :incomplete_actions, ->(actions) do + Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) } + end + + scope :completed_actions, ->(actions) do + Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) } + end + + scope :completed_actions_with_latest_in_range, ->(actions, range) do + actions = Array(actions) + if actions.size == 1 + where(column_name(actions[0]) => range) + else + action_columns = actions.map { |action| arel_table[column_name(action)] } + completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range)) + end + end + + class << self + def onboard(namespace) + return unless root_namespace?(namespace) + + create(namespace: namespace) + end + + def onboarding?(namespace) + where(namespace: namespace).any? + end + + def register(namespace, actions) + actions = Array(actions) + return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty? + + onboarding_progress = find_by(namespace: namespace) + return unless onboarding_progress + + now = Time.current + nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? } + return if nil_actions.empty? + + updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) } + onboarding_progress.update!(updates) + end + + def completed?(namespace, action) + return unless root_namespace?(namespace) && ACTIONS.include?(action) + + action_column = column_name(action) + where(namespace: namespace).where.not(action_column => nil).exists? + end + + def not_completed?(namespace_id, action) + return unless ACTIONS.include?(action) + + action_column = column_name(action) + exists?(namespace_id: namespace_id, action_column => nil) + end + + def column_name(action) + :"#{action}_at" + end + + private + + def root_namespace?(namespace) + namespace&.root? + end + end + + def number_of_completed_actions + attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size + end + + private + + def namespace_is_root_namespace + return unless namespace + + errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent? + end + end +end diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb deleted file mode 100644 index e5851c5cfc5..00000000000 --- a/app/models/onboarding_progress.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -class OnboardingProgress < ApplicationRecord - belongs_to :namespace, optional: false - - validate :namespace_is_root_namespace - - ACTIONS = [ - :git_pull, - :git_write, - :merge_request_created, - :pipeline_created, - :user_added, - :trial_started, - :subscription_created, - :required_mr_approvals_enabled, - :code_owners_enabled, - :scoped_label_created, - :security_scan_enabled, - :issue_created, - :issue_auto_closed, - :repository_imported, - :repository_mirrored, - :secure_dependency_scanning_run, - :secure_container_scanning_run, - :secure_dast_run, - :secure_secret_detection_run, - :secure_coverage_fuzzing_run, - :secure_api_fuzzing_run, - :secure_cluster_image_scanning_run, - :license_scanning_run - ].freeze - - scope :incomplete_actions, -> (actions) do - Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) } - end - - scope :completed_actions, -> (actions) do - Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) } - end - - scope :completed_actions_with_latest_in_range, -> (actions, range) do - actions = Array(actions) - if actions.size == 1 - where(column_name(actions[0]) => range) - else - action_columns = actions.map { |action| arel_table[column_name(action)] } - completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range)) - end - end - - class << self - def onboard(namespace) - return unless root_namespace?(namespace) - - create(namespace: namespace) - end - - def onboarding?(namespace) - where(namespace: namespace).any? - end - - def register(namespace, actions) - actions = Array(actions) - return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty? - - onboarding_progress = find_by(namespace: namespace) - return unless onboarding_progress - - now = Time.current - nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? } - return if nil_actions.empty? - - updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) } - onboarding_progress.update!(updates) - end - - def completed?(namespace, action) - return unless root_namespace?(namespace) && ACTIONS.include?(action) - - action_column = column_name(action) - where(namespace: namespace).where.not(action_column => nil).exists? - end - - def not_completed?(namespace_id, action) - return unless ACTIONS.include?(action) - - action_column = column_name(action) - where(namespace_id: namespace_id).where(action_column => nil).exists? - end - - def column_name(action) - :"#{action}_at" - end - - private - - def root_namespace?(namespace) - namespace && namespace.root? - end - end - - def number_of_completed_actions - attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size - end - - private - - def namespace_is_root_namespace - return unless namespace - - errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent? - end -end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index afd55b4f143..b4c09d99bb0 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -22,7 +22,8 @@ class Packages::Package < ApplicationRecord debian: 9, rubygems: 10, helm: 11, - terraform_module: 12 + terraform_module: 12, + rpm: 13 } enum status: { default: 0, hidden: 1, processing: 2, error: 3, pending_destruction: 4 } @@ -43,6 +44,7 @@ class Packages::Package < ApplicationRecord has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum' has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum' has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum' + has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum' has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum' has_many :build_infos, inverse_of: :package has_many :pipelines, through: :build_infos, disable_joins: true @@ -242,22 +244,23 @@ class Packages::Package < ApplicationRecord reverse_order_direction = direction == :asc ? desc_order_expression : asc_order_expression arel_order_classes = ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::AREL_ORDER_CLASSES.invert - ::Gitlab::Pagination::Keyset::Order.build([ - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: "#{join_table}_#{column_name}", - column_expression: join_class.arel_table[column_name], - order_expression: order_direction, - reversed_order_expression: reverse_order_direction, - order_direction: direction, - distinct: false, - add_to_projections: true - ), - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]), - add_to_projections: true - ) - ]) + ::Gitlab::Pagination::Keyset::Order.build( + [ + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: "#{join_table}_#{column_name}", + column_expression: join_class.arel_table[column_name], + order_expression: order_direction, + reversed_order_expression: reverse_order_direction, + order_direction: direction, + distinct: false, + add_to_projections: true + ), + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]), + add_to_projections: true + ) + ]) end def versions @@ -330,6 +333,12 @@ class Packages::Package < ApplicationRecord name.gsub(/#{Gitlab::Regex::Packages::PYPI_NORMALIZED_NAME_REGEX_STRING}/o, '-').downcase end + def touch_last_downloaded_at + ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do + update_column(:last_downloaded_at, Time.zone.now) + end + end + private def composer_tag_version? diff --git a/app/models/packages/policies/group.rb b/app/models/packages/policies/group.rb new file mode 100644 index 00000000000..66cd361f2ed --- /dev/null +++ b/app/models/packages/policies/group.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Packages + module Policies + class Group + attr_accessor :group + + delegate_missing_to :group + + def initialize(group) + @group = group + end + end + end +end diff --git a/app/models/packages/policies/project.rb b/app/models/packages/policies/project.rb new file mode 100644 index 00000000000..a5c6703be42 --- /dev/null +++ b/app/models/packages/policies/project.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Packages + module Policies + class Project + attr_accessor :project + + delegate_missing_to :project + + def initialize(project) + @project = project + end + end + end +end diff --git a/app/models/packages/rpm.rb b/app/models/packages/rpm.rb new file mode 100644 index 00000000000..fc66e7ec5c8 --- /dev/null +++ b/app/models/packages/rpm.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Rpm + def self.table_name_prefix + 'packages_rpm_' + end + end +end diff --git a/app/models/packages/rpm/metadatum.rb b/app/models/packages/rpm/metadatum.rb new file mode 100644 index 00000000000..07361995a12 --- /dev/null +++ b/app/models/packages/rpm/metadatum.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Packages + module Rpm + class Metadatum < ApplicationRecord + self.primary_key = :package_id + + belongs_to :package, -> { where(package_type: :rpm) }, inverse_of: :rpm_metadatum + + validates :package, presence: true + + validates :epoch, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + validates :release, + presence: true, + length: { maximum: 128 } + + validates :summary, + presence: true, + length: { maximum: 1000 } + + validates :description, + presence: true, + length: { maximum: 5000 } + + validates :arch, + presence: true, + length: { maximum: 255 } + + validates :license, + allow_nil: true, + length: { maximum: 1000 } + + validates :url, + allow_nil: true, + length: { maximum: 1000 } + + validate :rpm_package_type + + private + + def rpm_package_type + return if package&.rpm? + + errors.add(:base, _('Package type must be RPM')) + end + end + end +end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 2e25839c47f..16d5492a65e 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -33,6 +33,7 @@ class PagesDomain < ApplicationRecord validate :validate_pages_domain validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? } + validate :validate_custom_domain_count_per_project, on: :create default_value_for(:auto_ssl_enabled, allows_nil: false) { ::Gitlab::LetsEncrypt.enabled? } default_value_for :scope, allows_nil: false, value: :project @@ -57,6 +58,7 @@ class PagesDomain < ApplicationRecord where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold)))) end + scope :verified, -> { where.not(verified_at: nil) } scope :need_auto_ssl_renewal, -> do enabled_and_not_failed = where(auto_ssl_enabled: true, auto_ssl_failed: false) @@ -224,6 +226,16 @@ class PagesDomain < ApplicationRecord self.auto_ssl_failed = false end + def validate_custom_domain_count_per_project + return unless project + + unless project.can_create_custom_domains? + self.errors.add( + :base, + _("This project reached the limit of custom domains. (Max %d)") % Gitlab::CurrentSettings.max_pages_custom_domains_per_project) + end + end + private def pages_deployed? diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 7e6e366f8da..9ed25c56ed6 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -24,6 +24,8 @@ class PersonalAccessToken < ApplicationRecord scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) } scope :expired_today_and_not_notified, -> { where(["revoked = false AND expires_at = CURRENT_DATE AND after_expiry_notification_delivered = false"]) } scope :inactive, -> { where("revoked = true OR expires_at < CURRENT_DATE") } + scope :created_before, -> (date) { where("personal_access_tokens.created_at < :date", date: date) } + scope :last_used_before_or_unused, -> (date) { where("personal_access_tokens.created_at < :date AND (last_used_at < :date OR last_used_at IS NULL)", date: date) } scope :with_impersonation, -> { where(impersonation: true) } scope :without_impersonation, -> { where(impersonation: false) } scope :revoked, -> { where(revoked: true) } diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index 3461104ae35..f22a63ee980 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -81,8 +81,8 @@ class PoolRepository < ApplicationRecord object_pool.link(repository.raw) end - def unlink_repository(repository) - repository.disconnect_alternates + def unlink_repository(repository, disconnect: true) + repository.disconnect_alternates if disconnect if member_projects.where.not(id: repository.project.id).exists? true diff --git a/app/models/preloaders/environments/deployment_preloader.rb b/app/models/preloaders/environments/deployment_preloader.rb index 251d1837f19..84aa7bc834f 100644 --- a/app/models/preloaders/environments/deployment_preloader.rb +++ b/app/models/preloaders/environments/deployment_preloader.rb @@ -41,11 +41,11 @@ module Preloaders environment.association(association_name).target = associated_deployment environment.association(association_name).loaded! - if associated_deployment - # `last?` in DeploymentEntity requires this environment to be loaded - associated_deployment.association(:environment).target = environment - associated_deployment.association(:environment).loaded! - end + next unless associated_deployment + + # `last?` in DeploymentEntity requires this environment to be loaded + associated_deployment.association(:environment).target = environment + associated_deployment.association(:environment).loaded! end end end diff --git a/app/models/preloaders/group_policy_preloader.rb b/app/models/preloaders/group_policy_preloader.rb index 44030140ce3..23632a9b6c2 100644 --- a/app/models/preloaders/group_policy_preloader.rb +++ b/app/models/preloaders/group_policy_preloader.rb @@ -17,4 +17,4 @@ module Preloaders end end -Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader') +Preloaders::GroupPolicyPreloader.prepend_mod diff --git a/app/models/preloaders/project_policy_preloader.rb b/app/models/preloaders/project_policy_preloader.rb new file mode 100644 index 00000000000..fe9db3464c7 --- /dev/null +++ b/app/models/preloaders/project_policy_preloader.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Preloaders + class ProjectPolicyPreloader + def initialize(projects, current_user) + @projects = projects + @current_user = current_user + end + + def execute + return if projects.is_a?(ActiveRecord::NullRelation) + + ActiveRecord::Associations::Preloader.new.preload(projects, { group: :route, namespace: :owner }) + ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute + end + + private + + attr_reader :projects, :current_user + end +end + +Preloaders::ProjectPolicyPreloader.prepend_mod diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb new file mode 100644 index 00000000000..8d04e71774c --- /dev/null +++ b/app/models/preloaders/project_root_ancestor_preloader.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Preloaders + class ProjectRootAncestorPreloader + def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = []) + @projects = projects + @namespace_sti_name = namespace_sti_name + @root_ancestor_preloads = root_ancestor_preloads + end + + def execute + return if @projects.is_a?(ActiveRecord::NullRelation) + return unless ::Feature.enabled?(:use_traversal_ids) + + root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id") + .select('namespaces.*, root_query.id as source_id') + + root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any? + + root_ancestors_by_id = root_query.group_by(&:source_id) + + ActiveRecord::Associations::Preloader.new.preload(@projects, :namespace) + @projects.each do |project| + project.namespace.root_ancestor = root_ancestors_by_id[project.id]&.first + end + end + + private + + def join_sql + @projects + .joins(@namespace_sti_name) + .select('projects.id, namespaces.traversal_ids[1] as root_id') + .to_sql + end + end +end diff --git a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb index 99a31a620c5..f32184f168d 100644 --- a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb +++ b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb @@ -51,4 +51,4 @@ module Preloaders end end -# Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod +Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod diff --git a/app/models/project.rb b/app/models/project.rb index 0c49cc24a8d..c5fad189f87 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -46,13 +46,9 @@ class Project < ApplicationRecord extend Gitlab::ConfigHelper - ignore_columns :container_registry_enabled, remove_after: '2021-09-22', remove_with: '14.4' - BoardLimitExceeded = Class.new(StandardError) ExportLimitExceeded = Class.new(StandardError) - ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4' - ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4' ignore_columns :build_coverage_regex, remove_after: '2022-10-22', remove_with: '15.5' STATISTICS_ATTRIBUTE = 'repositories_count' @@ -123,6 +119,7 @@ class Project < ApplicationRecord before_validation :ensure_project_namespace_in_sync before_validation :set_package_registry_access_level, if: :packages_enabled_changed? + before_validation :remove_leading_spaces_on_name after_save :update_project_statistics, if: :saved_change_to_namespace_id? @@ -453,7 +450,7 @@ class Project < ApplicationRecord :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, - :releases_access_level, + :monitor_access_level, :releases_access_level, to: :project_feature, allow_nil: true delegate :show_default_award_emojis, :show_default_award_emojis=, @@ -461,6 +458,9 @@ class Project < ApplicationRecord :warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=, to: :project_setting, allow_nil: true + delegate :show_diff_preview_in_email, :show_diff_preview_in_email=, :show_diff_preview_in_email?, + to: :project_setting + delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting delegate :squash_option, :squash_option=, to: :project_setting delegate :mr_default_target_self, :mr_default_target_self=, to: :project_setting @@ -565,26 +565,29 @@ class Project < ApplicationRecord scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) } scope :sorted_by_similarity_desc, -> (search, include_in_select: false) do - order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [ - { column: arel_table["path"], multiplier: 1 }, - { column: arel_table["name"], multiplier: 0.7 }, - { column: arel_table["description"], multiplier: 0.2 } - ]) - - order = Gitlab::Pagination::Keyset::Order.build([ - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'similarity', - column_expression: order_expression, - order_expression: order_expression.desc, - order_direction: :desc, - distinct: false, - add_to_projections: true - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: Project.arel_table[:id].desc - ) - ]) + order_expression = Gitlab::Database::SimilarityScore.build_expression( + search: search, + rules: [ + { column: arel_table["path"], multiplier: 1 }, + { column: arel_table["name"], multiplier: 0.7 }, + { column: arel_table["description"], multiplier: 0.2 } + ]) + + order = Gitlab::Pagination::Keyset::Order.build( + [ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'similarity', + column_expression: order_expression, + order_expression: order_expression.desc, + order_direction: :desc, + distinct: false, + add_to_projections: true + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: Project.arel_table[:id].desc + ) + ]) order.apply_cursor_conditions(reorder(order)) end @@ -611,7 +614,7 @@ class Project < ApplicationRecord scope :include_integration, -> (integration_association_name) { includes(integration_association_name) } scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) } scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) } - scope :with_shared_runners, -> { where(shared_runners_enabled: true) } + scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) } scope :inside_path, ->(path) do # We need routes alias rs for JOIN so it does not conflict with # includes(:route) which we use in ProjectsFinder. @@ -1163,7 +1166,7 @@ class Project < ApplicationRecord latest_pipeline = ci_pipelines.latest_successful_for_ref(ref) return unless latest_pipeline - latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name) + latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name) end def latest_successful_build_for_sha(job_name, sha) @@ -1172,7 +1175,7 @@ class Project < ApplicationRecord latest_pipeline = ci_pipelines.latest_successful_for_sha(sha) return unless latest_pipeline - latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name) + latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name) end def latest_successful_build_for_ref!(job_name, ref = default_branch) @@ -1564,9 +1567,7 @@ class Project < ApplicationRecord end def disabled_integrations - disabled_integrations = [] - disabled_integrations << 'shimo' unless Feature.enabled?(:shimo_integration, self) - disabled_integrations + [] end def find_or_initialize_integration(name) @@ -2369,28 +2370,6 @@ class Project < ApplicationRecord .first end - def ci_variables_for(ref:, environment: nil) - cache_key = "ci_variables_for:project:#{self&.id}:ref:#{ref}:environment:#{environment}" - - ::Gitlab::SafeRequestStore.fetch(cache_key) do - uncached_ci_variables_for(ref: ref, environment: environment) - end - end - - def uncached_ci_variables_for(ref:, environment: nil) - result = if protected_for?(ref) - variables - else - variables.unprotected - end - - if environment - result.on_environment(environment) - else - result.where(environment_scope: '*') - end - end - def protected_for?(ref) raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref) @@ -2582,10 +2561,7 @@ class Project < ApplicationRecord def badges return project_badges unless group - Badge.from_union([ - project_badges, - GroupBadge.where(group: group.self_and_ancestors) - ]) + Badge.from_union([project_badges, GroupBadge.where(group: group.self_and_ancestors)]) end def merge_requests_allowing_push_to_user(user) @@ -2631,11 +2607,7 @@ class Project < ApplicationRecord def gitlab_deploy_token strong_memoize(:gitlab_deploy_token) do - if Feature.enabled?(:ci_variable_for_group_gitlab_deploy_token, self) - deploy_tokens.gitlab_deploy_token || group&.gitlab_deploy_token - else - deploy_tokens.gitlab_deploy_token - end + deploy_tokens.gitlab_deploy_token || group&.gitlab_deploy_token end end @@ -2693,7 +2665,12 @@ class Project < ApplicationRecord end def leave_pool_repository - pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil) + return if pool_repository.blank? + + # Disconnecting the repository can be expensive, so let's skip it if + # this repository is being deleted anyway. + pool_repository.unlink_repository(repository, disconnect: !pending_delete?) + update_column(:pool_repository_id, nil) end def link_pool_repository @@ -3045,10 +3022,24 @@ class Project < ApplicationRecord licensed_feature_available?(:security_training) end + def packages_policy_subject + if Feature.enabled?(:read_package_policy_rule, group) + ::Packages::Policies::Project.new(self) + else + self + end + end + def destroy_deployment_by_id(deployment_id) deployments.where(id: deployment_id).fast_destroy_all end + def can_create_custom_domains? + return true if Gitlab::CurrentSettings.max_pages_custom_domains_per_project == 0 + + pages_domains.count < Gitlab::CurrentSettings.max_pages_custom_domains_per_project + end + private # overridden in EE @@ -3300,6 +3291,10 @@ class Project < ApplicationRecord end end + def remove_leading_spaces_on_name + name&.lstrip! + end + def set_package_registry_access_level return if !project_feature || project_feature.package_registry_access_level_changed? diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 8623e477c06..dad8aaf0625 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -17,6 +17,7 @@ class ProjectFeature < ApplicationRecord pages metrics_dashboard analytics + monitor operations security_and_compliance container_registry diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 59d2e3deb4f..f5c346eda30 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProjectSetting < ApplicationRecord + include ::Gitlab::Utils::StrongMemoize + ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze belongs_to :project, inverse_of: :project_setting @@ -47,6 +49,15 @@ class ProjectSetting < ApplicationRecord end end + def show_diff_preview_in_email? + if project.group + super && project.group&.show_diff_preview_in_email? + else + !!super + end + end + strong_memoize_attr :show_diff_preview_in_email + private def validates_mr_default_target_self diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index a0af1b47d01..a91e0291438 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -11,9 +11,10 @@ class ProjectStatistics < ApplicationRecord default_value_for :snippets_size, 0 counter_attribute :build_artifacts_size - counter_attribute :storage_size counter_attribute_after_flush do |project_statistic| + project_statistic.refresh_storage_size! + Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id) end @@ -21,7 +22,6 @@ class ProjectStatistics < ApplicationRecord COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size, :container_registry_size].freeze INCREMENTABLE_COLUMNS = { - build_artifacts_size: %i[storage_size], packages_size: %i[storage_size], pipeline_artifacts_size: %i[storage_size], snippets_size: %i[storage_size] @@ -109,21 +109,25 @@ class ProjectStatistics < ApplicationRecord self.storage_size = storage_size end - # Since this incremental update method does not call update_storage_size above, - # we have to update the storage_size here as additional column. - # Additional columns are updated depending on key => [columns], which allows - # to update statistics which are and also those which aren't included in storage_size - # or any other additional summary column in the future. + def refresh_storage_size! + update_storage_size + save! + end + + # Since this incremental update method does not call update_storage_size above through before_save, + # we have to update the storage_size separately. + # + # For counter attributes, storage_size will be refreshed after the counter is flushed, + # through counter_attribute_after_flush + # + # For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS def self.increment_statistic(project, key, amount) - raise ArgumentError, "Cannot increment attribute: #{key}" unless INCREMENTABLE_COLUMNS.key?(key) + raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key) return if amount == 0 project.statistics.try do |project_statistics| - if project_statistics.counter_attribute_enabled?(key) - statistics_to_increment = [key] + INCREMENTABLE_COLUMNS[key].to_a - statistics_to_increment.each do |statistic| - project_statistics.delayed_increment_counter(statistic, amount) - end + if counter_attribute_enabled?(key) + project_statistics.delayed_increment_counter(key, amount) else legacy_increment_statistic(project, key, amount) end @@ -149,6 +153,10 @@ class ProjectStatistics < ApplicationRecord update_all(updates.join(', ')) end + def self.incrementable_attribute?(key) + INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key) + end + private def schedule_namespace_aggregation_worker diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb index dee4afdefa6..e66e1d5b42f 100644 --- a/app/models/projects/build_artifacts_size_refresh.rb +++ b/app/models/projects/build_artifacts_size_refresh.rb @@ -2,6 +2,7 @@ module Projects class BuildArtifactsSizeRefresh < ApplicationRecord + include AfterCommitQueue include BulkInsertSafe STALE_WINDOW = 2.hours @@ -52,6 +53,8 @@ module Projects scope :remaining, -> { with_state(:created, :pending).or(stale) } scope :processing_queue, -> { remaining.order(state: :desc) } + after_destroy :schedule_namespace_aggregation_worker + def self.enqueue_refresh(projects) now = Time.zone.now @@ -93,5 +96,13 @@ module Projects def started? !created? end + + private + + def schedule_namespace_aggregation_worker + run_after_commit do + Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) + end + end end end diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb index b0f138714a0..3155eede2bd 100644 --- a/app/models/projects/topic.rb +++ b/app/models/projects/topic.rb @@ -18,9 +18,11 @@ module Projects scope :without_assigned_projects, -> { where(total_projects_count: 0) } scope :order_by_non_private_projects_count, -> { order(non_private_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'] } - ]) + order_expression = Gitlab::Database::SimilarityScore.build_expression( + search: search, + rules: [ + { column: arel_table['name'] } + ]) reorder(order_expression.desc, arel_table['non_private_projects_count'].desc, arel_table['id']) end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 76c277e4b86..b3a918d8952 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -25,10 +25,12 @@ class ProtectedBranch < ApplicationRecord end # Check if branch name is marked as protected in the system - def self.protected?(project, ref_name, dry_run: true) + def self.protected?(project, ref_name) return true if project.empty_repo? && project.default_branch_protected? return false if ref_name.blank? + dry_run = Feature.disabled?(:rely_on_protected_branches_cache, project) + new_cache_result = new_cache(project, ref_name, dry_run: dry_run) return new_cache_result unless new_cache_result.nil? diff --git a/app/models/repository.rb b/app/models/repository.rb index 26c3b01a46e..ee1bea0e8d2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -194,6 +194,18 @@ class Repository CommitCollection.new(container, commits, ref) end + def list_commits_by(query, ref, author: nil, before: nil, after: nil, limit: 1000) + return [] unless exists? + return [] unless has_visible_content? + return [] unless query.present? && ref.present? + + commits = raw_repository.list_commits_by( + query, ref, author: author, before: before, after: after, limit: limit).map do |c| + commit(c) + end + CommitCollection.new(container, commits, ref) + end + def find_branch(name) raw_repository.find_branch(name) end @@ -779,8 +791,8 @@ class Repository raw_repository.branch_names_contains_sha(sha) end - def tag_names_contains(sha) - raw_repository.tag_names_contains_sha(sha) + def tag_names_contains(sha, limit: 0) + raw_repository.tag_names_contains_sha(sha, limit: limit) end def local_branches @@ -796,7 +808,7 @@ class Repository def create_dir(user, path, **options) options[:actions] = [{ action: :create_dir, file_path: path }] - multi_action(user, **options) + commit_files(user, **options) end def create_file(user, path, content, **options) @@ -808,7 +820,7 @@ class Repository options[:actions].push({ action: :chmod, file_path: path, execute_filemode: execute_filemode }) end - multi_action(user, **options) + commit_files(user, **options) end def update_file(user, path, content, **options) @@ -823,13 +835,13 @@ class Repository options[:actions].push({ action: :chmod, file_path: path, execute_filemode: execute_filemode }) end - multi_action(user, **options) + commit_files(user, **options) end def delete_file(user, path, **options) options[:actions] = [{ action: :delete, file_path: path }] - multi_action(user, **options) + commit_files(user, **options) end def with_cache_hooks @@ -843,14 +855,14 @@ class Repository result.newrev end - def multi_action(user, **options) + def commit_files(user, **options) start_project = options.delete(:start_project) if start_project options[:start_repository] = start_project.repository.raw_repository end - with_cache_hooks { raw.multi_action(user, **options) } + with_cache_hooks { raw.commit_files(user, **options) } end def merge(user, source_sha, merge_request, message) diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb index 689a9d8a8ae..6ebb9d5f176 100644 --- a/app/models/resource_state_event.rb +++ b/app/models/resource_state_event.rb @@ -3,8 +3,9 @@ class ResourceStateEvent < ResourceEvent include IssueResourceEvent include MergeRequestResourceEvent + include Importable - validate :exactly_one_issuable + validate :exactly_one_issuable, unless: :importing? belongs_to :source_merge_request, class_name: 'MergeRequest', foreign_key: :source_merge_request_id @@ -32,9 +33,9 @@ class ResourceStateEvent < ResourceEvent case state when 'closed' - issue_usage_counter.track_issue_closed_action(author: user) + issue_usage_counter.track_issue_closed_action(author: user, project: issue.project) when 'reopened' - issue_usage_counter.track_issue_reopened_action(author: user) + issue_usage_counter.track_issue_reopened_action(author: user, project: issue.project) else # no-op, nothing to do, not a state we're tracking end diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb index db87ff09159..26bf2a225d4 100644 --- a/app/models/resource_timebox_event.rb +++ b/app/models/resource_timebox_event.rb @@ -5,8 +5,9 @@ class ResourceTimeboxEvent < ResourceEvent include IssueResourceEvent include MergeRequestResourceEvent + include Importable - validate :exactly_one_issuable + validate :exactly_one_issuable, unless: :importing? enum action: { add: 1, @@ -34,7 +35,8 @@ class ResourceTimeboxEvent < ResourceEvent case self when ResourceMilestoneEvent - Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user) + Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user, + project: issue.project) else # no-op end diff --git a/app/models/route.rb b/app/models/route.rb index 2f6b0a8e8f1..f2fe1664f9e 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -39,17 +39,17 @@ class Route < ApplicationRecord attributes[:name] = route.name.sub(name_before_last_save, name) end - if attributes.present? - old_path = route.path + next if attributes.empty? - # Callbacks must be run manually - route.update_columns(attributes.merge(updated_at: Time.current)) + old_path = route.path - # We are not calling route.delete_conflicting_redirects here, in hopes - # of avoiding deadlocks. The parent (self, in this method) already - # called it, which deletes conflicts for all descendants. - route.create_redirect(old_path) if attributes[:path] - end + # Callbacks must be run manually + route.update_columns(attributes.merge(updated_at: Time.current)) + + # We are not calling route.delete_conflicting_redirects here, in hopes + # of avoiding deadlocks. The parent (self, in this method) already + # called it, which deletes conflicts for all descendants. + route.create_redirect(old_path) if attributes[:path] end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 943d09d983b..9b7c37dd23e 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -84,7 +84,7 @@ class Snippet < ApplicationRecord participant :notes_with_associations attr_spammable :title, spam_title: true - attr_spammable :content, spam_description: true + attr_spammable :description, spam_description: true attr_encrypted :secret_token, key: Settings.attr_encrypted_db_key_base_truncated, @@ -269,13 +269,7 @@ class Snippet < ApplicationRecord def check_for_spam?(user:) visibility_level_changed?(to: Snippet::PUBLIC) || - (public? && (title_changed? || content_changed?)) - end - - # snippets are the biggest sources of spam - override :allow_possible_spam? - def allow_possible_spam? - false + (public? && (title_changed? || description_changed?)) end def spammable_entity_type diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index 5ac159d9615..a959ad4d548 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -31,7 +31,7 @@ class SnippetRepository < ApplicationRecord options[:actions] = transform_file_entries(files) - capture_git_error { repository.multi_action(user, **options) } + capture_git_error { repository.commit_files(user, **options) } ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index cc389dbe3f4..4e86036952b 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -25,6 +25,7 @@ class SystemNoteMetadata < ApplicationRecord tag due_date start_date_or_due_date pinned_embed cherry_pick health_status approved unapproved status alert_issue_added relate unrelate new_alert_added severity attention_requested attention_request_removed contact timeline_event + issue_type relate_to_child unrelate_from_child relate_to_parent unrelate_from_parent ].freeze validates :note, presence: true, unless: :importing? diff --git a/app/models/todo.rb b/app/models/todo.rb index d165e60e4c3..634fa9e7eda 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -96,10 +96,11 @@ class Todo < ApplicationRecord def for_group_ids_and_descendants(group_ids) groups = Group.groups_including_descendants_by(group_ids) - from_union([ - for_project(Project.for_group(groups)), - for_group(groups) - ]) + from_union( + [ + for_project(Project.for_group(groups)), + for_group(groups) + ]) end # Returns `true` if the current user has any todos for the given target with the optional given state. diff --git a/app/models/user.rb b/app/models/user.rb index afee2d70844..8825c18ea48 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -92,7 +92,6 @@ class User < ApplicationRecord include ForcedEmailConfirmation include RequireEmailVerification - MINIMUM_INACTIVE_DAYS = 90 MINIMUM_DAYS_CREATED = 7 # Override Devise::Models::Trackable#update_tracked_fields! @@ -262,6 +261,7 @@ class User < ApplicationRecord presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } validates :username, presence: true + validate :check_password_weakness, if: :encrypted_password_changed? validates :namespace, presence: true validate :namespace_move_dir_allowed, if: :username_changed? @@ -488,7 +488,7 @@ class User < ApplicationRecord scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) } scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) } scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first, arel_table[:id].desc) } - scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) } + scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', Gitlab::CurrentSettings.deactivate_dormant_users_period.day.ago.to_date) } scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) } scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) } scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) } @@ -697,28 +697,29 @@ class User < ApplicationRecord scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query) scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit])) - order = Gitlab::Pagination::Keyset::Order.build([ - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'users_match_priority', - order_expression: sanitized_order_sql.asc, - add_to_projections: true, - distinct: false - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'users_name', - order_expression: arel_table[:name].asc, - add_to_projections: true, - nullable: :not_nullable, - distinct: false - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'users_id', - order_expression: arel_table[:id].asc, - add_to_projections: true, - nullable: :not_nullable, - distinct: true - ) - ]) + order = Gitlab::Pagination::Keyset::Order.build( + [ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'users_match_priority', + order_expression: sanitized_order_sql.asc, + add_to_projections: true, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'users_name', + order_expression: arel_table[:name].asc, + add_to_projections: true, + nullable: :not_nullable, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'users_id', + order_expression: arel_table[:id].asc, + add_to_projections: true, + nullable: :not_nullable, + distinct: true + ) + ]) scope.reorder(order) end @@ -1358,10 +1359,11 @@ class User < ApplicationRecord end def accessible_deploy_keys - DeployKey.from_union([ - DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)), - DeployKey.are_public - ]) + DeployKey.from_union( + [ + DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)), + DeployKey.are_public + ]) end def created_by @@ -1662,10 +1664,11 @@ class User < ApplicationRecord strong_memoize(:forkable_namespaces) do personal_namespace = Namespace.where(id: namespace_id) - Namespace.from_union([ - manageable_groups(include_groups_with_developer_maintainer_access: true), - personal_namespace - ]) + Namespace.from_union( + [ + manageable_groups(include_groups_with_developer_maintainer_access: true), + personal_namespace + ]) end end @@ -2072,6 +2075,7 @@ class User < ApplicationRecord callout_dismissed?(callout, ignore_dismissal_earlier_than) end + # Deprecated: do not use. See: https://gitlab.com/gitlab-org/gitlab/-/issues/371017 def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil) source_feature_name = "#{feature_name}_#{namespace.id}" callout = namespace_callouts_by_feature_name[source_feature_name] @@ -2151,10 +2155,6 @@ class User < ApplicationRecord end end - def mr_attention_requests_enabled? - Feature.enabled?(:mr_attention_requests, self) - end - def account_age_in_days (Date.current - created_at.to_date).to_i end @@ -2247,10 +2247,11 @@ class User < ApplicationRecord end def authorized_groups_without_shared_membership - Group.from_union([ - groups.select(*Namespace.cached_column_list), - authorized_projects.joins(:namespace).select(*Namespace.cached_column_list) - ]) + Group.from_union( + [ + groups.select(*Namespace.cached_column_list), + authorized_projects.joins(:namespace).select(*Namespace.cached_column_list) + ]) end def authorized_groups_with_shared_membership @@ -2260,10 +2261,10 @@ class User < ApplicationRecord Group .with(cte.to_arel) .from_union([ - Group.from(cte_alias), - Group.joins(:shared_with_group_links) - .where(group_group_links: { shared_with_group_id: Group.from(cte_alias) }) - ]) + Group.from(cte_alias), + Group.joins(:shared_with_group_links) + .where(group_group_links: { shared_with_group_id: Group.from(cte_alias) }) + ]) end def default_private_profile_to_false @@ -2314,6 +2315,14 @@ class User < ApplicationRecord errors.add(:username, _('ending with a reserved file extension is not allowed.')) end + def check_password_weakness + if Feature.enabled?(:block_weak_passwords) && + password.present? && + Security::WeakPasswords.weak_for_user?(password, self) + errors.add(:password, _('must not contain commonly used combinations of words and letters')) + end + end + def groups_with_developer_maintainer_project_access project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS] @@ -2325,7 +2334,7 @@ class User < ApplicationRecord end def no_recent_activity? - last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i + last_active_at.to_i <= Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_i end def update_highest_role? diff --git a/app/models/user_status.rb b/app/models/user_status.rb index dee976a4497..0c66f465356 100644 --- a/app/models/user_status.rb +++ b/app/models/user_status.rb @@ -29,6 +29,10 @@ class UserStatus < ApplicationRecord cache_markdown_field :message, pipeline: :emoji + def clear_status_after + clear_status_at + end + def clear_status_after=(value) self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now end diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 7b5c7fef7ba..03841ee48fa 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -43,12 +43,11 @@ module Users verification_reminder: 40, # EE-only ci_deprecation_warning_for_types_keyword: 41, security_training_feature_promotion: 42, # EE-only - storage_enforcement_banner_first_enforcement_threshold: 43, - storage_enforcement_banner_second_enforcement_threshold: 44, - storage_enforcement_banner_third_enforcement_threshold: 45, - storage_enforcement_banner_fourth_enforcement_threshold: 46, - attention_requests_top_nav: 47, - attention_requests_side_nav: 48, + storage_enforcement_banner_first_enforcement_threshold: 43, # EE-only + storage_enforcement_banner_second_enforcement_threshold: 44, # EE-only + storage_enforcement_banner_third_enforcement_threshold: 45, # EE-only + storage_enforcement_banner_fourth_enforcement_threshold: 46, # EE-only + # 47 and 48 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95446 # 49 was removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91533 # because the banner was no longer relevant. # Records will be migrated with https://gitlab.com/gitlab-org/gitlab/-/issues/367293 @@ -61,7 +60,8 @@ module Users namespace_storage_limit_banner_warning_threshold: 56, # EE-only namespace_storage_limit_banner_alert_threshold: 57, # EE-only namespace_storage_limit_banner_error_threshold: 58, # EE-only - project_quality_summary_feedback: 59 # EE-only + project_quality_summary_feedback: 59, # EE-only + merge_request_settings_moved_callout: 60 } validates :feature_name, diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 998a5deb0fd..272f31aa9ce 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -21,5 +21,11 @@ module Users network: network ).order(credit_card_validated_at: :desc).includes(:user) end + + def similar_holder_names_count + return 0 unless holder_name + + self.class.where('lower(holder_name) = lower(:value)', value: holder_name).count + end end end diff --git a/app/models/users/ghost_user_migration.rb b/app/models/users/ghost_user_migration.rb new file mode 100644 index 00000000000..1d93498e88b --- /dev/null +++ b/app/models/users/ghost_user_migration.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Users + class GhostUserMigration < ApplicationRecord + self.table_name = 'ghost_user_migrations' + + belongs_to :user + belongs_to :initiator_user, class_name: 'User' + + validates :user_id, presence: true + end +end diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb index 70498ae83e0..3e3e424e9c9 100644 --- a/app/models/users/group_callout.rb +++ b/app/models/users/group_callout.rb @@ -11,10 +11,10 @@ module Users enum feature_name: { invite_members_banner: 1, approaching_seat_count_threshold: 2, # EE-only - storage_enforcement_banner_first_enforcement_threshold: 3, - storage_enforcement_banner_second_enforcement_threshold: 4, - storage_enforcement_banner_third_enforcement_threshold: 5, - storage_enforcement_banner_fourth_enforcement_threshold: 6, + storage_enforcement_banner_first_enforcement_threshold: 3, # EE-only + storage_enforcement_banner_second_enforcement_threshold: 4, # EE-only + storage_enforcement_banner_third_enforcement_threshold: 5, # EE-only + storage_enforcement_banner_fourth_enforcement_threshold: 6, # EE-only preview_user_over_limit_free_plan_alert: 7, # EE-only user_reached_limit_free_plan_alert: 8, # EE-only free_group_limited_alert: 9, # EE-only diff --git a/app/models/users/namespace_callout.rb b/app/models/users/namespace_callout.rb index a20a196a4ef..4e655a96b57 100644 --- a/app/models/users/namespace_callout.rb +++ b/app/models/users/namespace_callout.rb @@ -11,10 +11,10 @@ module Users enum feature_name: { invite_members_banner: 1, approaching_seat_count_threshold: 2, # EE-only - storage_enforcement_banner_first_enforcement_threshold: 3, - storage_enforcement_banner_second_enforcement_threshold: 4, - storage_enforcement_banner_third_enforcement_threshold: 5, - storage_enforcement_banner_fourth_enforcement_threshold: 6, + storage_enforcement_banner_first_enforcement_threshold: 3, # EE-only + storage_enforcement_banner_second_enforcement_threshold: 4, # EE-only + storage_enforcement_banner_third_enforcement_threshold: 5, # EE-only + storage_enforcement_banner_fourth_enforcement_threshold: 6, # EE-only preview_user_over_limit_free_plan_alert: 7, # EE-only user_reached_limit_free_plan_alert: 8, # EE-only web_hook_disabled: 9 diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb index ddc5f8fb4de..98dacbe394a 100644 --- a/app/models/users/project_callout.rb +++ b/app/models/users/project_callout.rb @@ -9,7 +9,9 @@ module Users belongs_to :project enum feature_name: { - awaiting_members_banner: 1 # EE-only + awaiting_members_banner: 1, # EE-only + web_hook_disabled: 2, + ultimate_feature_removal_banner: 3 } validates :project, presence: true diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 1549c099a64..9a514b82506 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -3,7 +3,7 @@ class UsersStarProject < ApplicationRecord include Sortable - belongs_to :project, counter_cache: :star_count, touch: true + belongs_to :project, counter_cache: :star_count belongs_to :user validates :user, presence: true diff --git a/app/models/wiki.rb b/app/models/wiki.rb index d28a73b644f..fac79a8194a 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -103,6 +103,17 @@ class Wiki def find_by_id(container_id) container_class.find_by_id(container_id)&.wiki end + + def sluggified_full_path(title, extension) + sluggified_title(title) + '.' + extension + end + + def sluggified_title(title) + title = Gitlab::EncodingHelper.encode_utf8_no_detect(title) + title = File.expand_path(title, '/') + title = Pathname.new(title).relative_path_from('/').to_s + title.tr(' ', '-') + end end def initialize(container, user = nil) @@ -206,10 +217,11 @@ class Wiki # # Returns an initialized WikiPage instance or nil def find_page(title, version = nil, load_content: true) - page_title, page_dir = page_title_and_dir(title) - - if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content) - WikiPage.new(self, page) + if find_page_with_repository_rpcs? + create_wiki_repository unless repository_exists? + find_page_with_repository_rpcs(title, version, load_content: load_content) + else + find_page_with_legacy_wiki_service(title, version, load_content: load_content) end end @@ -419,19 +431,83 @@ class Wiki end def sluggified_full_path(title, extension) - sluggified_title(title) + '.' + extension + self.class.sluggified_full_path(title, extension) end def sluggified_title(title) - utf8_encoded_title = Gitlab::EncodingHelper.encode_utf8_no_detect(title) + self.class.sluggified_title(title) + end - sanitized_title(utf8_encoded_title).tr(' ', '-') + def canonicalize_filename(filename) + Gitlab::Git::Wiki::GollumSlug.canonicalize_filename(filename) end - def sanitized_title(title) - clean_absolute_path = File.expand_path(title, '/') + def find_page_with_legacy_wiki_service(title, version, load_content: false) + page_title, page_dir = page_title_and_dir(title) + + if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content) + WikiPage.new(self, page) + end + end + + def find_matched_file(title, version) + escaped_path = RE2::Regexp.escape(sluggified_title(title)) + # We could not use ALLOWED_EXTENSIONS_REGEX constant or similar regexp with + # Regexp.union. The result combination complicated modifiers: + # /(?i-mx:md|mkdn?|mdown|markdown)|(?i-mx:rdoc).../ + # Regexp used by Gitaly is Go's Regexp package. It does not support those + # features. So, we have to compose another more-friendly regexp to pass to + # Gitaly side. + extension_regexp = Wiki::MARKUPS.map { |_, format| format[:extension_regex].source }.join("|") + path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{extension_regexp})$") + + matched_files = repository.search_files_by_regexp(path_regexp, version) + return if matched_files.blank? + + Gitlab::EncodingHelper.encode_utf8_no_detect(matched_files.first) + end + + def find_page_format(path) + ext = File.extname(path).downcase[1..] + MARKUPS.find { |_, markup| markup[:extension_regex].match?(ext) }&.first + end + + def check_page_historical(path, commit) + repository.last_commit_for_path('HEAD', path).id != commit.id + end + + def find_page_with_repository_rpcs(title, version, load_content: true) + version = version.presence || 'HEAD' + path = find_matched_file(title, version) + return if path.blank? + + blob_options = load_content ? {} : { limit: 0 } + blob = repository.blob_at(version, path, **blob_options) + commit = repository.commit(blob.commit_id) + format = find_page_format(path) + + page = Gitlab::Git::WikiPage.new( + url_path: sluggified_title(path.sub(/\.[^.]+\z/, "")), + title: canonicalize_filename(path), + format: format, + path: sluggified_title(path), + raw_data: blob.data, + name: canonicalize_filename(path), + historical: version == 'HEAD' ? false : check_page_historical(path, commit), + version: Gitlab::Git::WikiPageVersion.new(commit, format) + ) + WikiPage.new(self, page) + end + + def find_page_with_repository_rpcs? + group = + if container.is_a?(::Group) + container + else + container.group + end - Pathname.new(clean_absolute_path).relative_path_from('/').to_s + Feature.enabled?(:wiki_find_page_with_normal_repository_rpcs, group, type: :development) end end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 451359c1f85..05e45fa5b29 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -37,11 +37,11 @@ class WorkItem < Issue override :parent_link_confidentiality def parent_link_confidentiality if confidential? && work_item_children.public_only.exists? - errors.add(:confidential, _('confidential parent can not be used if there are non-confidential children.')) + errors.add(:base, _('A confidential work item cannot have a parent that already has non-confidential children.')) end if !confidential? && work_item_parent&.confidential? - errors.add(:confidential, _('associated parent is confidential and can not have non-confidential children.')) + errors.add(:base, _('A non-confidential work item cannot have a confidential parent.')) end end diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb index 1e84d172bef..ec3b7957c79 100644 --- a/app/models/work_items/widgets/description.rb +++ b/app/models/work_items/widgets/description.rb @@ -3,7 +3,13 @@ module WorkItems module Widgets class Description < Base - delegate :description, to: :work_item + delegate :description, :edited?, :last_edited_at, to: :work_item + + def last_edited_by + return unless work_item.edited? + + work_item.last_edited_by + end end end end |