diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 13:34:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 13:34:06 +0300 |
commit | 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch) | |
tree | d7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/models | |
parent | 446d496a6d000c73a304be52587cd9bbc7493136 (diff) |
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/models')
125 files changed, 1443 insertions, 486 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb index dded0eb1dc3..823685f78f4 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -1,5 +1,24 @@ # frozen_string_literal: true +# Backing store for GitLab session data. +# +# The raw session information is stored by the Rails session store +# (config/initializers/session_store.rb). These entries are accessible by the +# rack_key_name class method and consistute the base of the session data +# entries. All other entries in the session store can be traced back to these +# entries. +# +# After a user logs in (config/initializers/warden.rb) a further entry is made +# in Redis. This entry holds a record of the user's logged in session. These +# are accessible with the key_name(user_id, session_id) class method. These +# entries will expire. Lookups to these entries are lazilly cleaned on future +# user access. +# +# There is a reference to all sessions that belong to a specific user. A +# user may login through multiple browsers/devices and thus record multiple +# login sessions. These are accessible through the lookup_key_name(user_id) +# class method. +# class ActiveSession include ActiveModel::Model @@ -143,6 +162,10 @@ class ActiveSession list(user).reject(&:is_impersonated) end + def self.rack_key_name(session_id) + "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" + end + def self.key_name(user_id, session_id = '*') "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}" end @@ -197,7 +220,7 @@ class ActiveSession end def self.rack_session_keys(rack_session_ids) - rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" } + rack_session_ids.map { |session_id| rack_key_name(session_id)} end def self.raw_active_session_entries(redis, session_ids, user_id) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 5655ea4d4bf..33c058dab96 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -29,6 +29,21 @@ class ApplicationSetting < ApplicationRecord @repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze end + def self.kroki_formats_attributes + { + blockdiag: { + label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)' + }, + bpmn: { + label: 'BPMN' + }, + excalidraw: { + label: 'Excalidraw' + } + } + end + + store_accessor :kroki_formats, *ApplicationSetting.kroki_formats_attributes.keys, prefix: true store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true # Include here so it can override methods from @@ -43,6 +58,8 @@ class ApplicationSetting < ApplicationRecord serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize + serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize + # See https://gitlab.com/gitlab-org/gitlab/-/issues/300916 serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize cache_markdown_field :sign_in_text @@ -52,6 +69,7 @@ class ApplicationSetting < ApplicationRecord default_value_for :id, 1 default_value_for :repository_storages_weighted, {} + default_value_for :kroki_formats, {} chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds @@ -133,6 +151,8 @@ class ApplicationSetting < ApplicationRecord validate :validate_kroki_url, if: :kroki_enabled + validates :kroki_formats, json_schema: { filename: 'application_setting_kroki_formats' } + validates :plantuml_url, presence: true, if: :plantuml_enabled @@ -442,6 +462,13 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :notes_create_limit, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + validates :notes_create_limit_allowlist, + length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, + allow_nil: false + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, @@ -523,6 +550,10 @@ class ApplicationSetting < ApplicationRecord current_without_cache end + def self.find_or_create_without_cache + current_without_cache || create_from_defaults + end + # Due to the frequency with which settings are accessed, it is # likely that during a backup restore a running GitLab process # will insert a new `application_settings` row before the @@ -557,6 +588,25 @@ class ApplicationSetting < ApplicationRecord end end + kroki_formats_attributes.keys.each do |key| + define_method :"kroki_formats_#{key}=" do |value| + super(::Gitlab::Utils.to_boolean(value)) + end + end + + def kroki_format_supported?(diagram_type) + case diagram_type + when 'excalidraw' + return kroki_formats_excalidraw + when 'bpmn' + return kroki_formats_bpmn + end + + return kroki_formats_blockdiag if ::Gitlab::Kroki::BLOCKDIAG_FORMATS.include?(diagram_type) + + ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.include?(diagram_type) + end + private def parsed_grafana_url diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index b05355f14b4..2911ae6b1c8 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -100,6 +100,8 @@ module ApplicationSettingImplementation max_import_size: 0, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, mirror_available: true, + notes_create_limit: 300, + notes_create_limit_allowlist: [], notify_on_unknown_sign_in: true, outbound_local_requests_whitelist: [], password_authentication_enabled_for_git: true, @@ -174,6 +176,7 @@ module ApplicationSettingImplementation container_registry_expiration_policies_worker_capacity: 0, kroki_enabled: false, kroki_url: nil, + kroki_formats: { blockdiag: false, bpmn: false, excalidraw: false }, rate_limiting_response_text: nil } end @@ -269,13 +272,21 @@ module ApplicationSettingImplementation self.protected_paths = strings_to_array(values) end - def asset_proxy_whitelist=(values) + def notes_create_limit_allowlist_raw + array_to_string(self.notes_create_limit_allowlist) + end + + def notes_create_limit_allowlist_raw=(values) + self.notes_create_limit_allowlist = strings_to_array(values).map(&:downcase) + end + + def asset_proxy_allowlist=(values) values = strings_to_array(values) if values.is_a?(String) - # make sure we always whitelist the running host + # make sure we always allow the running host values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host) - self[:asset_proxy_whitelist] = values + self[:asset_proxy_allowlist] = values end def repository_storages diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index d1c0bb11dc8..32c9d44f836 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -55,15 +55,20 @@ class AuditEvent < ApplicationRecord end def author_name - lazy_author.name + author&.name end def formatted_details details.merge(details.slice(:from, :to).transform_values(&:to_s)) end + def author + lazy_author&.itself.presence || + ::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name])) + end + def lazy_author - BatchLoader.for(author_id).batch(default_value: default_author_value, replace_methods: false) do |author_ids, loader| + BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader| User.select(:id, :name, :username).where(id: author_ids).find_each do |user| loader.call(user.id, user) end diff --git a/app/models/board.rb b/app/models/board.rb index a57d101b30a..85fad762ebe 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -44,6 +44,14 @@ class Board < ApplicationRecord def scoped? false end + + def self.to_type + name.demodulize + end + + def to_type + self.class.to_type + end end Board.prepend_if_ee('EE::Board') diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index a4d0b7485ba..16224fde502 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -43,6 +43,8 @@ class BulkImports::Entity < ApplicationRecord validate :validate_parent_is_a_group, if: :parent validate :validate_imported_entity_type + validate :validate_destination_namespace_ascendency, if: :group_entity? + enum source_type: { group_entity: 0, project_entity: 1 } state_machine :status, initial: :created do @@ -107,4 +109,17 @@ class BulkImports::Entity < ApplicationRecord ) end end + + def validate_destination_namespace_ascendency + source = Group.find_by_full_path(source_full_path) + + return unless source + + if source.self_and_descendants.any? { |namespace| namespace.full_path == destination_namespace } + errors.add( + :destination_namespace, + s_('BulkImport|destination group cannot be part of the source group tree') + ) + end + end end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index ef3891908f7..ca400cebe4e 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -27,7 +27,7 @@ module Ci # rubocop:enable Cop/ActiveRecordSerialize state_machine :status do - after_transition [:created, :manual] => :pending do |bridge| + after_transition [:created, :manual, :waiting_for_resource] => :pending do |bridge| next unless bridge.downstream_project bridge.run_after_commit do @@ -156,6 +156,10 @@ module Ci false end + def any_unmet_prerequisites? + false + end + def expanded_environment_name end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5e3f42d7c2c..db151126caf 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -20,7 +20,6 @@ module Ci belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' - belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :builds belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id RUNNER_FEATURES = { @@ -38,7 +37,6 @@ module Ci DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD' has_one :deployment, as: :deployable, class_name: 'Deployment' - has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build @@ -230,27 +228,20 @@ module Ci end def with_preloads - preload(:job_artifacts_archive, :job_artifacts, project: [:namespace]) + preload(:job_artifacts_archive, :job_artifacts, :tags, project: [:namespace]) end end state_machine :status do event :enqueue do - transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :requires_resource? transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites? end event :enqueue_scheduled do - transition scheduled: :waiting_for_resource, if: :requires_resource? transition scheduled: :preparing, if: :any_unmet_prerequisites? transition scheduled: :pending end - event :enqueue_waiting_for_resource do - transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites? - transition waiting_for_resource: :pending - end - event :enqueue_preparing do transition preparing: :pending end @@ -279,23 +270,6 @@ module Ci build.scheduled_at = build.options_scheduled_at end - before_transition any => :waiting_for_resource do |build| - build.waiting_for_resource_at = Time.current - end - - before_transition on: :enqueue_waiting_for_resource do |build| - next unless build.requires_resource? - - build.resource_group.assign_resource_to(build) # If false is returned, it stops the transition - end - - after_transition any => :waiting_for_resource do |build| - build.run_after_commit do - Ci::ResourceGroups::AssignResourceFromResourceGroupWorker - .perform_async(build.resource_group_id) - end - end - before_transition on: :enqueue_preparing do |build| !build.any_unmet_prerequisites? # If false is returned, it stops the transition end @@ -328,16 +302,6 @@ module Ci end end - after_transition any => ::Ci::Build.completed_statuses do |build| - next unless build.resource_group_id.present? - next unless build.resource_group.release_resource_from(build) - - build.run_after_commit do - Ci::ResourceGroups::AssignResourceFromResourceGroupWorker - .perform_async(build.resource_group_id) - end - end - after_transition any => [:success, :failed, :canceled] do |build| build.run_after_commit do build.run_status_commit_hooks! @@ -403,7 +367,7 @@ module Ci def detailed_status(current_user) Gitlab::Ci::Status::Build::Factory - .new(self, current_user) + .new(self.present, current_user) .fabricate! end @@ -467,6 +431,11 @@ module Ci pipeline.builds.retried.where(name: self.name).count end + override :all_met_to_become_pending? + def all_met_to_become_pending? + super && !any_unmet_prerequisites? + end + def any_unmet_prerequisites? prerequisites.present? end @@ -501,10 +470,6 @@ module Ci end end - def requires_resource? - self.resource_group_id.present? - end - def has_environment? environment.present? end @@ -821,7 +786,9 @@ module Ci end def artifacts_file_for_type(type) - job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file + file_types = Ci::JobArtifact.associated_file_types_for(type) + file_types_ids = file_types&.map { |file_type| Ci::JobArtifact.file_types[file_type] } + job_artifacts.find_by(file_type: file_types_ids)&.file end def coverage_regex @@ -941,19 +908,12 @@ module Ci end def collect_coverage_reports!(coverage_report) - project_path, worktree_paths = if Feature.enabled?(:smart_cobertura_parser, project) - # If the flag is disabled, we intentionally pass nil - # for both project_path and worktree_paths to fallback - # to the non-smart behavior of the parser - [project.full_path, pipeline.all_worktree_paths] - end - each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob| Gitlab::Ci::Parsers.fabricate!(file_type).parse!( blob, coverage_report, - project_path: project_path, - worktree_paths: worktree_paths + project_path: project.full_path, + worktree_paths: pipeline.all_worktree_paths ) end @@ -1122,7 +1082,6 @@ module Ci end def conditionally_allow_failure!(exit_code) - return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? return unless exit_code if allowed_to_fail_with_code?(exit_code) diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb index a6abeb517c1..b50ecf99439 100644 --- a/app/models/ci/build_dependencies.rb +++ b/app/models/ci/build_dependencies.rb @@ -103,7 +103,7 @@ module Ci end def valid_local? - return true if Feature.enabled?(:ci_disable_validates_dependencies) + return true unless Gitlab::Ci::Features.validate_build_dependencies?(project) local.all?(&:valid_dependency?) end diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index ceefb6a8b8a..d4f9f78a1ac 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -77,6 +77,22 @@ module Ci end ## + # Sometime we need to ensure that the first read goes to a primary + # database, what is especially important in EE. This method does not + # change the behavior in CE. + # + def with_read_consistency(build, &block) + return yield unless consistent_reads_enabled?(build) + + ::Gitlab::Database::Consistency + .with_read_consistency(&block) + end + + def consistent_reads_enabled?(build) + Feature.enabled?(:gitlab_ci_trace_read_consistency, build.project, type: :development, default_enabled: true) + end + + ## # Sometimes we do not want to read raw data. This method makes it easier # to find attributes that are just metadata excluding raw data. # @@ -154,8 +170,8 @@ module Ci in_lock(lock_key, **lock_params) do # exclusive Redis lock is acquired first raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save? - self.reset.then do |chunk| # we ensure having latest lock_version - chunk.unsafe_persist_data! # we migrate the data and update data store + self.class.with_read_consistency(build) do + self.reset.then { |chunk| chunk.unsafe_persist_data! } end end rescue FailedToObtainLockError diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb index 27b579bf428..cbf0c0a1696 100644 --- a/app/models/ci/build_trace_chunks/fog.rb +++ b/app/models/ci/build_trace_chunks/fog.rb @@ -14,15 +14,7 @@ module Ci end def set_data(model, new_data) - if Feature.enabled?(:ci_live_trace_use_fog_attributes, default_enabled: true) - files.create(create_attributes(model, new_data)) - else - # TODO: Support AWS S3 server side encryption - files.create({ - key: key(model), - body: new_data - }) - end + files.create(create_attributes(model, new_data)) end def append_data(model, new_data, offset) diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb index 8be42eb48d6..5091e3ff04a 100644 --- a/app/models/ci/build_trace_section.rb +++ b/app/models/ci/build_trace_section.rb @@ -2,6 +2,7 @@ module Ci class BuildTraceSection < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning extend Gitlab::Ci::Model belongs_to :build, class_name: 'Ci::Build' diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb index e9f3366b939..23c96e63724 100644 --- a/app/models/ci/daily_build_group_report_result.rb +++ b/app/models/ci/daily_build_group_report_result.rb @@ -9,14 +9,19 @@ module Ci belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id belongs_to :project + belongs_to :group validates :data, json_schema: { filename: "daily_build_group_report_result_data" } scope :with_included_projects, -> { includes(:project) } + scope :by_ref_path, -> (ref_path) { where(ref_path: ref_path) } scope :by_projects, -> (ids) { where(project_id: ids) } + scope :by_group, -> (group_id) { where(group_id: group_id) } scope :with_coverage, -> { where("(data->'coverage') IS NOT NULL") } scope :with_default_branch, -> { where(default_branch: true) } scope :by_date, -> (start_date) { where(date: report_window(start_date)..Date.current) } + scope :by_dates, -> (start_date, end_date) { where(date: start_date..end_date) } + scope :ordered_by_date_and_group_name, -> { order(date: :desc, group_name: :asc) } store_accessor :data, :coverage diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index f13be3b3c86..f927111758a 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -19,6 +19,8 @@ module Ci NON_ERASABLE_FILE_TYPES = %w[trace].freeze TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze UNSUPPORTED_FILE_TYPES = %i[license_management].freeze + SAST_REPORT_TYPES = %w[sast].freeze + SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze DEFAULT_FILE_NAMES = { archive: nil, metadata: nil, @@ -150,6 +152,14 @@ module Ci with_file_types(REPORT_TYPES.keys.map(&:to_s)) end + scope :sast_reports, -> do + with_file_types(SAST_REPORT_TYPES) + end + + scope :secret_detection_reports, -> do + with_file_types(SECRET_DETECTION_REPORT_TYPES) + end + scope :test_reports, -> do with_file_types(TEST_REPORT_FILE_TYPES) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 88c7002b1b6..3be107ea2e1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -251,6 +251,7 @@ module Ci after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| pipeline.run_after_commit do ::Ci::PipelineArtifacts::CoverageReportWorker.perform_async(pipeline.id) + ::Ci::PipelineArtifacts::CreateQualityReportWorker.perform_async(pipeline.id) end end @@ -263,8 +264,6 @@ module Ci end after_transition any => any do |pipeline| - next unless Feature.enabled?(:jira_sync_builds, pipeline.project) - pipeline.run_after_commit do # Passing the seq-id ensures this is idempotent seq_id = ::Atlassian::JiraConnect::Client.generate_update_sequence_id @@ -678,7 +677,7 @@ module Ci def number_of_warnings BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader| - ::Ci::Build.where(commit_id: pipeline_ids) + ::CommitStatus.where(commit_id: pipeline_ids) .latest .failed_but_allowed .group(:commit_id) @@ -805,7 +804,7 @@ module Ci variables.concat(merge_request.predefined_variables) end - if Gitlab::Ci::Features.pipeline_open_merge_requests?(project) && open_merge_requests_refs.any? + if open_merge_requests_refs.any? variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(',')) end @@ -962,7 +961,7 @@ module Ci def detailed_status(current_user) Gitlab::Ci::Status::Pipeline::Factory - .new(self, current_user) + .new(self.present, current_user) .fabricate! end @@ -998,13 +997,23 @@ module Ci end def has_coverage_reports? - pipeline_artifacts&.has_code_coverage? + pipeline_artifacts&.report_exists?(:code_coverage) end def can_generate_coverage_reports? has_reports?(Ci::JobArtifact.coverage_reports) end + def has_codequality_mr_diff_report? + pipeline_artifacts&.report_exists?(:code_quality_mr_diff) + end + + def can_generate_codequality_reports? + return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project) + + has_reports?(Ci::JobArtifact.codequality_reports) + end + def test_report_summary strong_memoize(:test_report_summary) do Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results) @@ -1206,6 +1215,21 @@ module Ci end # rubocop:enable Rails/FindEach + # EE-only + def merge_train_pipeline? + false + end + + def security_reports(report_types: []) + reports_scope = report_types.empty? ? ::Ci::JobArtifact.security_reports : ::Ci::JobArtifact.security_reports(file_types: report_types) + + ::Gitlab::Ci::Reports::Security::Reports.new(self).tap do |security_reports| + latest_report_builds(reports_scope).each do |build| + build.collect_security_reports!(security_reports) + end + end + end + private def add_message(severity, content) diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb index b6db8cad667..f538a4cd808 100644 --- a/app/models/ci/pipeline_artifact.rb +++ b/app/models/ci/pipeline_artifact.rb @@ -14,7 +14,13 @@ module Ci EXPIRATION_DATE = 1.week.freeze DEFAULT_FILE_NAMES = { - code_coverage: 'code_coverage.json' + code_coverage: 'code_coverage.json', + code_quality_mr_diff: 'code_quality_mr_diff.json' + }.freeze + + REPORT_TYPES = { + code_coverage: :raw, + code_quality_mr_diff: :raw }.freeze belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts @@ -30,15 +36,20 @@ module Ci update_project_statistics project_statistics_name: :pipeline_artifacts_size enum file_type: { - code_coverage: 1 + code_coverage: 1, + code_quality_mr_diff: 2 } - def self.has_code_coverage? - where(file_type: :code_coverage).exists? - end + class << self + def report_exists?(file_type) + return false unless REPORT_TYPES.key?(file_type) + + where(file_type: file_type).exists? + end - def self.find_with_code_coverage - find_by(file_type: :code_coverage) + def find_by_file_type(file_type) + find_by(file_type: file_type) + end end def present diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 8c9ad343f32..2fae077dd87 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -21,7 +21,7 @@ module Ci validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :description, presence: true - validates :variables, variable_duplicates: true + validates :variables, nested_attributes_duplicates: true strip_attributes :cron diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 6aaf6ac530b..fae65ed0632 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -3,6 +3,11 @@ module Ci class Processable < ::CommitStatus include Gitlab::Utils::StrongMemoize + extend ::Gitlab::Utils::Override + + has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable + + belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables accepts_nested_attributes_for :needs @@ -20,6 +25,48 @@ module Ci where('NOT EXISTS (?)', needs) end + state_machine :status do + event :enqueue do + transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :with_resource_group? + end + + event :enqueue_scheduled do + transition scheduled: :waiting_for_resource, if: :with_resource_group? + end + + event :enqueue_waiting_for_resource do + transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites? + transition waiting_for_resource: :pending + end + + before_transition any => :waiting_for_resource do |processable| + processable.waiting_for_resource_at = Time.current + end + + before_transition on: :enqueue_waiting_for_resource do |processable| + next unless processable.with_resource_group? + + processable.resource_group.assign_resource_to(processable) + end + + after_transition any => :waiting_for_resource do |processable| + processable.run_after_commit do + Ci::ResourceGroups::AssignResourceFromResourceGroupWorker + .perform_async(processable.resource_group_id) + end + end + + after_transition any => ::Ci::Processable.completed_statuses do |processable| + next unless processable.with_resource_group? + next unless processable.resource_group.release_resource_from(processable) + + processable.run_after_commit do + Ci::ResourceGroups::AssignResourceFromResourceGroupWorker + .perform_async(processable.resource_group_id) + end + end + end + def self.select_with_aggregated_needs(project) aggregated_needs_names = Ci::BuildNeed .scoped_build @@ -77,6 +124,15 @@ module Ci raise NotImplementedError end + override :all_met_to_become_pending? + def all_met_to_become_pending? + super && !with_resource_group? + end + + def with_resource_group? + self.resource_group_id.present? + end + # Overriding scheduling_type enum's method for nil `scheduling_type`s def scheduling_type_dag? scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super diff --git a/app/models/ci/resource.rb b/app/models/ci/resource.rb index ee5b6546165..e0e1fab642d 100644 --- a/app/models/ci/resource.rb +++ b/app/models/ci/resource.rb @@ -5,9 +5,9 @@ module Ci extend Gitlab::Ci::Model belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources - belongs_to :build, class_name: 'Ci::Build', inverse_of: :resource + belongs_to :processable, class_name: 'Ci::Processable', foreign_key: 'build_id', inverse_of: :resource - scope :free, -> { where(build: nil) } - scope :retained_by, -> (build) { where(build: build) } + scope :free, -> { where(processable: nil) } + scope :retained_by, -> (processable) { where(processable: processable) } end end diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb index eb18f3da0bf..85fbe03e1c9 100644 --- a/app/models/ci/resource_group.rb +++ b/app/models/ci/resource_group.rb @@ -7,7 +7,7 @@ module Ci belongs_to :project, inverse_of: :resource_groups has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group - has_many :builds, class_name: 'Ci::Build', inverse_of: :resource_group + has_many :processables, class_name: 'Ci::Processable', inverse_of: :resource_group validates :key, length: { maximum: 255 }, @@ -19,12 +19,12 @@ module Ci ## # NOTE: This is concurrency-safe method that the subquery in the `UPDATE` # works as explicit locking. - def assign_resource_to(build) - resources.free.limit(1).update_all(build_id: build.id) > 0 + def assign_resource_to(processable) + resources.free.limit(1).update_all(build_id: processable.id) > 0 end - def release_resource_from(build) - resources.retained_by(build).update_all(build_id: nil) > 0 + def release_resource_from(processable) + resources.retained_by(processable).update_all(build_id: nil) > 0 end private diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index cc6bd1870b9..ae80692d598 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -118,7 +118,7 @@ module Ci def number_of_warnings BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader| - ::Ci::Build.where(stage_id: stage_ids) + ::CommitStatus.where(stage_id: stage_ids) .latest .failed_but_allowed .group(:stage_id) diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index c58a3bab1a9..c5b9dddb1da 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -4,6 +4,7 @@ module Clusters class Agent < ApplicationRecord self.table_name = 'cluster_agents' + belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project has_many :agent_tokens, class_name: 'Clusters::AgentToken' diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb index 5c9561ffa98..b260822f784 100644 --- a/app/models/clusters/agent_token.rb +++ b/app/models/clusters/agent_token.rb @@ -8,6 +8,7 @@ module Clusters self.table_name = 'cluster_agent_tokens' belongs_to :agent, class_name: 'Clusters::Agent' + belongs_to :created_by_user, class_name: 'User', optional: true before_save :ensure_token end diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 8560826928a..2a051233de2 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class CertManager < ApplicationRecord VERSION = 'v0.10.1' CRD_VERSION = '0.10' diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb index 2b1a86706a4..07378b4e8dc 100644 --- a/app/models/clusters/applications/crossplane.rb +++ b/app/models/clusters/applications/crossplane.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Crossplane < ApplicationRecord VERSION = '0.4.1' diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 36324e7f3e0..e7d4d737b8e 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Ingress < ApplicationRecord VERSION = '1.40.2' INGRESS_CONTAINER_NAME = 'nginx-ingress-controller' diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ff907c6847f..8d7d9c20bfa 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -4,6 +4,8 @@ require 'securerandom' module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Jupyter < ApplicationRecord VERSION = '0.9.0' diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 7c131e031c1..6867d7b6934 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Knative < ApplicationRecord VERSION = '0.10.0' REPOSITORY = 'https://charts.gitlab.io' diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 56acac53e0b..f87eccecf9f 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.24.0' + VERSION = '0.25.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e3dcd5b0d07..da5f4cc1862 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -7,6 +7,7 @@ module Clusters include EnumWithNil include AfterCommitQueue include ReactiveCaching + include NullifyIfBlank RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze @@ -25,7 +26,6 @@ module Clusters key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-cbc' - before_validation :nullify_blank_namespace before_validation :enforce_namespace_to_lower_case before_validation :enforce_ca_whitespace_trimming @@ -64,6 +64,8 @@ module Clusters default_value_for :authorization_type, :rbac + nullify_if_blank :namespace + def predefined_variables(project:, environment_name:, kubernetes_namespace: nil) Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'KUBE_URL', value: api_url) @@ -255,10 +257,6 @@ module Clusters true end - def nullify_blank_namespace - self.namespace = nil if namespace.blank? - end - def extract_relevant_pod_data(pods) pods.map do |pod| { diff --git a/app/models/commit.rb b/app/models/commit.rb index edce9ad293e..bf168aaacc5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -36,7 +36,7 @@ class Commit LINK_EXTENSION_PATTERN = /(patch)/.freeze cache_markdown_field :title, pipeline: :single_line - cache_markdown_field :full_title, pipeline: :single_line + cache_markdown_field :full_title, pipeline: :single_line, limit: 1.kilobyte cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte class << self diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c2aecc524d4..ea2f425c5f6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -209,14 +209,26 @@ class CommitStatus < ApplicationRecord end def group_name - # 'rspec:linux: 1/10' => 'rspec:linux' - common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '') + simplified_commit_status_group_name_feature_flag = Gitlab::SafeRequestStore.fetch("project:#{project_id}:simplified_commit_status_group_name") do + Feature.enabled?(:simplified_commit_status_group_name, project, default_enabled: false) + end + + if simplified_commit_status_group_name_feature_flag + # Only remove one or more [...] "X/Y" "X Y" from the end of build names. + # More about the regular expression logic: https://docs.gitlab.com/ee/ci/jobs/#group-jobs-in-a-pipeline + + name.to_s.sub(%r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z}, '').strip + else + # Prior implementation, remove [...] "X/Y" "X Y" from the beginning and middle of build names + # 'rspec:linux: 1/10' => 'rspec:linux' + common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '') - # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' - common_name.gsub!(%r{: \[.*\]\s*\z}, '') + # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' + common_name.gsub!(%r{: \[.*\]\s*\z}, '') - common_name.strip! - common_name + common_name.strip! + common_name + end end def failed_but_allowed? @@ -256,15 +268,7 @@ class CommitStatus < ApplicationRecord end def all_met_to_become_pending? - !any_unmet_prerequisites? && !requires_resource? - end - - def any_unmet_prerequisites? - false - end - - def requires_resource? - false + true end def auto_canceled? diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index f1c39dda49d..080ff07ec0c 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -49,6 +49,14 @@ module Analytics end end + def start_event_identifier + backward_compatible_identifier(:start_event_identifier) || super + end + + def end_event_identifier + backward_compatible_identifier(:end_event_identifier) || super + end + def start_event_label_based? start_event_identifier && start_event.label_based? end @@ -128,6 +136,17 @@ module Analytics .id_in(label_id) .exists? end + + # Temporary, will be removed in 13.10 + def backward_compatible_identifier(attribute_name) + removed_identifier = 6 # References IssueFirstMentionedInCommit removed on https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51975 + replacement_identifier = :issue_first_mentioned_in_commit + + # ActiveRecord returns nil if the column value is not part of the Enum definition + if self[attribute_name].nil? && read_attribute_before_type_cast(attribute_name) == removed_identifier + replacement_identifier + end + end end end end diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index baa99fa5a7f..bbf9ecbcfe9 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -26,20 +26,31 @@ module AtomicInternalId extend ActiveSupport::Concern + MissingValueError = Class.new(StandardError) + class_methods do def has_internal_id( # rubocop:disable Naming/PredicateName - column, scope:, init: :not_given, ensure_if: nil, track_if: nil, - presence: true, backfill: false, hook_names: :create) + column, scope:, init: :not_given, ensure_if: nil, track_if: nil, presence: true, hook_names: :create) raise "has_internal_id init must not be nil if given." if init.nil? raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope) init = infer_init(scope) if init == :not_given - before_validation :"track_#{scope}_#{column}!", on: hook_names, if: track_if - before_validation :"ensure_#{scope}_#{column}!", on: hook_names, if: ensure_if - validates column, presence: presence + callback_names = Array.wrap(hook_names).map { |hook_name| :"before_#{hook_name}" } + callback_names.each do |callback_name| + # rubocop:disable GitlabSecurity/PublicSend + public_send(callback_name, :"track_#{scope}_#{column}!", if: track_if) + public_send(callback_name, :"ensure_#{scope}_#{column}!", if: ensure_if) + # rubocop:enable GitlabSecurity/PublicSend + end + after_rollback :"clear_#{scope}_#{column}!", on: hook_names, if: ensure_if + + if presence + before_create :"validate_#{column}_exists!" + before_update :"validate_#{column}_exists!" + end define_singleton_internal_id_methods(scope, column, init) - define_instance_internal_id_methods(scope, column, init, backfill) + define_instance_internal_id_methods(scope, column, init) end private @@ -62,10 +73,8 @@ module AtomicInternalId # - track_{scope}_{column}! # - reset_{scope}_{column} # - {column}= - def define_instance_internal_id_methods(scope, column, init, backfill) + def define_instance_internal_id_methods(scope, column, init) define_method("ensure_#{scope}_#{column}!") do - return if backfill && self.class.where(column => nil).exists? - scope_value = internal_id_read_scope(scope) value = read_attribute(column) return value unless scope_value @@ -79,6 +88,8 @@ module AtomicInternalId internal_id_scope_usage, init) write_attribute(column, value) + + @internal_id_set_manually = false end value @@ -110,6 +121,7 @@ module AtomicInternalId super(value).tap do |v| # Indicate the iid was set from externally @internal_id_needs_tracking = true + @internal_id_set_manually = true end end @@ -128,6 +140,20 @@ module AtomicInternalId read_attribute(column) end + + define_method("clear_#{scope}_#{column}!") do + return if @internal_id_set_manually + + return unless public_send(:"#{column}_previously_changed?") # rubocop:disable GitlabSecurity/PublicSend + + write_attribute(column, nil) + end + + define_method("validate_#{column}_exists!") do + value = read_attribute(column) + + raise MissingValueError, "#{column} was unexpectedly blank!" if value.blank? + end end # Defines class methods: diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index de176ffde5c..ee56322cce7 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -83,6 +83,6 @@ module CacheableAttributes end def cache! - self.class.cache_backend.write(self.class.cache_key, self, expires_in: 1.minute) + self.class.cache_backend.write(self.class.cache_key, self, expires_in: Gitlab.config.gitlab['application_settings_cache_seconds'] || 60) end end diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb index 52c3a4106e3..1132e4e79ac 100644 --- a/app/models/concerns/can_move_repository_storage.rb +++ b/app/models/concerns/can_move_repository_storage.rb @@ -16,10 +16,10 @@ module CanMoveRepositoryStorage !skip_git_transfer_check && git_transfer_in_progress? raise RepositoryReadOnlyError, _('Repository already read-only') if - self.class.where(id: id).pick(:repository_read_only) + _safe_read_repository_read_only_column raise ActiveRecord::RecordNotSaved, _('Database update failed') unless - update_column(:repository_read_only, true) + _update_repository_read_only_column(true) nil end @@ -30,7 +30,7 @@ module CanMoveRepositoryStorage def set_repository_writable! with_lock do raise ActiveRecord::RecordNotSaved, _('Database update failed') unless - update_column(:repository_read_only, false) + _update_repository_read_only_column(false) nil end @@ -43,4 +43,19 @@ module CanMoveRepositoryStorage def reference_counter(type:) Gitlab::ReferenceCounter.new(type.identifier_for_container(self)) end + + private + + # Not all resources that can move repositories have the `repository_read_only` + # in their table, for example groups. We need these methods to override the + # behavior in those classes in order to access the column. + def _safe_read_repository_read_only_column + # This was added originally this way because of + # https://gitlab.com/gitlab-org/gitlab/-/commit/43f9b98302d3985312c9f8b66018e2835d8293d2 + self.class.where(id: id).pick(:repository_read_only) + end + + def _update_repository_read_only_column(value) + update_column(:repository_read_only, value) + end end diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb index e1f07fa162c..f8314d8b429 100644 --- a/app/models/concerns/enums/ci/pipeline.rb +++ b/app/models/concerns/enums/ci/pipeline.rb @@ -10,6 +10,9 @@ module Enums unknown_failure: 0, config_error: 1, external_validation_failure: 2, + activity_limit_exceeded: 20, + size_limit_exceeded: 21, + job_activity_limit_exceeded: 22, deployments_limit_exceeded: 23 } end @@ -71,11 +74,10 @@ module Enums remote_source: 4, external_project_source: 5, bridge_source: 6, - parameter_source: 7 + parameter_source: 7, + compliance_source: 8 } end end end end - -Enums::Ci::Pipeline.prepend_if_ee('EE::Enums::Ci::Pipeline') diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb index 20b72957ec2..ed9bce87da1 100644 --- a/app/models/concerns/featurable.rb +++ b/app/models/concerns/featurable.rb @@ -88,9 +88,6 @@ module Featurable end def feature_available?(feature, user) - # This feature might not be behind a feature flag at all, so default to true - return false unless ::Feature.enabled?(feature, user, default_enabled: true) - get_permission(user, feature) end diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index 9692941d8b2..b9ad78c14fd 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -15,15 +15,6 @@ module HasRepository delegate :base_dir, :disk_path, to: :storage - class_methods do - def pick_repository_storage - # We need to ensure application settings are fresh when we pick - # a repository storage to use. - Gitlab::CurrentSettings.expire_current_application_settings - Gitlab::CurrentSettings.pick_repository_storage - end - end - def valid_repo? repository.exists? rescue diff --git a/app/models/concerns/nullify_if_blank.rb b/app/models/concerns/nullify_if_blank.rb new file mode 100644 index 00000000000..5a5cc51509b --- /dev/null +++ b/app/models/concerns/nullify_if_blank.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Helper that sets attributes to nil prior to validation if they +# are blank (are false, empty or contain only whitespace), to avoid +# unnecessarily persisting empty strings. +# +# Model usage: +# +# class User < ApplicationRecord +# include NullifyIfBlank +# +# nullify_if_blank :name, :email +# end +# +# +# Test usage: +# +# RSpec.describe User do +# it { is_expected.to nullify_if_blank(:name) } +# it { is_expected.to nullify_if_blank(:email) } +# end +# +module NullifyIfBlank + extend ActiveSupport::Concern + + class_methods do + def nullify_if_blank(*attributes) + self.attributes_to_nullify += attributes + end + end + + included do + class_attribute :attributes_to_nullify, + instance_accessor: false, + instance_predicate: false, + default: Set.new + + before_validation :nullify_blank_attributes + end + + private + + def nullify_blank_attributes + self.class.attributes_to_nullify.each do |attribute| + assign_attributes(attribute => nil) if read_attribute(attribute).blank? + end + end +end diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb index 82055822cfb..c7af841e450 100644 --- a/app/models/concerns/optimized_issuable_label_filter.rb +++ b/app/models/concerns/optimized_issuable_label_filter.rb @@ -13,7 +13,7 @@ module OptimizedIssuableLabelFilter def by_label(items) return items unless params.labels? - return super if Feature.disabled?(:optimized_issuable_label_filter) + return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml) target_model = items.model @@ -29,7 +29,7 @@ module OptimizedIssuableLabelFilter # Taken from IssuableFinder def count_by_state return super if root_namespace.nil? - return super if Feature.disabled?(:optimized_issuable_label_filter) + return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml) count_params = params.merge(state: nil, sort: nil, force_cte: true) finder = self.class.new(current_user, count_params) diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb index 4aa633e0357..760ebb49980 100644 --- a/app/models/concerns/packages/debian/architecture.rb +++ b/app/models/concerns/packages/debian/architecture.rb @@ -7,6 +7,12 @@ module Packages included do belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures + # files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile", + foreign_key: :architecture_id, + inverse_of: :architecture, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :distribution, presence: true diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb new file mode 100644 index 00000000000..7b342c7b684 --- /dev/null +++ b/app/models/concerns/packages/debian/component.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Packages + module Debian + module Component + extend ActiveSupport::Concern + + included do + belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :components + # files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile", + foreign_key: :component_id, + inverse_of: :component, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + + validates :distribution, + presence: true + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { scope: %i[distribution_id] }, + format: { with: Gitlab::Regex.debian_component_regex } + + scope :with_distribution, ->(distribution) { where(distribution: distribution) } + scope :with_name, ->(name) { where(name: name) } + end + end + end +end diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb new file mode 100644 index 00000000000..3cc2c291e96 --- /dev/null +++ b/app/models/concerns/packages/debian/component_file.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Packages + module Debian + module ComponentFile + extend ActiveSupport::Concern + + included do + include Sortable + include FileStoreMounter + + def self.container_foreign_key + "#{container_type}_id".to_sym + end + + def self.distribution_class + "::Packages::Debian::#{container_type.capitalize}Distribution".constantize + end + + belongs_to :component, class_name: "Packages::Debian::#{container_type.capitalize}Component", inverse_of: :files + belongs_to :architecture, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", inverse_of: :files, optional: true + + enum file_type: { packages: 1, source: 2, di_packages: 3 } + enum compression_type: { gz: 1, bz2: 2, xz: 3 } + + validates :component, presence: true + validates :file_type, presence: true + validates :architecture, presence: true, unless: :source? + validates :architecture, absence: true, if: :source? + validates :file, length: { minimum: 0, allow_nil: false } + validates :size, presence: true + validates :file_store, presence: true + validates :file_md5, presence: true + validates :file_sha256, presence: true + + scope :with_container, ->(container) do + joins(component: :distribution) + .where("packages_debian_#{container_type}_distributions" => { container_foreign_key => container.id }) + end + + scope :with_codename_or_suite, ->(codename_or_suite) do + joins(component: :distribution) + .merge(distribution_class.with_codename_or_suite(codename_or_suite)) + end + + scope :with_component_name, ->(component_name) do + joins(:component) + .where("packages_debian_#{container_type}_components" => { name: component_name }) + end + + scope :with_file_type, ->(file_type) { where(file_type: file_type) } + + scope :with_architecture_name, ->(architecture_name) do + left_outer_joins(:architecture) + .where("packages_debian_#{container_type}_architectures" => { name: architecture_name }) + end + + scope :with_compression_type, ->(compression_type) { where(compression_type: compression_type) } + scope :with_file_sha256, ->(file_sha256) { where(file_sha256: file_sha256) } + + scope :preload_distribution, -> { includes(component: :distribution) } + + mount_file_store_uploader Packages::Debian::ComponentFileUploader + + before_validation :update_size_from_file + + def file_name + case file_type + when 'di_packages' + 'Packages' + else + file_type.capitalize + end + end + + def relative_path + case file_type + when 'packages' + "#{component.name}/binary-#{architecture.name}/#{file_name}#{extension}" + when 'source' + "#{component.name}/source/#{file_name}#{extension}" + when 'di_packages' + "#{component.name}/debian-installer/binary-#{architecture.name}/#{file_name}#{extension}" + end + end + + private + + def extension + return '' unless compression_type + + ".#{compression_type}" + end + + def update_size_from_file + self.size ||= file.size + end + end + end + end +end diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb index 285d293c9ee..08fb9ccf3ea 100644 --- a/app/models/concerns/packages/debian/distribution.rb +++ b/app/models/concerns/packages/debian/distribution.rb @@ -18,6 +18,16 @@ module Packages belongs_to container_type belongs_to :creator, class_name: 'User' + # component_files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :components, + class_name: "Packages::Debian::#{container_type.capitalize}Component", + foreign_key: :distribution_id, + inverse_of: :distribution, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :component_files, + through: :components, + source: :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile" has_many :architectures, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", foreign_key: :distribution_id, diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 65195a8d5aa..cf23a27244c 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -40,20 +40,26 @@ module ProtectedRef end def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) - access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| + all_matching_rules_allow?(ref, action: action, protected_refs: protected_refs) do |access_level| access_level.check_access(user) end end def developers_can?(action, ref, protected_refs: nil) - access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| + all_matching_rules_allow?(ref, action: action, protected_refs: protected_refs) do |access_level| access_level.access_level == Gitlab::Access::DEVELOPER end end - def access_levels_for_ref(ref, action:, protected_refs: nil) - self.matching(ref, protected_refs: protected_refs) - .flat_map(&:"#{action}_access_levels") + def all_matching_rules_allow?(ref, action:, protected_refs: nil, &block) + access_levels_groups = + self.matching(ref, protected_refs: protected_refs).map(&:"#{action}_access_levels") + + return false if access_levels_groups.blank? + + access_levels_groups.all? do |access_levels| + access_levels.any?(&block) + end end # Returns all protected refs that match the given ref name. diff --git a/app/models/concerns/repositories/can_housekeep_repository.rb b/app/models/concerns/repositories/can_housekeep_repository.rb index 2b79851a07c..946f82c5f36 100644 --- a/app/models/concerns/repositories/can_housekeep_repository.rb +++ b/app/models/concerns/repositories/can_housekeep_repository.rb @@ -16,6 +16,10 @@ module Repositories Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) } end + def git_garbage_collect_worker_klass + raise NotImplementedError + end + private def pushes_since_gc_redis_shared_state_key diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb index a45b4626628..8607f0d94f4 100644 --- a/app/models/concerns/repository_storage_movable.rb +++ b/app/models/concerns/repository_storage_movable.rb @@ -20,7 +20,7 @@ module RepositoryStorageMovable validate :container_repository_writable, on: :create default_value_for(:destination_storage_name, allows_nil: false) do - pick_repository_storage + Repository.pick_storage_shard end state_machine initial: :initial do @@ -68,6 +68,18 @@ module RepositoryStorageMovable storage_move.update_repository_storage(storage_move.destination_storage_name) end + after_transition started: :replicated do |storage_move| + # We have several scripts in place that replicate some statistics information + # to other databases. Some of them depend on the updated_at column + # to identify the models they need to extract. + # + # If we don't update the `updated_at` of the container after a repository storage move, + # the scripts won't know that they need to sync them. + # + # See https://gitlab.com/gitlab-data/analytics/-/issues/7868 + storage_move.container.touch + end + before_transition started: :failed do |storage_move| storage_move.container.set_repository_writable! end @@ -82,16 +94,6 @@ module RepositoryStorageMovable end end - class_methods do - private - - def pick_repository_storage - container_klass = reflect_on_association(:container).class_name.constantize - - container_klass.pick_repository_storage - end - end - # Projects, snippets, and group wikis has different db structure. In projects, # we need to update some columns in this step, but we don't with the other resources. # diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 9cd1a22b203..2daea388939 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -45,6 +45,17 @@ module Spammable self.needs_recaptcha = true end + ## + # Indicates if a recaptcha should be rendered before allowing this model to be saved. + # + def render_recaptcha? + return false unless Gitlab::Recaptcha.enabled? + + return false if self.errors.count > 1 # captcha should not be rendered if are still other errors + + self.needs_recaptcha? + end + def spam! self.spam = true end diff --git a/app/models/concerns/suppress_composite_primary_key_warning.rb b/app/models/concerns/suppress_composite_primary_key_warning.rb new file mode 100644 index 00000000000..32634e7bc72 --- /dev/null +++ b/app/models/concerns/suppress_composite_primary_key_warning.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# When extended, silences this warning below: +# WARNING: Active Record does not support composite primary key. +# +# project_authorizations has composite primary key. Composite primary key is ignored. +# +# See https://gitlab.com/gitlab-org/gitlab/-/issues/292909 +module SuppressCompositePrimaryKeyWarning + extend ActiveSupport::Concern + + private + + def suppress_composite_primary_key(pk) + silence_warnings do + super + end + end +end diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb index 4728cb658dc..672402ee4d6 100644 --- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -85,10 +85,18 @@ module TokenAuthenticatableStrategies end def find_by_encrypted_token(token, unscoped) - encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC + encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce) + relation(unscoped).find_by(encrypted_field => encrypted_value) end + def find_hashed_iv(token) + token_record = TokenWithIv.find_by_plaintext_token(token) + + token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC + end + def insecure_strategy @insecure_strategy ||= TokenAuthenticatableStrategies::Insecure .new(klass, token_field, options) diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index 473b430bb04..db5df6c2c9f 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -16,7 +16,8 @@ module TriggerableHooks deployment_hooks: :deployment_events, feature_flag_hooks: :feature_flag_events, release_hooks: :releases_events, - member_hooks: :member_events + member_hooks: :member_events, + subgroup_hooks: :subgroup_events }.freeze extend ActiveSupport::Concern diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 0d7ce966537..e2bdf8ffce2 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -4,6 +4,7 @@ class ContainerRepository < ApplicationRecord include Gitlab::Utils::StrongMemoize include Gitlab::SQL::Pattern include EachBatch + include Sortable WAITING_CLEANUP_STATUSES = %i[cleanup_scheduled cleanup_unfinished].freeze diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 4f8f86965d7..f4c914c6a3a 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class CustomEmoji < ApplicationRecord + NAME_REGEXP = /[a-z0-9_-]+/.freeze + belongs_to :namespace, inverse_of: :custom_emoji belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' @@ -17,7 +19,12 @@ class CustomEmoji < ApplicationRecord uniqueness: { scope: [:namespace_id, :name] }, presence: true, length: { maximum: 36 }, - format: { with: /\A[a-z0-9][a-z0-9\-_]*[a-z0-9]\z/ } + + format: { with: /\A#{NAME_REGEXP}\z/ } + + scope :by_name, -> (names) { where(name: names) } + + alias_attribute :url, :file # this might need a change in https://gitlab.com/gitlab-org/gitlab/-/issues/230467 private diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 6f40466394a..f000e474605 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -38,6 +38,7 @@ class Deployment < ApplicationRecord scope :for_status, -> (status) { where(status: status) } scope :for_project, -> (project_id) { where(project_id: project_id) } + scope :for_projects, -> (projects) { where(project: projects) } scope :visible, -> { where(status: %i[running success failed canceled]) } scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } @@ -45,11 +46,8 @@ class Deployment < ApplicationRecord scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) } scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) } - scope :finished_between, -> (start_date, end_date = nil) do - selected = where('deployments.finished_at >= ?', start_date) - selected = selected.where('deployments.finished_at < ?', end_date) if end_date - selected - end + scope :finished_after, ->(date) { where('finished_at >= ?', date) } + scope :finished_before, ->(date) { where('finished_at < ?', date) } FINISHED_STATUSES = %i[success failed canceled].freeze @@ -112,7 +110,6 @@ class Deployment < ApplicationRecord after_transition any => any - [:skipped] do |deployment, transition| next if transition.loopback? - next unless Feature.enabled?(:jira_sync_deployments, deployment.project) deployment.run_after_commit do ::JiraConnect::SyncDeploymentsWorker.perform_async(id) @@ -121,8 +118,6 @@ class Deployment < ApplicationRecord end after_create unless: :importing? do |deployment| - next unless Feature.enabled?(:jira_sync_deployments, deployment.project) - run_after_commit do ::JiraConnect::SyncDeploymentsWorker.perform_async(deployment.id) end @@ -353,6 +348,13 @@ class Deployment < ApplicationRecord File.join(environment.ref_path, 'deployments', iid.to_s) end + def equal_to?(params) + ref == params[:ref] && + tag == params[:tag] && + sha == params[:sha] && + status == params[:status] + end + private def legacy_finished_at diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb index 64a578e16bf..7949bd81605 100644 --- a/app/models/deployment_merge_request.rb +++ b/app/models/deployment_merge_request.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class DeploymentMergeRequest < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + belongs_to :deployment, optional: false belongs_to :merge_request, optional: false diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb index f5e52c04944..e2d10cc7e78 100644 --- a/app/models/design_management/design.rb +++ b/app/models/design_management/design.rb @@ -228,17 +228,6 @@ module DesignManagement project end - def immediately_before?(next_design) - return false if next_design.relative_position <= relative_position - - interloper = self.class.on_issue(issue).where( - "relative_position <@ int4range(?, ?, '()')", - *[self, next_design].map(&:relative_position) - ) - - !interloper.exists? - end - def notes_with_associations notes.includes(:author) end diff --git a/app/models/event.rb b/app/models/event.rb index 671def16151..401dfc4cb02 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -294,10 +294,14 @@ class Event < ApplicationRecord note? && target && target.for_merge_request? end - def project_snippet_note? + def snippet_note? note? && target && target.for_snippet? end + def project_snippet_note? + note? && target && target.for_project_snippet? + end + def personal_snippet_note? note? && target && target.for_personal_snippet? end diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 7dbc95f617a..354b1e0b6b9 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -10,6 +10,10 @@ class Experiment < ApplicationRecord find_or_create_by!(name: name).record_user_and_group(user, group_type, context) end + def self.add_group(name, variant:, group:) + find_or_create_by!(name: name).record_group_and_variant!(group, variant) + end + def self.record_conversion_event(name, user) find_or_create_by!(name: name).record_conversion_event_for_user(user) end @@ -24,4 +28,8 @@ class Experiment < ApplicationRecord def record_conversion_event_for_user(user) experiment_users.find_by(user: user, converted_at: nil)&.touch(:converted_at) end + + def record_group_and_variant!(group, variant) + experiment_subjects.find_or_initialize_by(group: group).update!(variant: variant) + end end diff --git a/app/models/group.rb b/app/models/group.rb index 903d0154969..1eaa4499eb5 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -48,6 +48,7 @@ class Group < Namespace has_many :labels, class_name: 'GroupLabel' has_many :variables, class_name: 'Ci::GroupVariable' + has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult' has_many :custom_attributes, class_name: 'GroupCustomAttribute' has_many :boards @@ -75,7 +76,7 @@ class Group < Namespace has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob' has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest' - # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent accepts_nested_attributes_for :variables, allow_destroy: true @@ -84,7 +85,7 @@ class Group < Namespace validate :visibility_level_allowed_by_sub_groups validate :visibility_level_allowed_by_parent validate :two_factor_authentication_allowed - validates :variables, variable_duplicates: true + validates :variables, nested_attributes_duplicates: true validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } @@ -169,6 +170,15 @@ class Group < Namespace where('NOT EXISTS (?)', services) end + # This method can be used only if all groups have the same top-level + # group + def preset_root_ancestor_for(groups) + return groups if groups.size < 2 + + root = groups.first.root_ancestor + groups.drop(1).each { |group| group.root_ancestor = root } + end + private def public_to_user_arel(user) diff --git a/app/models/issue.rb b/app/models/issue.rb index 5da9f67f6ef..79d0229a281 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -132,7 +132,7 @@ class Issue < ApplicationRecord scope :counts_by_state, -> { reorder(nil).group(:state_id).count } scope :service_desk, -> { where(author: ::User.support_bot) } - scope :inc_relations_for_view, -> { includes(author: :status) } + scope :inc_relations_for_view, -> { includes(author: :status, assignees: :status) } # An issue can be uniquely identified by project_id and iid # Takes one or more sets of composite IDs, expressed as hash-like records of diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb index 7f3d552b3d9..d62f0eb170c 100644 --- a/app/models/issue_assignee.rb +++ b/app/models/issue_assignee.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class IssueAssignee < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + belongs_to :issue belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :issue_assignees diff --git a/app/models/issue_link.rb b/app/models/issue_link.rb index 5448ebdf50b..ba97874ed39 100644 --- a/app/models/issue_link.rb +++ b/app/models/issue_link.rb @@ -17,9 +17,11 @@ class IssueLink < ApplicationRecord TYPE_RELATES_TO = 'relates_to' TYPE_BLOCKS = 'blocks' + # we don't store is_blocked_by in the db but need it for displaying the relation + # from the target (used in IssueLink.inverse_link_type) TYPE_IS_BLOCKED_BY = 'is_blocked_by' - enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 } + enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 } def self.inverse_link_type(type) type diff --git a/app/models/label.rb b/app/models/label.rb index 54129c7c7f3..7a31b095cfc 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -12,7 +12,7 @@ class Label < ApplicationRecord cache_markdown_field :description, pipeline: :single_line - DEFAULT_COLOR = '#428BCA' + DEFAULT_COLOR = '#6699cc' default_value_for :color, DEFAULT_COLOR diff --git a/app/models/license_template.rb b/app/models/license_template.rb index bd24259984b..548066107c1 100644 --- a/app/models/license_template.rb +++ b/app/models/license_template.rb @@ -12,11 +12,12 @@ class LicenseTemplate (fullname|name\sof\s(author|copyright\sowner)) [\>\}\]]}xi.freeze - attr_reader :key, :name, :category, :nickname, :url, :meta + attr_reader :key, :name, :project, :category, :nickname, :url, :meta - def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {}) + def initialize(key:, name:, project:, category:, content:, nickname: nil, url: nil, meta: {}) @key = key @name = name + @project = project @category = category @content = content @nickname = nickname @@ -24,6 +25,10 @@ class LicenseTemplate @meta = meta end + def project_id + project&.id + end + def popular? category == :Popular end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 64b8223a1f0..1374e8a814a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -50,12 +50,15 @@ class MergeRequest < ApplicationRecord end end - has_many :merge_request_diffs + has_many :merge_request_diffs, + -> { regular }, inverse_of: :merge_request has_many :merge_request_context_commits, inverse_of: :merge_request has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files has_one :merge_request_diff, - -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request + -> { regular.order('merge_request_diffs.id DESC') }, inverse_of: :merge_request + has_one :merge_head_diff, + -> { merge_head }, inverse_of: :merge_request, class_name: 'MergeRequestDiff' has_one :cleanup_schedule, inverse_of: :merge_request belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff' @@ -270,8 +273,7 @@ class MergeRequest < ApplicationRecord by_commit_sha(sha), by_squash_commit_sha(sha), by_merge_commit_sha(sha) - ], - remove_duplicates: false + ] ) end scope :by_cherry_pick_sha, -> (sha) do @@ -477,13 +479,17 @@ class MergeRequest < ApplicationRecord # This is used after project import, to reset the IDs to the correct # values. It is not intended to be called without having already scoped the # relation. + # + # Only set `regular` merge request diffs as latest so `merge_head` diff + # won't be considered as `MergeRequest#merge_request_diff`. def self.set_latest_merge_request_diff_ids! - update = ' + update = " latest_merge_request_diff_id = ( SELECT MAX(id) FROM merge_request_diffs WHERE merge_requests.id = merge_request_diffs.merge_request_id - )'.squish + AND merge_request_diffs.diff_type = #{MergeRequestDiff.diff_types[:regular]} + )".squish self.each_batch do |batch| batch.update_all(update) @@ -915,6 +921,10 @@ class MergeRequest < ApplicationRecord closed? && !source_project_missing? && source_branch_exists? end + def can_be_closed? + opened? + end + def ensure_merge_request_diff merge_request_diff.persisted? || create_merge_request_diff end @@ -922,7 +932,7 @@ class MergeRequest < ApplicationRecord def create_merge_request_diff fetch_ref! - # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37435 + # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377 Gitlab::GitalyClient.allow_n_plus_1_calls do merge_request_diffs.create! reload_merge_request_diff @@ -996,7 +1006,7 @@ class MergeRequest < ApplicationRecord # rubocop: enable CodeReuse/ServiceClass def diffable_merge_ref? - open? && merge_ref_head.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?) + open? && merge_head_diff.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?) end # Returns boolean indicating the merge_status should be rechecked in order to @@ -1478,8 +1488,26 @@ class MergeRequest < ApplicationRecord compare_reports(Ci::GenerateCoverageReportsService) end + def has_codequality_mr_diff_report? + return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project) + + actual_head_pipeline&.has_codequality_mr_diff_report? + end + + # TODO: this method and compare_test_reports use the same + # result type, which is handled by the controller's #reports_response. + # we should minimize mistakes by isolating the common parts. + # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 + def find_codequality_mr_diff_reports + unless has_codequality_mr_diff_report? + return { status: :error, status_reason: 'This merge request does not have codequality mr diff reports' } + end + + compare_reports(Ci::GenerateCodequalityMrDiffReportService) + end + def has_codequality_reports? - return false unless Feature.enabled?(:codequality_mr_diff, project) + return false unless ::Gitlab::Ci::Features.display_codequality_backend_comparison?(project) actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports) end @@ -1530,6 +1558,26 @@ class MergeRequest < ApplicationRecord end || { status: :parsing } end + def has_sast_reports? + !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports) + end + + def has_secret_detection_reports? + !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports) + end + + def compare_sast_reports(current_user) + return missing_report_error("SAST") unless has_sast_reports? + + compare_reports(::Ci::CompareSecurityReportsService, current_user, 'sast') + end + + def compare_secret_detection_reports(current_user) + return missing_report_error("secret detection") unless has_secret_detection_reports? + + compare_reports(::Ci::CompareSecurityReportsService, current_user, 'secret_detection') + end + def calculate_reactive_cache(identifier, current_user_id = nil, report_type = nil, *args) service_class = identifier.constantize @@ -1760,7 +1808,7 @@ class MergeRequest < ApplicationRecord end def allows_reviewers? - Feature.enabled?(:merge_request_reviewers, project, default_enabled: true) + true end def allows_multiple_reviewers? @@ -1771,8 +1819,23 @@ class MergeRequest < ApplicationRecord true end + def find_reviewer(user) + merge_request_reviewers.find_by(user_id: user.id) + end + + def enabled_reports + { + sast: report_type_enabled?(:sast), + secret_detection: report_type_enabled?(:secret_detection) + } + end + private + def missing_report_error(report_type) + { status: :error, status_reason: "This merge request does not have #{report_type} reports" } + end + def with_rebase_lock if Feature.enabled?(:merge_request_rebase_nowait_lock, default_enabled: true) with_retried_nowait_lock { yield } @@ -1814,6 +1877,10 @@ class MergeRequest < ApplicationRecord key = Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(project, self, format: :json) Gitlab::EtagCaching::Store.new.touch(key) end + + def report_type_enabled?(report_type) + !!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type) + end end MergeRequest.prepend_if_ee('::EE::MergeRequest') diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb index d3fe256fb1b..5c611da0684 100644 --- a/app/models/merge_request/metrics.rb +++ b/app/models/merge_request/metrics.rb @@ -5,12 +5,14 @@ class MergeRequest::Metrics < ApplicationRecord belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id belongs_to :latest_closed_by, class_name: 'User' belongs_to :merged_by, class_name: 'User' + belongs_to :target_project, class_name: 'Project', inverse_of: :merge_requests before_save :ensure_target_project_id scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) } scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) } scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) } + scope :by_target_project, ->(project) { where(target_project_id: project) } def self.time_to_merge_expression Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))') @@ -21,6 +23,12 @@ class MergeRequest::Metrics < ApplicationRecord def ensure_target_project_id self.target_project_id ||= merge_request.target_project_id end + + def self.total_time_to_merge + with_valid_time_to_merge + .pluck(time_to_merge_expression) + .first + end end MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics') diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb index 59cc82cfaf5..e081a96dc10 100644 --- a/app/models/merge_request_context_commit.rb +++ b/app/models/merge_request_context_commit.rb @@ -12,6 +12,9 @@ class MergeRequestContextCommit < ApplicationRecord validates :sha, presence: true validates :sha, uniqueness: { message: 'has already been added' } + serialize :trailers, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize + validates :trailers, json_schema: { filename: 'git_trailers' } + # Sort by committed date in descending order to ensure latest commits comes on the top scope :order_by_committed_date_desc, -> { order('committed_date DESC') } diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb index b89d1983ce3..6f15df1b70f 100644 --- a/app/models/merge_request_context_commit_diff_file.rb +++ b/app/models/merge_request_context_commit_diff_file.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class MergeRequestContextCommitDiffFile < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + include Gitlab::EncodingHelper include ShaAttribute include DiffFile diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index d23e66b9697..fb873ddbbab 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -32,6 +32,7 @@ class MergeRequestDiff < ApplicationRecord has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) } validates :base_commit_sha, :head_commit_sha, :start_commit_sha, sha: true + validates :merge_request_id, uniqueness: { scope: :diff_type }, if: :merge_head? state_machine :state, initial: :empty do event :clean do @@ -50,6 +51,11 @@ class MergeRequestDiff < ApplicationRecord state :overflow_diff_lines_limit end + enum diff_type: { + regular: 1, + merge_head: 2 + } + scope :with_files, -> { without_states(:without_files, :empty) } scope :viewable, -> { without_state(:empty) } scope :by_commit_sha, ->(sha) do @@ -72,6 +78,7 @@ class MergeRequestDiff < ApplicationRecord join_condition = merge_requests[:id].eq(mr_diffs[:merge_request_id]) .and(mr_diffs[:id].not_eq(merge_requests[:latest_merge_request_diff_id])) + .and(mr_diffs[:diff_type].eq(diff_types[:regular])) arel_join = mr_diffs.join(merge_requests).on(join_condition) joins(arel_join.join_sources) @@ -196,6 +203,10 @@ class MergeRequestDiff < ApplicationRecord end def set_as_latest_diff + # Don't set merge_head diff as latest so it won't get considered as the + # MergeRequest#merge_request_diff. + return if merge_head? + MergeRequest .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id) .update_all(latest_merge_request_diff_id: self.id) @@ -203,8 +214,16 @@ class MergeRequestDiff < ApplicationRecord def ensure_commit_shas self.start_commit_sha ||= merge_request.target_branch_sha - self.head_commit_sha ||= merge_request.source_branch_sha - self.base_commit_sha ||= find_base_sha + + if merge_head? && merge_request.merge_ref_head.present? + diff_refs = merge_request.merge_ref_head.diff_refs + + self.head_commit_sha ||= diff_refs.head_sha + self.base_commit_sha ||= diff_refs.base_sha + else + self.head_commit_sha ||= merge_request.source_branch_sha + self.base_commit_sha ||= find_base_sha + end end # Override head_commit_sha to keep compatibility with merge request diff @@ -749,7 +768,7 @@ class MergeRequestDiff < ApplicationRecord end def sort_diffs? - Feature.enabled?(:sort_diffs, project, default_enabled: false) + Feature.enabled?(:sort_diffs, project, default_enabled: :yaml) end end diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 9f6933d0879..259690ef308 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class MergeRequestDiffCommit < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + include BulkInsertSafe include ShaAttribute include CachedCommit @@ -10,6 +12,9 @@ class MergeRequestDiffCommit < ApplicationRecord sha_attribute :sha alias_attribute :id, :sha + serialize :trailers, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize + validates :trailers, json_schema: { filename: 'git_trailers' } + # Deprecated; use `bulk_insert!` from `BulkInsertSafe` mixin instead. # cf. https://gitlab.com/gitlab-org/gitlab/issues/207989 for progress def self.create_bulk(merge_request_diff_id, commits) @@ -23,10 +28,30 @@ class MergeRequestDiffCommit < ApplicationRecord relative_order: index, sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]), - committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]) + committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]), + trailers: commit_hash.fetch(:trailers, {}).to_json ) end Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert end + + def self.oldest_merge_request_id_per_commit(project_id, shas) + # This method is defined here and not on MergeRequest, otherwise the SHA + # values used in the WHERE below won't be encoded correctly. + select(['merge_request_diff_commits.sha AS sha', 'min(merge_requests.id) AS merge_request_id']) + .joins(:merge_request_diff) + .joins( + 'INNER JOIN merge_requests ' \ + 'ON merge_requests.latest_merge_request_diff_id = merge_request_diffs.id' + ) + .where(sha: shas) + .where( + merge_requests: { + target_project_id: project_id, + state_id: MergeRequest.available_states[:merged] + } + ) + .group(:sha) + end end diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb index 817e77bf12f..f3f64971426 100644 --- a/app/models/merge_request_diff_file.rb +++ b/app/models/merge_request_diff_file.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class MergeRequestDiffFile < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + include BulkInsertSafe include Gitlab::EncodingHelper include DiffFile diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb index c4e5274f832..4a1f31a7f39 100644 --- a/app/models/merge_request_reviewer.rb +++ b/app/models/merge_request_reviewer.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true class MergeRequestReviewer < ApplicationRecord + enum state: { + unreviewed: 0, + reviewed: 1 + } + + validates :state, + presence: true, + inclusion: { in: MergeRequestReviewer.states.keys } + belongs_to :merge_request belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers end diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb index 2f2bf91e436..c6b5a967af9 100644 --- a/app/models/milestone_release.rb +++ b/app/models/milestone_release.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class MilestoneRelease < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + belongs_to :milestone belongs_to :release diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6f7b377ee52..3342fb1fce9 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -12,6 +12,7 @@ class Namespace < ApplicationRecord include FromUnion include Gitlab::Utils::StrongMemoize include IgnorableColumns + include Namespaces::Traversal::Recursive # Prevent users from creating unreasonably deep level of nesting. # The number 20 was taken based on maximum nesting level of @@ -103,6 +104,10 @@ class Namespace < ApplicationRecord ) end + # Make sure that the name is same as strong_memoize name in root_ancestor + # method + attr_writer :root_ancestor + class << self def by_path(path) find_by('lower(path) = :value', value: path.downcase) @@ -243,50 +248,6 @@ class Namespace < ApplicationRecord projects.with_shared_runners.any? end - # Returns all ancestors, self, and descendants of the current namespace. - def self_and_hierarchy - Gitlab::ObjectHierarchy - .new(self.class.where(id: id)) - .all_objects - end - - # Returns all the ancestors of the current namespaces. - def ancestors - return self.class.none unless parent_id - - Gitlab::ObjectHierarchy - .new(self.class.where(id: parent_id)) - .base_and_ancestors - end - - # returns all ancestors upto but excluding the given namespace - # when no namespace is given, all ancestors upto the top are returned - def ancestors_upto(top = nil, hierarchy_order: nil) - Gitlab::ObjectHierarchy.new(self.class.where(id: id)) - .ancestors(upto: top, hierarchy_order: hierarchy_order) - end - - def self_and_ancestors(hierarchy_order: nil) - return self.class.where(id: id) unless parent_id - - Gitlab::ObjectHierarchy - .new(self.class.where(id: id)) - .base_and_ancestors(hierarchy_order: hierarchy_order) - end - - # Returns all the descendants of the current namespace. - def descendants - Gitlab::ObjectHierarchy - .new(self.class.where(parent_id: id)) - .base_and_descendants - end - - def self_and_descendants - Gitlab::ObjectHierarchy - .new(self.class.where(id: id)) - .base_and_descendants - end - def user_ids_for_project_authorizations [owner_id] end @@ -312,14 +273,6 @@ class Namespace < ApplicationRecord parent_id.present? || parent.present? end - def root_ancestor - return self if persisted? && parent_id.nil? - - strong_memoize(:root_ancestor) do - self_and_ancestors.reorder(nil).find_by(parent_id: nil) - end - end - def subgroup? has_parent? end diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb new file mode 100644 index 00000000000..c46cc521735 --- /dev/null +++ b/app/models/namespaces/traversal/recursive.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Namespaces + module Traversal + module Recursive + extend ActiveSupport::Concern + + def root_ancestor + return self if persisted? && parent_id.nil? + + strong_memoize(:root_ancestor) do + self_and_ancestors.reorder(nil).find_by(parent_id: nil) + end + end + + # Returns all ancestors, self, and descendants of the current namespace. + def self_and_hierarchy + Gitlab::ObjectHierarchy + .new(self.class.where(id: id)) + .all_objects + end + + # Returns all the ancestors of the current namespaces. + def ancestors + return self.class.none unless parent_id + + Gitlab::ObjectHierarchy + .new(self.class.where(id: parent_id)) + .base_and_ancestors + end + + # returns all ancestors upto but excluding the given namespace + # when no namespace is given, all ancestors upto the top are returned + def ancestors_upto(top = nil, hierarchy_order: nil) + Gitlab::ObjectHierarchy.new(self.class.where(id: id)) + .ancestors(upto: top, hierarchy_order: hierarchy_order) + end + + def self_and_ancestors(hierarchy_order: nil) + return self.class.where(id: id) unless parent_id + + Gitlab::ObjectHierarchy + .new(self.class.where(id: id)) + .base_and_ancestors(hierarchy_order: hierarchy_order) + end + + # Returns all the descendants of the current namespace. + def descendants + Gitlab::ObjectHierarchy + .new(self.class.where(parent_id: id)) + .base_and_descendants + end + + def self_and_descendants + Gitlab::ObjectHierarchy + .new(self.class.where(id: id)) + .base_and_descendants + end + end + end +end diff --git a/app/models/note.rb b/app/models/note.rb index 77f7726079c..fdc972d9726 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -259,6 +259,10 @@ class Note < ApplicationRecord noteable_type == 'AlertManagement::Alert' end + def for_project_snippet? + noteable.is_a?(ProjectSnippet) + end + def for_personal_snippet? noteable.is_a?(PersonalSnippet) end @@ -542,7 +546,7 @@ class Note < ApplicationRecord end def skip_notification? - review.present? + review.present? || author.blocked? || author.ghost? end private diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb index 419bbd595e9..8a444f8934e 100644 --- a/app/models/onboarding_progress.rb +++ b/app/models/onboarding_progress.rb @@ -22,6 +22,24 @@ class OnboardingProgress < ApplicationRecord :repository_mirrored ].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) @@ -29,6 +47,10 @@ class OnboardingProgress < ApplicationRecord safe_find_or_create_by(namespace: namespace) end + def onboarding?(namespace) + where(namespace: namespace).any? + end + def register(namespace, action) return unless root_namespace?(namespace) && ACTIONS.include?(action) @@ -44,12 +66,12 @@ class OnboardingProgress < ApplicationRecord where(namespace: namespace).where.not(action_column => nil).exists? end - private - def column_name(action) :"#{action}_at" end + private + def root_namespace?(namespace) namespace && namespace.root? end diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 442f9d36c43..be3f719ddb3 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -6,6 +6,7 @@ module Operations include AtomicInternalId include IidRoutes include Limitable + include Referable self.table_name = 'operations_feature_flags' self.limit_scope = :project @@ -65,6 +66,31 @@ module Operations .reorder(:id) .references(:operations_scopes) end + + def reference_prefix + '[feature_flag:' + end + + def reference_pattern + @reference_pattern ||= %r{ + #{Regexp.escape(reference_prefix)}(#{::Project.reference_pattern}\/)?(?<feature_flag>\d+)#{Regexp.escape(reference_postfix)} + }x + end + + def link_reference_pattern + @link_reference_pattern ||= super("feature_flags", /(?<feature_flag>\d+)\/edit/) + end + + def reference_postfix + ']' + end + end + + def to_reference(from = nil, full: false) + project + .to_reference_base(from, full: full) + .then { |reference_base| reference_base.present? ? "#{reference_base}/" : nil } + .then { |reference_base| "#{self.class.reference_prefix}#{reference_base}#{iid}#{self.class.reference_postfix}" } end def related_issues(current_user, preload:) diff --git a/app/models/packages/composer/cache_file.rb b/app/models/packages/composer/cache_file.rb new file mode 100644 index 00000000000..ecd7596b989 --- /dev/null +++ b/app/models/packages/composer/cache_file.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Packages + module Composer + class CacheFile < ApplicationRecord + include FileStoreMounter + + self.table_name = 'packages_composer_cache_files' + + mount_file_store_uploader Packages::Composer::CacheUploader + + belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' + belongs_to :namespace + + validates :namespace, presence: true + + scope :with_namespace, ->(namespace) { where(namespace: namespace) } + scope :with_sha, ->(sha) { where(file_sha256: sha) } + scope :expired, -> { where("delete_at <= ?", Time.current) } + scope :without_namespace, -> { where(namespace_id: nil) } + end + end +end diff --git a/app/models/packages/composer/metadatum.rb b/app/models/packages/composer/metadatum.rb index 3026f5ea878..363858a3ed1 100644 --- a/app/models/packages/composer/metadatum.rb +++ b/app/models/packages/composer/metadatum.rb @@ -9,6 +9,9 @@ module Packages belongs_to :package, -> { where(package_type: :composer) }, inverse_of: :composer_metadatum validates :package, :target_sha, :composer_json, presence: true + + scope :for_package, ->(name, project_id) { joins(:package).where(packages_packages: { name: name, project_id: project_id, package_type: Packages::Package.package_types[:composer] }) } + scope :locked_for_update, -> { lock('FOR UPDATE') } end end end diff --git a/app/models/packages/debian/group_component.rb b/app/models/packages/debian/group_component.rb new file mode 100644 index 00000000000..81e02c363b0 --- /dev/null +++ b/app/models/packages/debian/group_component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupComponent < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::Component +end diff --git a/app/models/packages/debian/group_component_file.rb b/app/models/packages/debian/group_component_file.rb new file mode 100644 index 00000000000..333aab044a4 --- /dev/null +++ b/app/models/packages/debian/group_component_file.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupComponentFile < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::ComponentFile +end diff --git a/app/models/packages/debian/project_component.rb b/app/models/packages/debian/project_component.rb new file mode 100644 index 00000000000..98cd7fd589b --- /dev/null +++ b/app/models/packages/debian/project_component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectComponent < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::Component +end diff --git a/app/models/packages/debian/project_component_file.rb b/app/models/packages/debian/project_component_file.rb new file mode 100644 index 00000000000..60ac29f91c2 --- /dev/null +++ b/app/models/packages/debian/project_component_file.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectComponentFile < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::ComponentFile +end diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb index a73c12d172d..22f1008b3b5 100644 --- a/app/models/packages/debian/project_distribution.rb +++ b/app/models/packages/debian/project_distribution.rb @@ -5,5 +5,8 @@ class Packages::Debian::ProjectDistribution < ApplicationRecord :project end + has_many :publications, class_name: 'Packages::Debian::Publication', inverse_of: :distribution, foreign_key: :distribution_id + has_many :packages, class_name: 'Packages::Package', through: :publications + include Packages::Debian::Distribution end diff --git a/app/models/packages/debian/publication.rb b/app/models/packages/debian/publication.rb new file mode 100644 index 00000000000..93f5aa11d81 --- /dev/null +++ b/app/models/packages/debian/publication.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Packages::Debian::Publication < ApplicationRecord + belongs_to :package, + -> { where(package_type: :debian).where.not(version: nil) }, + inverse_of: :debian_publication, + class_name: 'Packages::Package' + belongs_to :distribution, + inverse_of: :publications, + class_name: 'Packages::Debian::ProjectDistribution', + foreign_key: :distribution_id + + validates :package, presence: true + validate :valid_debian_package_type + + validates :distribution, presence: true + + private + + def valid_debian_package_type + return errors.add(:package, _('type must be Debian')) unless package&.debian? + return errors.add(:package, _('must be a Debian package')) unless package.debian_package? + end +end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 2067a800ad5..391540634be 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -17,13 +17,18 @@ class Packages::Package < ApplicationRecord has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum' 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_many :build_infos, inverse_of: :package has_many :pipelines, through: :build_infos + has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication' + has_one :debian_distribution, through: :debian_publication, source: :distribution, inverse_of: :packages, class_name: 'Packages::Debian::ProjectDistribution' accepts_nested_attributes_for :conan_metadatum + accepts_nested_attributes_for :debian_publication accepts_nested_attributes_for :maven_metadatum delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan + delegate :codename, :suite, to: :debian_distribution, prefix: :debian_distribution validates :project, presence: true validates :name, presence: true @@ -31,7 +36,8 @@ class Packages::Package < ApplicationRecord validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: -> { conan? || generic? || debian? } validates :name, - uniqueness: { scope: %i[project_id version package_type] }, unless: :conan? + uniqueness: { scope: %i[project_id version package_type] }, unless: -> { conan? || debian_package? } + validate :unique_debian_package_name, if: :debian_package? validate :valid_conan_package_recipe, if: :conan? validate :valid_npm_package_name, if: :npm? @@ -59,7 +65,11 @@ class Packages::Package < ApplicationRecord if: :debian_package? validate :forbidden_debian_changes, if: :debian? - enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7, golang: 8, debian: 9 } + enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, + composer: 6, generic: 7, golang: 8, debian: 9, + rubygems: 10 } + + enum status: { default: 0, hidden: 1, processing: 2 } scope :with_name, ->(name) { where(name: name) } scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) } @@ -68,6 +78,8 @@ class Packages::Package < ApplicationRecord scope :with_version, ->(version) { where(version: version) } scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) } scope :with_package_type, ->(package_type) { where(package_type: package_type) } + scope :with_status, ->(status) { where(status: status) } + scope :displayable, -> { with_status(:default) } scope :including_build_info, -> { includes(pipelines: :user) } scope :including_project_route, -> { includes(project: { namespace: :route }) } scope :including_tags, -> { includes(:tags) } @@ -251,6 +263,18 @@ class Packages::Package < ApplicationRecord end end + def unique_debian_package_name + return unless debian_publication&.distribution + + package_exists = debian_publication.distribution.packages + .with_name(name) + .with_version(version) + .id_not_in(id) + .exists? + + errors.add(:base, _('Debian package already exists in Distribution')) if package_exists + end + def forbidden_debian_changes return unless persisted? diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index 389edaea392..9059f61b5de 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -55,10 +55,6 @@ class Packages::PackageFile < ApplicationRecord Gitlab::Routing.url_helpers.download_project_package_file_path(project, self) end - def local? - file_store == ::Packages::PackageFileUploader::Store::LOCAL - end - private def update_size_from_file diff --git a/app/models/packages/rubygems/metadatum.rb b/app/models/packages/rubygems/metadatum.rb new file mode 100644 index 00000000000..42db1f3defc --- /dev/null +++ b/app/models/packages/rubygems/metadatum.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Packages + module Rubygems + class Metadatum < ApplicationRecord + self.table_name = 'packages_rubygems_metadata' + self.primary_key = :package_id + + belongs_to :package, -> { where(package_type: :rubygems) }, inverse_of: :rubygems_metadatum + + validates :package, presence: true + + validate :rubygems_package_type + + private + + def rubygems_package_type + unless package&.rubygems? + errors.add(:base, _('Package type must be RubyGems')) + end + end + end + end +end diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index 84928468ad1..c6781f8f6e3 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -4,6 +4,8 @@ module Pages class LookupPath include Gitlab::Utils::StrongMemoize + LegacyStorageDisabledError = Class.new(::StandardError) + def initialize(project, trim_prefix: nil, domain: nil) @project = project @domain = domain @@ -24,7 +26,7 @@ module Pages end def source - zip_source || file_source + zip_source || legacy_source end def prefix @@ -52,6 +54,8 @@ module Pages return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project) + return if deployment.migrated? && !Feature.enabled?(:pages_serve_from_migrated_zip, project) + global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s { @@ -64,11 +68,17 @@ module Pages } end - def file_source + def legacy_source + raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true) + { type: 'file', path: File.join(project.full_path, 'public/') } + rescue LegacyStorageDisabledError => e + Gitlab::ErrorTracking.track_exception(e) + + nil end end end diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb index 7e42b8e6ae2..90cb8253b52 100644 --- a/app/models/pages/virtual_domain.rb +++ b/app/models/pages/virtual_domain.rb @@ -17,9 +17,16 @@ module Pages end def lookup_paths - projects.map do |project| + paths = projects.map do |project| project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain) - end.sort_by(&:prefix).reverse + end + + # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524 + # source can only be nil if pages_serve_from_legacy_storage FF is disabled + # we can remove this filtering once we remove legacy storage + paths = paths.select(&:source) + + paths.sort_by(&:prefix).reverse end private diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index 61818a63764..d67a92af6af 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -2,14 +2,18 @@ # PagesDeployment stores a zip archive containing GitLab Pages web-site class PagesDeployment < ApplicationRecord + include EachBatch include FileStoreMounter + MIGRATED_FILE_NAME = "_migrated.zip" + attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store } belongs_to :project, optional: false belongs_to :ci_build, class_name: 'Ci::Build', optional: true scope :older_than, -> (id) { where('id < ?', id) } + scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) } validates :file, presence: true validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } @@ -25,6 +29,10 @@ class PagesDeployment < ApplicationRecord # this is to be adressed in https://gitlab.com/groups/gitlab-org/-/epics/589 end + def migrated? + file.filename == MIGRATED_FILE_NAME + end + private def set_size diff --git a/app/models/project.rb b/app/models/project.rb index ec790798806..2b9b7dcf733 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -75,7 +75,7 @@ class Project < ApplicationRecord default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) do - pick_repository_storage + Repository.pick_storage_shard end default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled } @@ -117,7 +117,7 @@ class Project < ApplicationRecord use_fast_destroy :build_trace_chunks - after_destroy -> { run_after_commit { remove_pages } } + after_destroy -> { run_after_commit { legacy_remove_pages } } after_destroy :remove_exports after_validation :check_pending_delete @@ -200,7 +200,7 @@ class Project < ApplicationRecord # Packages has_many :packages, class_name: 'Packages::Package' has_many :package_files, through: :packages, class_name: 'Packages::PackageFile' - # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project @@ -218,6 +218,7 @@ class Project < ApplicationRecord # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project + has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues has_many :labels, class_name: 'ProjectLabel' @@ -410,7 +411,7 @@ class Project < ApplicationRecord delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci - delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, to: :ci_cd_settings, prefix: :ci + delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, :keep_latest_artifacts_available?, to: :ci_cd_settings delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?, to: :ci_cd_settings delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true @@ -456,7 +457,7 @@ class Project < ApplicationRecord validates :repository_storage, presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } - validates :variables, variable_duplicates: { scope: :environment_scope } + validates :variables, nested_attributes_duplicates: { scope: :environment_scope } validates :bfg_object_map, file_size: { maximum: :max_attachment_size } validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true } @@ -836,8 +837,12 @@ class Project < ApplicationRecord webide_pipelines.running_or_pending.for_user(user) end - def latest_pipeline_locked - ci_keep_latest_artifact? ? :artifacts_locked : :unlocked + def default_pipeline_lock + if keep_latest_artifacts_available? + return :artifacts_locked + end + + :unlocked end def autoclose_referenced_issues @@ -1314,21 +1319,11 @@ class Project < ApplicationRecord end def external_issue_tracker - if has_external_issue_tracker.nil? - cache_has_external_issue_tracker - end + cache_has_external_issue_tracker if has_external_issue_tracker.nil? - if has_external_issue_tracker? - strong_memoize(:external_issue_tracker) do - services.external_issue_trackers.first - end - else - nil - end - end + return unless has_external_issue_tracker? - def cache_has_external_issue_tracker - update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write? + @external_issue_tracker ||= services.external_issue_trackers.first end def external_references_supported? @@ -1356,9 +1351,9 @@ class Project < ApplicationRecord end def disabled_services - return %w(datadog alerts) unless Feature.enabled?(:datadog_ci_integration, self) + return %w(datadog) unless Feature.enabled?(:datadog_ci_integration, self) - %w(alerts) + [] end def find_or_initialize_service(name) @@ -1797,16 +1792,16 @@ class Project < ApplicationRecord .delete_all end - # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal? + # TODO: remove this method https://gitlab.com/gitlab-org/gitlab/-/issues/320775 # rubocop: disable CodeReuse/ServiceClass - def remove_pages + def legacy_remove_pages + return unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true) + # Projects with a missing namespace cannot have their pages removed return unless namespace mark_pages_as_not_deployed unless destroyed? - DestroyPagesDeploymentsWorker.perform_async(id) - # 1. We rename pages to temporary directory # 2. We wait 5 minutes, due to NFS caching # 3. We asynchronously remove pages with force @@ -2532,6 +2527,11 @@ class Project < ApplicationRecord tracing_setting&.external_url end + override :git_garbage_collect_worker_klass + def git_garbage_collect_worker_klass + Projects::GitGarbageCollectWorker + end + private def find_service(services, name) @@ -2690,6 +2690,10 @@ class Project < ApplicationRecord def cache_has_external_wiki update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? end + + def cache_has_external_issue_tracker + update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write? + end end Project.prepend_if_ee('EE::Project') diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index 366852d93bf..2c3f70654f8 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ProjectAuthorization < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning include FromUnion belongs_to :user diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index e5fc481b035..31be0759cd0 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -21,6 +21,11 @@ class ProjectCiCdSetting < ApplicationRecord super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true) end + def keep_latest_artifacts_available? + # The project level feature can only be enabled when the feature is enabled instance wide + Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact? + end + private def set_default_git_depth diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb index 2bef0056732..58dbac9057f 100644 --- a/app/models/project_pages_metadatum.rb +++ b/app/models/project_pages_metadatum.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProjectPagesMetadatum < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + include EachBatch self.primary_key = :project_id @@ -11,4 +13,5 @@ class ProjectPagesMetadatum < ApplicationRecord scope :deployed, -> { where(deployed: true) } scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) } + scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) } end diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb deleted file mode 100644 index 4afce0dfe95..00000000000 --- a/app/models/project_services/alerts_service.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -# This service is scheduled for removal. All records must -# be deleted before the class can be removed. -# https://gitlab.com/groups/gitlab-org/-/epics/5056 -class AlertsService < Service - before_save :prevent_save - - def self.to_param - 'alerts' - end - - def self.supported_events - %w() - end - - private - - def prevent_save - errors.add(:base, _('Alerts endpoint is deprecated and should not be created or modified. Use HTTP Integrations instead.')) - log_error('Prevented attempt to save or update deprecated AlertsService') - - # Stops execution of callbacks and database operation while - # preserving expectations of #save (will not raise) & #save! (raises) - # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution - throw :abort # rubocop:disable Cop/BanCatchThrow - end -end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index c9e97efb4ac..1d50d5cf19e 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -11,11 +11,13 @@ class ChatNotificationService < Service tag_push pipeline wiki_page deployment ].freeze + SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze + EVENT_CHANNEL = proc { |event| "#{event}_channel" } default_value_for :category, 'chat' - prop_accessor :webhook, :username, :channel, :branches_to_be_notified + prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified # Custom serialized properties initialization prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) @@ -62,12 +64,16 @@ class ChatNotificationService < Service { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true }.freeze, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }.freeze, { type: 'checkbox', name: 'notify_only_broken_pipelines' }.freeze, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze + { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze, + { type: 'text', name: 'labels_to_be_notified', placeholder: 'e.g. ~backend', help: 'Only supported for issue, merge request and note events.' }.freeze ].freeze end def execute(data) return unless supported_events.include?(data[:object_kind]) + + return unless notify_label?(data) + return unless webhook.present? object_kind = data[:object_kind] @@ -114,6 +120,22 @@ class ChatNotificationService < Service private + def labels_to_be_notified_list + return [] if labels_to_be_notified.nil? + + labels_to_be_notified.delete('~').split(',').map(&:strip) + end + + def notify_label?(data) + return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present? + + issue_labels = data.dig(:issue, :labels) || [] + merge_request_labels = data.dig(:merge_request, :labels) || [] + label_titles = (issue_labels + merge_request_labels).pluck(:title) + + (labels_to_be_notified_list & label_titles).any? + end + # every notifier must implement this independently def notify(message, opts) raise NotImplementedError diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb index 6db446fc04c..8a6f4de540c 100644 --- a/app/models/project_services/confluence_service.rb +++ b/app/models/project_services/confluence_service.rb @@ -30,8 +30,8 @@ class ConfluenceService < Service s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab') end - def detailed_description - return unless project.wiki_enabled? + def help + return unless project&.wiki_enabled? if activated? wiki_url = project.wiki.web_url diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb index 3a742bfdcda..a48dea71645 100644 --- a/app/models/project_services/datadog_service.rb +++ b/app/models/project_services/datadog_service.rb @@ -12,14 +12,22 @@ class DatadogService < Service prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env - with_options presence: true, if: :activated? do - validates :api_key, format: { with: /\A\w+\z/ } - validates :datadog_site, format: { with: /\A[\w\.]+\z/ }, unless: :api_url - validates :api_url, public_url: true, unless: :datadog_site + with_options if: :activated? do + validates :api_key, presence: true, format: { with: /\A\w+\z/ } + validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true } + validates :api_url, public_url: { allow_blank: true } + validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? } + validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? } end after_save :compose_service_hook, if: :activated? + def initialize_properties + super + + self.datadog_site ||= DEFAULT_SITE + end + def self.supported_events SUPPORTED_EVENTS end @@ -54,27 +62,37 @@ class DatadogService < Service def fields [ { - type: 'text', name: 'datadog_site', - placeholder: DEFAULT_SITE, default: DEFAULT_SITE, + type: 'text', + name: 'datadog_site', + placeholder: DEFAULT_SITE, help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site', required: false }, { - type: 'text', name: 'api_url', title: 'Custom URL', + type: 'text', + name: 'api_url', + title: 'API URL', help: '(Advanced) Define the full URL for your Datadog site directly', required: false }, { - type: 'password', name: 'api_key', title: 'API key', + type: 'password', + name: 'api_key', + title: 'API key', help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog", required: true }, { - type: 'text', name: 'datadog_service', title: 'Service', placeholder: 'gitlab-ci', + type: 'text', + name: 'datadog_service', + title: 'Service', + placeholder: 'gitlab-ci', help: 'Name of this GitLab instance that all data will be tagged with' }, { - type: 'text', name: 'datadog_env', title: 'Env', + type: 'text', + name: 'datadog_env', + title: 'Env', help: 'The environment tag that traces will be tagged with' } ] @@ -90,7 +108,7 @@ class DatadogService < Service url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site) url = URI.parse(url) url.path = File.join(url.path || '/', api_key) - query = { service: datadog_service, env: datadog_env }.compact + query = { service: datadog_service.presence, env: datadog_env.presence }.compact url.query = query.to_query unless query.empty? url.to_s end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index dafd3d095ec..5857d86f921 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Accessible as Project#external_issue_tracker class JiraService < IssueTrackerService extend ::Gitlab::Utils::Override include Gitlab::Routing @@ -30,7 +31,8 @@ class JiraService < IssueTrackerService # TODO: we can probably just delegate as part of # https://gitlab.com/gitlab-org/gitlab/issues/29404 - data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled, :vulnerabilities_enabled, :vulnerabilities_issuetype + data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled, + :vulnerabilities_enabled, :vulnerabilities_issuetype, :proxy_address, :proxy_port, :proxy_username, :proxy_password before_update :reset_password after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type? @@ -157,11 +159,14 @@ class JiraService < IssueTrackerService # support any events. end - def find_issue(issue_key) - jira_request { client.Issue.find(issue_key) } + def find_issue(issue_key, rendered_fields: false) + options = {} + options = options.merge(expand: 'renderedFields') if rendered_fields + + jira_request { client.Issue.find(issue_key, options) } end - def close_issue(entity, external_issue) + def close_issue(entity, external_issue, current_user) issue = find_issue(external_issue.iid) return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? @@ -178,6 +183,7 @@ class JiraService < IssueTrackerService # if it is closed, so we don't have one comment for every commit. issue = find_issue(issue.key) if transition_issue(issue) add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) + log_usage(:close_issue, current_user) end def create_cross_reference_note(mentioned, noteable, author) @@ -213,7 +219,7 @@ class JiraService < IssueTrackerService } } - add_comment(data, jira_issue) + add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) } end def valid_connection? @@ -274,6 +280,12 @@ class JiraService < IssueTrackerService end end + def log_usage(action, user) + key = "i_ecosystem_jira_service_#{action}" + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id) + end + def add_issue_solved_comment(issue, commit_id, commit_url) link_title = "Solved by commit #{commit_id}." comment = "Issue solved with [#{commit_id}|#{commit_url}]." diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb index 00b6ab6a70f..6cbcb1550c1 100644 --- a/app/models/project_services/jira_tracker_data.rb +++ b/app/models/project_services/jira_tracker_data.rb @@ -7,6 +7,15 @@ class JiraTrackerData < ApplicationRecord attr_encrypted :api_url, encryption_options attr_encrypted :username, encryption_options attr_encrypted :password, encryption_options + attr_encrypted :proxy_address, encryption_options + attr_encrypted :proxy_port, encryption_options + attr_encrypted :proxy_username, encryption_options + attr_encrypted :proxy_password, encryption_options + + validates :proxy_address, length: { maximum: 2048 } + validates :proxy_port, length: { maximum: 5 } + validates :proxy_username, length: { maximum: 255 } + validates :proxy_password, length: { maximum: 255 } enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment end diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb deleted file mode 100644 index e55335d9aae..00000000000 --- a/app/models/project_services/mock_deployment_service.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -# Deprecated, to be deleted in 13.8 (https://gitlab.com/gitlab-org/gitlab/-/issues/293914) -# -# This was a class used only in development environment but became unusable -# since DeploymentService was deleted -class MockDeploymentService < Service - default_value_for :category, 'deployment' - - def title - 'Mock deployment' - end - - def description - 'Mock deployment service' - end - - def self.to_param - 'mock_deployment' - end - - # No terminals support - def terminals(environment) - [] - end - - def self.supported_events - %w() - end - - def predefined_variables(project:, environment_name:) - [] - end - - def can_test? - false - end -end diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index aca7eec3382..83ff0702b88 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -21,4 +21,4 @@ class ProjectSetting < ApplicationRecord end end -ProjectSetting.prepend_if_ee('EE::ProjectSetting') +ProjectSetting.prepend_ee_mod diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 7605ef54d5b..8c3dcaa7c0f 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -31,6 +31,7 @@ class ProjectStatistics < ApplicationRecord scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) } + scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) } def total_repository_size repository_size + lfs_objects_size diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index f28440f2444..ea51dca8a42 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -19,7 +19,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord end def check_access(user) - if Feature.enabled?(:deploy_keys_on_protected_branches, project) && user && deploy_key.present? + if user && deploy_key.present? return true if user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user) end diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb index 6a32c480b04..2786ecb641a 100644 --- a/app/models/push_event_payload.rb +++ b/app/models/push_event_payload.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PushEventPayload < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + include ShaAttribute belongs_to :event, inverse_of: :push_event_payload diff --git a/app/models/readme_blob.rb b/app/models/readme_blob.rb deleted file mode 100644 index 695b4e3ffe3..00000000000 --- a/app/models/readme_blob.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class ReadmeBlob < SimpleDelegator - include BlobActiveModel - - attr_reader :repository - - def initialize(blob, repository) - @repository = repository - - super(blob) - end - - def rendered_markup - repository.rendered_readme - end -end diff --git a/app/models/release.rb b/app/models/release.rb index 2b82fdc37f6..60c2abcacb3 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -24,6 +24,7 @@ class Release < ApplicationRecord validates :project, :tag, presence: true validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } + validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] } scope :sorted, -> { order(released_at: :desc) } scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) } diff --git a/app/models/repository.rb b/app/models/repository.rb index c19448332f8..06a13194e1a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -39,11 +39,11 @@ class Repository # # For example, for entry `:commit_count` there's a method called `commit_count` which # stores its data in the `commit_count` cache key. - CACHED_METHODS = %i(size commit_count rendered_readme readme_path contribution_guide + CACHED_METHODS = %i(size commit_count readme_path contribution_guide changelog license_blob license_key gitignore gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? root_ref merged_branch_names - has_visible_content? issue_template_names merge_request_template_names + has_visible_content? issue_template_names_by_category merge_request_template_names_by_category user_defined_metrics_dashboard_paths xcode_project? has_ambiguous_refs?).freeze # Methods that use cache_method but only memoize the value @@ -53,15 +53,15 @@ class Repository # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # the corresponding methods to call for refreshing caches. METHOD_CACHES_FOR_FILE_TYPES = { - readme: %i(rendered_readme readme_path), + readme: %i(readme_path), changelog: :changelog, license: %i(license_blob license_key license), contributing: :contribution_guide, gitignore: :gitignore, gitlab_ci: :gitlab_ci_yml, avatar: :avatar, - issue_template: :issue_template_names, - merge_request_template: :merge_request_template_names, + issue_template: :issue_template_names_by_category, + merge_request_template: :merge_request_template_names_by_category, metrics_dashboard: :user_defined_metrics_dashboard_paths, xcode_config: :xcode_project? }.freeze @@ -151,7 +151,8 @@ class Repository all: !!opts[:all], first_parent: !!opts[:first_parent], order: opts[:order], - literal_pathspec: opts.fetch(:literal_pathspec, true) + literal_pathspec: opts.fetch(:literal_pathspec, true), + trailers: opts[:trailers] } commits = Gitlab::Git::Commit.where(options) @@ -497,23 +498,7 @@ class Repository end def blob_at(sha, path) - blob = Blob.decorate(raw_repository.blob_at(sha, path), container) - - # Don't attempt to return a special result if there is no blob at all - return unless blob - - # Don't attempt to return a special result if this can't be a README - return blob unless Gitlab::FileDetector.type_of(blob.name) == :readme - - # Don't attempt to return a special result unless we're looking at HEAD - return blob unless head_commit&.sha == sha - - case path - when head_tree&.readme_path - ReadmeBlob.new(blob, self) - else - blob - end + Blob.decorate(raw_repository.blob_at(sha, path), container) rescue Gitlab::Git::Repository::NoRepository nil end @@ -587,15 +572,16 @@ class Repository end cache_method :avatar - def issue_template_names - Gitlab::Template::IssueTemplate.dropdown_names(project) + # store issue_template_names as hash + def issue_template_names_by_category + Gitlab::Template::IssueTemplate.repository_template_names(project) end - cache_method :issue_template_names, fallback: [] + cache_method :issue_template_names_by_category, fallback: {} - def merge_request_template_names - Gitlab::Template::MergeRequestTemplate.dropdown_names(project) + def merge_request_template_names_by_category + Gitlab::Template::MergeRequestTemplate.repository_template_names(project) end - cache_method :merge_request_template_names, fallback: [] + cache_method :merge_request_template_names_by_category, fallback: {} def user_defined_metrics_dashboard_paths Gitlab::Metrics::Dashboard::RepoDashboardFinder.list_dashboards(project) @@ -611,15 +597,6 @@ class Repository end cache_method :readme_path - def rendered_readme - return unless readme - - context = { project: project } - - MarkupHelper.markup_unsafe(readme.name, readme.data, context) - end - cache_method :rendered_readme - def contribution_guide file_on_head(:contributing) end @@ -1058,6 +1035,10 @@ class Repository blob_data_at(sha, '.lfsconfig') end + def changelog_config(ref = 'HEAD') + blob_data_at(ref, Gitlab::Changelog::Config::FILE_PATH) + end + def fetch_ref(source_repository, source_ref:, target_ref:) raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) end @@ -1142,6 +1123,13 @@ class Repository end end + # Choose one of the available repository storage options based on a normalized weighted probability. + # We should always use the latest settings, to avoid picking a deleted shard. + def self.pick_storage_shard(expire: true) + Gitlab::CurrentSettings.expire_current_application_settings if expire + Gitlab::CurrentSettings.pick_repository_storage + end + private # TODO Genericize finder, later split this on finders by Ref or Oid diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb index 6b1793a551f..b7a96211fb1 100644 --- a/app/models/repository_language.rb +++ b/app/models/repository_language.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class RepositoryLanguage < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + belongs_to :project belongs_to :programming_language diff --git a/app/models/service.rb b/app/models/service.rb index e5626462dd3..c49e0869b21 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -46,7 +46,6 @@ class Service < ApplicationRecord after_initialize :initialize_properties after_commit :reset_updated_properties - after_commit :cache_project_has_external_issue_tracker belongs_to :project, inverse_of: :services belongs_to :group, inverse_of: :services @@ -55,11 +54,11 @@ class Service < ApplicationRecord validates :project_id, presence: true, unless: -> { template? || instance? || group_id } validates :group_id, presence: true, unless: -> { template? || instance? || project_id } validates :project_id, :group_id, absence: true, if: -> { template? || instance? } - validates :type, uniqueness: { scope: :project_id }, unless: -> { template? || instance? || group_id }, on: :create - validates :type, uniqueness: { scope: :group_id }, unless: -> { template? || instance? || project_id } validates :type, presence: true - validates :template, uniqueness: { scope: :type }, if: -> { template? } - validates :instance, uniqueness: { scope: :type }, if: -> { instance? } + validates :type, uniqueness: { scope: :template }, if: :template? + validates :type, uniqueness: { scope: :instance }, if: :instance? + validates :type, uniqueness: { scope: :project_id }, if: :project_id? + validates :type, uniqueness: { scope: :group_id }, if: :group_id? validate :validate_is_instance_or_template validate :validate_belongs_to_project_or_group @@ -438,10 +437,6 @@ class Service < ApplicationRecord ProjectServiceWorker.perform_async(id, data) end - def external_issue_tracker? - category == :issue_tracker && active? - end - def external_wiki? type == 'ExternalWikiService' && active? end @@ -461,12 +456,6 @@ class Service < ApplicationRecord errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id end - def cache_project_has_external_issue_tracker - if project && !project.destroyed? - project.cache_has_external_issue_tracker - end - end - def valid_recipients? activated? && !importing? end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index c4a7c5e25dc..ab8782ed87f 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -317,7 +317,7 @@ class Snippet < ApplicationRecord end def repository_storage - snippet_repository&.shard_name || self.class.pick_repository_storage + snippet_repository&.shard_name || Repository.pick_storage_shard end # Repositories are created by default with the `master` branch. diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index efbbd86ae4a..eb7d465d585 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -22,7 +22,9 @@ module Terraform scope :versioning_not_enabled, -> { where(versioning_enabled: false) } scope :ordered_by_name, -> { order(:name) } + scope :with_name, -> (name) { where(name: name) } + validates :name, presence: true, uniqueness: { scope: :project_id } validates :project_id, presence: true validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, format: { with: HEX_REGEXP, message: 'only allows hex characters' } diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb index 19d708616fc..432ac5b6422 100644 --- a/app/models/terraform/state_version.rb +++ b/app/models/terraform/state_version.rb @@ -9,16 +9,14 @@ module Terraform belongs_to :build, class_name: 'Ci::Build', optional: true, foreign_key: :ci_build_id scope :ordered_by_version_desc, -> { order(version: :desc) } + scope :with_files_stored_locally, -> { where(file_store: Terraform::StateUploader::Store::LOCAL) } + scope :preload_state, -> { includes(:terraform_state) } default_value_for(:file_store) { StateUploader.default_store } mount_file_store_uploader StateUploader delegate :project_id, :uuid, to: :terraform_state, allow_nil: true - - def local? - file_store == ObjectStorage::Store::LOCAL - end end end diff --git a/app/models/token_with_iv.rb b/app/models/token_with_iv.rb new file mode 100644 index 00000000000..115f40b4a82 --- /dev/null +++ b/app/models/token_with_iv.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# rubocop: todo Gitlab/NamespacedClass +class TokenWithIv < ApplicationRecord + validates :hashed_token, presence: true + validates :iv, presence: true + validates :hashed_plaintext_token, presence: true + + def self.find_by_hashed_token(value) + find_by(hashed_token: ::Digest::SHA256.digest(value)) + end + + def self.find_by_plaintext_token(value) + find_by(hashed_plaintext_token: ::Digest::SHA256.digest(value)) + end + + def self.find_nonce_by_hashed_token(value) + return unless table_exists? + + token_record = find_by_hashed_token(value) + token_record&.iv + end +end diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb index 1a389081913..65dc7a47533 100644 --- a/app/models/u2f_registration.rb +++ b/app/models/u2f_registration.rb @@ -4,11 +4,19 @@ class U2fRegistration < ApplicationRecord belongs_to :user - after_commit :schedule_webauthn_migration, on: :create - after_commit :update_webauthn_registration, on: :update, if: :counter_changed? - def schedule_webauthn_migration - BackgroundMigrationWorker.perform_async('MigrateU2fWebauthn', [id, id]) + after_create :create_webauthn_registration + after_update :update_webauthn_registration, if: :counter_changed? + + def create_webauthn_registration + converter = Gitlab::Auth::U2fWebauthnConverter.new(self) + WebauthnRegistration.create!(converter.convert) + rescue StandardError => ex + Gitlab::AppJsonLogger.error( + event: 'u2f_migration', + error: ex.class.name, + backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(ex.backtrace), + message: "U2F to WebAuthn conversion failed") end def update_webauthn_registration diff --git a/app/models/user.rb b/app/models/user.rb index b4ec6064ff8..1f8b680c7e5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -116,6 +116,13 @@ class User < ApplicationRecord has_one :user_synced_attributes_metadata, autosave: true has_one :aws_role, class_name: 'Aws::Role' + # Followers + has_many :followed_users, foreign_key: :follower_id, class_name: 'Users::UserFollowUser' + has_many :followees, through: :followed_users + + has_many :following_users, foreign_key: :followee_id, class_name: 'Users::UserFollowUser' + has_many :followers, through: :following_users + # Groups has_many :members has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember' @@ -960,8 +967,8 @@ class User < ApplicationRecord end # rubocop: disable CodeReuse/ServiceClass - def refresh_authorized_projects - Users::RefreshAuthorizedProjectsService.new(self).execute + def refresh_authorized_projects(source: nil) + Users::RefreshAuthorizedProjectsService.new(self, source: source).execute end # rubocop: enable CodeReuse/ServiceClass @@ -1442,6 +1449,29 @@ class User < ApplicationRecord end end + def following?(user) + self.followees.exists?(user.id) + end + + def follow(user) + return false if self.id == user.id + + begin + followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id) + self.followees.reset if followee.persisted? + rescue ActiveRecord::RecordNotUnique + false + end + end + + def unfollow(user) + if Users::UserFollowUser.where(follower_id: self.id, followee_id: user.id).delete_all > 0 + self.followees.reset + else + false + end + end + def manageable_namespaces @manageable_namespaces ||= [namespace] + manageable_groups end diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index ad5651f9439..d93fe611538 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -7,19 +7,19 @@ class UserCallout < ApplicationRecord gke_cluster_integration: 1, gcp_signup_offer: 2, cluster_security_warning: 3, - gold_trial: 4, # EE-only - geo_enable_hashed_storage: 5, # EE-only - geo_migrate_hashed_storage: 6, # EE-only - canary_deployment: 7, # EE-only - gold_trial_billings: 8, # EE-only + gold_trial: 4, # EE-only + geo_enable_hashed_storage: 5, # EE-only + geo_migrate_hashed_storage: 6, # EE-only + canary_deployment: 7, # EE-only + gold_trial_billings: 8, # EE-only suggest_popover_dismissed: 9, tabs_position_highlight: 10, - threat_monitoring_info: 11, # EE-only - account_recovery_regular_check: 12, # EE-only + threat_monitoring_info: 11, # EE-only + account_recovery_regular_check: 12, # EE-only webhooks_moved: 13, service_templates_deprecated: 14, admin_integrations_moved: 15, - web_ide_alert_dismissed: 16, # no longer in use + web_ide_alert_dismissed: 16, # no longer in use active_user_count_threshold: 18, # EE-only buy_pipeline_minutes_notification_dot: 19, # EE-only personal_access_token_expiry: 21, # EE-only @@ -27,7 +27,9 @@ class UserCallout < ApplicationRecord customize_homepage: 23, feature_flags_new_version: 24, registration_enabled_callout: 25, - new_user_signups_cap_reached: 26 # EE-only + new_user_signups_cap_reached: 26, # EE-only + unfinished_tag_cleanup_callout: 27, + eoa_bronze_plan_banner: 28 # EE-only } validates :user, presence: true diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb index 7e7a387d3d4..4c8cc5fc83a 100644 --- a/app/models/user_interacted_project.rb +++ b/app/models/user_interacted_project.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class UserInteractedProject < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + belongs_to :user belongs_to :project diff --git a/app/models/user_status.rb b/app/models/user_status.rb index 0e1ae0b7338..1c8634e47c3 100644 --- a/app/models/user_status.rb +++ b/app/models/user_status.rb @@ -7,6 +7,16 @@ class UserStatus < ApplicationRecord DEFAULT_EMOJI = 'speech_balloon' + CLEAR_STATUS_QUICK_OPTIONS = { + '30_minutes' => 30.minutes, + '3_hours' => 3.hours, + '8_hours' => 8.hours, + '1_day' => 1.day, + '3_days' => 3.days, + '7_days' => 7.days, + '30_days' => 30.days + }.freeze + belongs_to :user enum availability: { not_set: 0, busy: 1 } @@ -15,5 +25,11 @@ class UserStatus < ApplicationRecord validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names } validates :message, length: { maximum: 100 }, allow_blank: true + scope :scheduled_for_cleanup, -> { where(arel_table[:clear_status_at].lteq(Time.current)) } + cache_markdown_field :message, pipeline: :emoji + + def clear_status_after=(value) + self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now + end end diff --git a/app/models/users/user_follow_user.rb b/app/models/users/user_follow_user.rb new file mode 100644 index 00000000000..a94239a746c --- /dev/null +++ b/app/models/users/user_follow_user.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +module Users + class UserFollowUser < ApplicationRecord + belongs_to :follower, class_name: 'User' + belongs_to :followee, class_name: 'User' + end +end diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb index ab29afd0d08..7728c9c174e 100644 --- a/app/models/vulnerability.rb +++ b/app/models/vulnerability.rb @@ -12,17 +12,9 @@ class Vulnerability < ApplicationRecord '[vulnerability:' end - def self.reference_prefix_escaped - '[vulnerability[' - end - def self.reference_postfix ']' end - - def self.reference_postfix_escaped - ']' - end end -Vulnerability.prepend_if_ee('EE::Vulnerability') +Vulnerability.prepend_ee_mod diff --git a/app/models/wiki.rb b/app/models/wiki.rb index 11c10a61d18..45747c0b03c 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -104,7 +104,7 @@ class Wiki end def empty? - list_pages(limit: 1).empty? + !repository_exists? || list_pages(limit: 1).empty? end def exists? @@ -256,6 +256,15 @@ class Wiki def after_post_receive end + override :git_garbage_collect_worker_klass + def git_garbage_collect_worker_klass + Wikis::GitGarbageCollectWorker + end + + def cleanup + @repository = nil + end + private def commit_details(action, message = nil, title = nil) |