From 6438df3a1e0fb944485cebf07976160184697d72 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 20 Jan 2021 13:34:23 -0600 Subject: Add latest changes from gitlab-org/gitlab@13-8-stable-ee --- app/models/alert_management/http_integration.rb | 4 + app/models/application_record.rb | 5 + app/models/application_setting.rb | 46 ++++++++- app/models/application_setting_implementation.rb | 6 +- app/models/audit_event.rb | 3 + app/models/audit_event_archived.rb | 10 ++ app/models/audit_event_partitioned.rb | 14 --- app/models/authentication_event.rb | 8 +- app/models/blob_viewer/gitlab_ci_yml.rb | 4 +- app/models/ci/bridge.rb | 14 +-- app/models/ci/build.rb | 56 ++++++++-- app/models/ci/build_need.rb | 2 +- app/models/ci/build_runner_session.rb | 4 +- app/models/ci/commit_with_pipeline.rb | 38 +++++++ app/models/ci/group.rb | 24 ++++- app/models/ci/job_artifact.rb | 10 +- app/models/ci/pipeline.rb | 6 +- app/models/ci/ref.rb | 7 ++ app/models/clusters/applications/cert_manager.rb | 2 +- app/models/clusters/applications/knative.rb | 2 +- app/models/clusters/applications/runner.rb | 2 +- app/models/clusters/platforms/kubernetes.rb | 2 +- app/models/commit.rb | 8 +- app/models/commit_status.rb | 12 ++- app/models/commit_with_pipeline.rb | 2 +- app/models/concerns/boards/listable.rb | 52 ++++++++++ app/models/concerns/bulk_insert_safe.rb | 4 +- app/models/concerns/ci/artifactable.rb | 3 +- app/models/concerns/each_batch.rb | 16 +-- app/models/concerns/enums/ci/commit_status.rb | 36 +++++++ app/models/concerns/enums/commit_status.rb | 35 ------- app/models/concerns/enums/vulnerability.rb | 46 +++++++++ app/models/concerns/issuable.rb | 11 ++ app/models/concerns/issue_available_features.rb | 5 +- app/models/concerns/milestoneable.rb | 2 +- app/models/concerns/noteable.rb | 21 ++++ .../concerns/packages/debian/architecture.rb | 25 +++++ .../concerns/packages/debian/distribution.rb | 115 +++++++++++++++++++++ .../repositories/can_housekeep_repository.rb | 25 +++++ app/models/cycle_analytics/level_base.rb | 74 ------------- app/models/cycle_analytics/project_level.rb | 21 +++- .../cycle_analytics/project_level_stage_adapter.rb | 44 ++++++++ app/models/deployment.rb | 17 +++ app/models/diff_note.rb | 4 + app/models/experiment.rb | 4 +- app/models/gpg_key.rb | 2 + app/models/group.rb | 3 + app/models/issue.rb | 4 + app/models/list.rb | 36 +------ app/models/member.rb | 2 + app/models/members/group_member.rb | 12 ++- app/models/merge_request.rb | 33 +++--- app/models/milestone.rb | 1 + app/models/namespace.rb | 11 +- app/models/namespace/package_setting.rb | 28 +++++ app/models/namespace/root_storage_statistics.rb | 2 +- app/models/namespace_onboarding_action.rb | 27 ----- app/models/onboarding_progress.rb | 65 ++++++++++++ app/models/packages/conan/file_metadatum.rb | 4 +- app/models/packages/debian.rb | 9 ++ app/models/packages/debian/file_metadatum.rb | 56 ++++++++++ app/models/packages/debian/group_architecture.rb | 9 ++ app/models/packages/debian/group_distribution.rb | 9 ++ app/models/packages/debian/project_architecture.rb | 9 ++ app/models/packages/debian/project_distribution.rb | 9 ++ app/models/packages/dependency.rb | 2 +- app/models/packages/event.rb | 28 +++-- app/models/packages/package.rb | 7 ++ app/models/packages/package_file.rb | 9 ++ app/models/pages_domain.rb | 4 +- app/models/plan.rb | 2 +- app/models/project.rb | 71 ++++++------- app/models/project_feature_usage.rb | 4 +- app/models/project_pages_metadatum.rb | 3 + app/models/project_services/alerts_service.rb | 86 ++------------- app/models/project_services/alerts_service_data.rb | 14 --- app/models/project_services/datadog_service.rb | 8 +- app/models/project_services/jira_service.rb | 10 +- .../mattermost_slash_commands_service.rb | 2 +- .../slack_slash_commands_service.rb | 2 +- app/models/protectable_dropdown.rb | 2 + app/models/release.rb | 2 +- app/models/remote_mirror.rb | 2 +- app/models/repository.rb | 21 +--- app/models/service.rb | 8 -- app/models/snippet.rb | 18 +++- app/models/snippet_repository.rb | 1 + app/models/snippet_repository_storage_move.rb | 6 +- app/models/terraform/state.rb | 9 ++ app/models/user.rb | 19 +++- app/models/user_preference.rb | 2 - app/models/wiki.rb | 1 + 92 files changed, 1075 insertions(+), 450 deletions(-) create mode 100644 app/models/audit_event_archived.rb delete mode 100644 app/models/audit_event_partitioned.rb create mode 100644 app/models/ci/commit_with_pipeline.rb create mode 100644 app/models/concerns/boards/listable.rb create mode 100644 app/models/concerns/enums/ci/commit_status.rb delete mode 100644 app/models/concerns/enums/commit_status.rb create mode 100644 app/models/concerns/enums/vulnerability.rb create mode 100644 app/models/concerns/packages/debian/architecture.rb create mode 100644 app/models/concerns/packages/debian/distribution.rb create mode 100644 app/models/concerns/repositories/can_housekeep_repository.rb delete mode 100644 app/models/cycle_analytics/level_base.rb create mode 100644 app/models/cycle_analytics/project_level_stage_adapter.rb create mode 100644 app/models/namespace/package_setting.rb delete mode 100644 app/models/namespace_onboarding_action.rb create mode 100644 app/models/onboarding_progress.rb create mode 100644 app/models/packages/debian.rb create mode 100644 app/models/packages/debian/file_metadatum.rb create mode 100644 app/models/packages/debian/group_architecture.rb create mode 100644 app/models/packages/debian/group_distribution.rb create mode 100644 app/models/packages/debian/project_architecture.rb create mode 100644 app/models/packages/debian/project_distribution.rb delete mode 100644 app/models/project_services/alerts_service_data.rb (limited to 'app/models') diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index 0c916c576cb..2d9a2d7031c 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -52,6 +52,10 @@ module AlertManagement endpoint_identifier == LEGACY_IDENTIFIER end + def token_changed? + attribute_changed?(:token) + end + # Blank token assignment triggers token reset def prevent_token_assignment if token.present? && token_changed? diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 71235ed1002..44d1b6cf907 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -77,4 +77,9 @@ class ApplicationRecord < ActiveRecord::Base def self.where_exists(query) where('EXISTS (?)', query.select(1)) end + + def self.declarative_enum(enum_mod) + values = enum_mod.definition.transform_values { |v| v[:value] } + enum(enum_mod.key => values) + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9b9db7f93fd..5655ea4d4bf 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -171,7 +171,7 @@ class ApplicationSetting < ApplicationRecord validates :default_artifacts_expire_in, presence: true, duration: true validates :container_expiration_policies_enable_historic_entries, - inclusion: { in: [true, false], message: 'must be a boolean value' } + inclusion: { in: [true, false], message: _('must be a boolean value') } validates :container_registry_token_expire_delay, presence: true, @@ -303,9 +303,15 @@ class ApplicationSetting < ApplicationRecord validates :container_registry_delete_tags_service_timeout, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :container_registry_cleanup_tags_service_max_list_size, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :container_registry_expiration_policies_worker_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :invisible_captcha_enabled, + inclusion: { in: [true, false], message: _('must be a boolean value') } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -400,6 +406,42 @@ class ApplicationSetting < ApplicationRecord validates :ci_jwt_signing_key, rsa_key: true, allow_nil: true + validates :rate_limiting_response_text, + length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') }, + allow_blank: true + + validates :throttle_unauthenticated_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_unauthenticated_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_api_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_api_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_web_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_web_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_protected_paths_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_protected_paths_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, @@ -430,7 +472,7 @@ class ApplicationSetting < ApplicationRecord attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm validates :disable_feed_token, - inclusion: { in: [true, false], message: 'must be a boolean value' } + inclusion: { in: [true, false], message: _('must be a boolean value') } before_validation :ensure_uuid! diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 105889a364a..b05355f14b4 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -91,12 +91,13 @@ module ApplicationSettingImplementation housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, import_sources: Settings.gitlab['import_sources'], + invisible_captcha_enabled: false, issues_create_limit: 300, local_markdown_version: 0, login_recaptcha_protection_enabled: false, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], - max_import_size: 50, + max_import_size: 0, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, mirror_available: true, notify_on_unknown_sign_in: true, @@ -172,7 +173,8 @@ module ApplicationSettingImplementation container_registry_delete_tags_service_timeout: 250, container_registry_expiration_policies_worker_capacity: 0, kroki_enabled: false, - kroki_url: nil + kroki_url: nil, + rate_limiting_response_text: nil } end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index a4d991b040c..d1c0bb11dc8 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -4,6 +4,7 @@ class AuditEvent < ApplicationRecord include CreatedAtFilterable include BulkInsertSafe include EachBatch + include PartitionedTable PARALLEL_PERSISTENCE_COLUMNS = [ :author_name, @@ -15,6 +16,8 @@ class AuditEvent < ApplicationRecord self.primary_key = :id + partitioned_by :created_at, strategy: :monthly + serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize belongs_to :user, foreign_key: :author_id diff --git a/app/models/audit_event_archived.rb b/app/models/audit_event_archived.rb new file mode 100644 index 00000000000..3119f56fbcc --- /dev/null +++ b/app/models/audit_event_archived.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# This model is not intended to be used. +# It is a temporary reference to the pre-partitioned +# audit_events table. +# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206 +# for details. +class AuditEventArchived < ApplicationRecord + self.table_name = 'audit_events_archived' +end diff --git a/app/models/audit_event_partitioned.rb b/app/models/audit_event_partitioned.rb deleted file mode 100644 index 672daebd14a..00000000000 --- a/app/models/audit_event_partitioned.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -# This model is not yet intended to be used. -# It is in a transitioning phase while we are partitioning -# the table on the database-side. -# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206 -# for details. -class AuditEventPartitioned < ApplicationRecord - include PartitionedTable - - self.table_name = 'audit_events_part_5fc467ac26' - - partitioned_by :created_at, strategy: :monthly -end diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb index 9d191e6ae4d..1e822629ba1 100644 --- a/app/models/authentication_event.rb +++ b/app/models/authentication_event.rb @@ -3,10 +3,10 @@ class AuthenticationEvent < ApplicationRecord include UsageStatistics - TWO_FACTOR = 'two-factor'.freeze - TWO_FACTOR_U2F = 'two-factor-via-u2f-device'.freeze - TWO_FACTOR_WEBAUTHN = 'two-factor-via-webauthn-device'.freeze - STANDARD = 'standard'.freeze + TWO_FACTOR = 'two-factor' + TWO_FACTOR_U2F = 'two-factor-via-u2f-device' + TWO_FACTOR_WEBAUTHN = 'two-factor-via-webauthn-device' + STANDARD = 'standard' STATIC_PROVIDERS = [TWO_FACTOR, TWO_FACTOR_U2F, TWO_FACTOR_WEBAUTHN, STANDARD].freeze belongs_to :user, optional: true diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 11228e620c9..e255b6d15d2 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -15,7 +15,9 @@ module BlobViewer prepare! - @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, opts) + @validation_message = Gitlab::Ci::Lint + .new(project: opts[:project], current_user: opts[:user], sha: opts[:sha]) + .validate(blob.data).errors.first end def valid?(opts) diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 19a0d424e33..ef3891908f7 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -7,7 +7,6 @@ module Ci include Importable include AfterCommitQueue include Ci::HasRef - extend ::Gitlab::Utils::Override InvalidBridgeTypeError = Class.new(StandardError) InvalidTransitionError = Class.new(StandardError) @@ -200,13 +199,6 @@ module Ci end end - override :dependency_variables - def dependency_variables - return [] unless ::Feature.enabled?(:ci_bridge_dependency_variables, project, default_enabled: true) - - super - end - def target_revision_ref downstream_pipeline_params.dig(:target_revision, :ref) end @@ -218,7 +210,8 @@ module Ci project: downstream_project, source: :pipeline, target_revision: { - ref: target_ref || downstream_project.default_branch + ref: target_ref || downstream_project.default_branch, + variables_attributes: downstream_variables }, execute_params: { ignore_skip_ci: true, @@ -238,7 +231,8 @@ module Ci checkout_sha: parent_pipeline.sha, before: parent_pipeline.before_sha, source_sha: parent_pipeline.source_sha, - target_sha: parent_pipeline.target_sha + target_sha: parent_pipeline.target_sha, + variables_attributes: downstream_variables }, execute_params: { ignore_skip_ci: true, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 71939f070cb..5e3f42d7c2c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -27,7 +27,8 @@ module Ci upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }, refspecs: -> (build) { build.merge_request_ref? }, artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }, - multi_build_steps: -> (build) { build.multi_build_steps? } + multi_build_steps: -> (build) { build.multi_build_steps? }, + return_exit_code: -> (build) { build.exit_codes_defined? } }.freeze DEFAULT_RETRIES = { @@ -146,6 +147,12 @@ module Ci .includes(:metadata, :job_artifacts_metadata) end + scope :with_project_and_metadata, -> do + if Feature.enabled?(:non_public_artifacts, type: :development) + joins(:metadata).includes(:project, :metadata) + end + end + scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) } scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) } scope :last_month, -> { where('created_at > ?', Date.today - 1.month) } @@ -382,12 +389,8 @@ module Ci end after_transition any => [:skipped, :canceled] do |build, transition| - if Feature.enabled?(:cd_skipped_deployment_status, build.project) - if transition.to_name == :skipped - build.deployment&.skip - else - build.deployment&.cancel - end + if transition.to_name == :skipped + build.deployment&.skip else build.deployment&.cancel end @@ -741,6 +744,16 @@ module Ci artifacts_metadata? end + def artifacts_public? + return true unless Feature.enabled?(:non_public_artifacts, type: :development) + + artifacts_public = options.dig(:artifacts, :public) + + return true if artifacts_public.nil? # Default artifacts:public to true + + options.dig(:artifacts, :public) + end + def artifacts_metadata_entry(path, **options) artifacts_metadata.open do |metadata_stream| metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( @@ -1007,14 +1020,23 @@ module Ci end def debug_mode? - return false unless Feature.enabled?(:restrict_access_to_build_debug_mode, default_enabled: true) - # TODO: Have `debug_mode?` check against data on sent back from runner # to capture all the ways that variables can be set. # See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955) variables.any? { |variable| variable[:key] == 'CI_DEBUG_TRACE' && variable[:value].casecmp('true') == 0 } end + def drop_with_exit_code!(failure_reason, exit_code) + transaction do + conditionally_allow_failure!(exit_code) + drop!(failure_reason) + end + end + + def exit_codes_defined? + options.dig(:allow_failure_criteria, :exit_codes).present? + end + protected def run_status_commit_hooks! @@ -1098,6 +1120,22 @@ module Ci Gitlab::ErrorTracking.track_exception(e) end 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) + update_columns(allow_failure: true) + end + end + + def allowed_to_fail_with_code?(exit_code) + options + .dig(:allow_failure_criteria, :exit_codes) + .to_a + .include?(exit_code) + end end end diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index b977a5f4419..fac615f97b9 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -6,7 +6,7 @@ module Ci include BulkInsertSafe - belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :needs + belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs validates :build, presence: true validates :name, presence: true, length: { maximum: 128 } diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index bc7f17f046c..b6196048ca1 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -7,8 +7,8 @@ module Ci extend Gitlab::Ci::Model TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' - DEFAULT_SERVICE_NAME = 'build'.freeze - DEFAULT_PORT_NAME = 'default_port'.freeze + DEFAULT_SERVICE_NAME = 'build' + DEFAULT_PORT_NAME = 'default_port' self.table_name = 'ci_builds_runner_session' diff --git a/app/models/ci/commit_with_pipeline.rb b/app/models/ci/commit_with_pipeline.rb new file mode 100644 index 00000000000..7f952fb77a0 --- /dev/null +++ b/app/models/ci/commit_with_pipeline.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Ci::CommitWithPipeline < SimpleDelegator + include Presentable + + def initialize(commit) + @latest_pipelines = {} + super(commit) + end + + def pipelines + project.ci_pipelines.where(sha: sha) + end + + def last_pipeline + strong_memoize(:last_pipeline) do + pipelines.last + end + end + + def latest_pipeline(ref = nil) + @latest_pipelines.fetch(ref) do |ref| + @latest_pipelines[ref] = latest_pipeline_for_project(ref, project) + end + end + + def latest_pipeline_for_project(ref, pipeline_project) + pipeline_project.ci_pipelines.latest_pipeline_per_commit(id, ref)[id] + end + + def set_latest_pipeline_for_ref(ref, pipeline) + @latest_pipelines[ref] = pipeline + end + + def status(ref = nil) + latest_pipeline(ref)&.status + end +end diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index f0c035635b9..c7c0ec61e62 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -24,9 +24,22 @@ module Ci def status strong_memoize(:status) do + status_struct.status + end + end + + def success? + status.to_s == 'success' + end + + def has_warnings? + status_struct.warnings? + end + + def status_struct + strong_memoize(:status_struct) do Gitlab::Ci::Status::Composite .new(@jobs) - .status end end @@ -39,8 +52,13 @@ module Ci end end - def self.fabricate(project, stage) - stage.latest_statuses + # Construct a grouping of statuses for this stage. + # We allow the caller to pass in statuses for efficiency (avoiding N+1 + # queries). + def self.fabricate(project, stage, statuses = nil) + statuses ||= stage.latest_statuses + + statuses .sort_by(&:sortable_name).group_by(&:group_name) .map do |group_name, grouped_statuses| self.new(project, stage, name: group_name, jobs: grouped_statuses) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index c80d50ea131..f13be3b3c86 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -9,6 +9,7 @@ module Ci include Sortable include Artifactable include FileStoreMounter + include EachBatch extend Gitlab::Ci::Model TEST_REPORT_FILE_TYPES = %w[junit].freeze @@ -133,6 +134,12 @@ module Ci scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) } + scope :with_job, -> do + if Feature.enabled?(:non_public_artifacts, type: :development) + joins(:job).includes(:job) + end + end + scope :with_file_types, -> (file_types) do types = self.file_types.select { |file_type| file_types.include?(file_type) }.values @@ -170,7 +177,8 @@ module Ci end scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) } - scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked).order(expire_at: :desc) } + scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) } + scope :order_expired_desc, -> { order(expire_at: :desc) } scope :with_destroy_preloads, -> { includes(project: [:route, :statistics]) } scope :scoped_project, -> { where('ci_job_artifacts.project_id = projects.id') } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5e5f51d776f..4a579892e3f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -68,8 +68,8 @@ module Ci has_many :variables, class_name: 'Ci::PipelineVariable' has_many :deployments, through: :builds has_many :environments, -> { distinct }, through: :deployments - has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' - has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts + has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :downloadable_artifacts, -> { not_expired.downloadable.with_job }, through: :latest_builds, source: :job_artifacts has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline @@ -249,7 +249,7 @@ module Ci after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| pipeline.run_after_commit do - ::Ci::Pipelines::CreateArtifactWorker.perform_async(pipeline.id) + ::Ci::PipelineArtifacts::CoverageReportWorker.perform_async(pipeline.id) end end diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index 6e9b8416c10..713a0bf9c45 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -33,6 +33,9 @@ module Ci state :still_failing, value: 5 after_transition any => [:fixed, :success] do |ci_ref| + # Do not try to unlock if no artifacts are locked + next unless ci_ref.artifacts_locked? + ci_ref.run_after_commit do Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id) end @@ -54,6 +57,10 @@ module Ci Ci::Pipeline.last_finished_for_ref_id(self.id)&.id end + def artifacts_locked? + self.pipelines.where(locked: :artifacts_locked).exists? + end + def update_status_by!(pipeline) retry_lock(self) do next unless last_finished_pipeline_id == pipeline.id diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index d32fff14590..8560826928a 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -65,7 +65,7 @@ module Clusters end def retry_command(command) - "for i in $(seq 1 90); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)" + Gitlab::Kubernetes::PodCmd.retry_command(command, times: 90) end def post_delete_script diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index b1c3116d77c..7c131e031c1 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Knative < ApplicationRecord - VERSION = '0.9.0' + VERSION = '0.10.0' REPOSITORY = 'https://charts.gitlab.io' METRICS_CONFIG = 'https://gitlab.com/gitlab-org/charts/knative/-/raw/v0.9.0/vendor/istio-metrics.yml' FETCH_IP_ADDRESS_DELAY = 30.seconds diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 1e41b6f4f31..56acac53e0b 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.23.0' + VERSION = '0.24.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 84de5828491..e3dcd5b0d07 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -247,7 +247,7 @@ module Clusters def prevent_modification return if provided_by_user? - if api_url_changed? || token_changed? || ca_pem_changed? + if api_url_changed? || attribute_changed?(:token) || ca_pem_changed? errors.add(:base, _('Cannot modify managed Kubernetes cluster')) return false end diff --git a/app/models/commit.rb b/app/models/commit.rb index 80dd02981c1..edce9ad293e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -37,7 +37,7 @@ class Commit cache_markdown_field :title, pipeline: :single_line cache_markdown_field :full_title, pipeline: :single_line - cache_markdown_field :description, pipeline: :commit_description + cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte class << self def decorate(commits, container) @@ -80,7 +80,7 @@ class Commit def diff_hard_limit_files(project: nil) if Feature.enabled?(:increased_diff_limits, project) - 2000 + 3000 else 1000 end @@ -88,7 +88,7 @@ class Commit def diff_hard_limit_lines(project: nil) if Feature.enabled?(:increased_diff_limits, project) - 75000 + 100000 else 50000 end @@ -148,7 +148,7 @@ class Commit to: :with_pipeline def with_pipeline - @with_pipeline ||= CommitWithPipeline.new(self) + @with_pipeline ||= Ci::CommitWithPipeline.new(self) end def id diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ee9c2501bfc..a399ffc32de 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -80,9 +80,9 @@ class CommitStatus < ApplicationRecord merge(or_conditions) end - # We use `Enums::CommitStatus.failure_reasons` here so that EE can more easily + # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily # extend this `Hash` with new values. - enum_with_nil failure_reason: Enums::CommitStatus.failure_reasons + enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons ## # We still create some CommitStatuses outside of CreatePipelineService. @@ -159,6 +159,12 @@ class CommitStatus < ApplicationRecord commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason] end + before_transition [:skipped, :manual] => :created do |commit_status, transition| + transition.args.first.try do |user| + commit_status.user = user + end + end + after_transition do |commit_status, transition| next if transition.loopback? next if commit_status.processed? @@ -203,7 +209,7 @@ class CommitStatus < ApplicationRecord def group_name # 'rspec:linux: 1/10' => 'rspec:linux' - common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '') + 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}, '') diff --git a/app/models/commit_with_pipeline.rb b/app/models/commit_with_pipeline.rb index f382ae8f55a..7f952fb77a0 100644 --- a/app/models/commit_with_pipeline.rb +++ b/app/models/commit_with_pipeline.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class CommitWithPipeline < SimpleDelegator +class Ci::CommitWithPipeline < SimpleDelegator include Presentable def initialize(commit) diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb new file mode 100644 index 00000000000..b7c0a8b3489 --- /dev/null +++ b/app/models/concerns/boards/listable.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Boards + module Listable + extend ActiveSupport::Concern + + included do + validates :label, :position, presence: true, if: :label? + validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable? + + before_destroy :can_be_destroyed + + scope :ordered, -> { order(:list_type, :position) } + scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } + scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } + end + + class_methods do + def destroyable_types + [:label] + end + + def movable_types + [:label] + end + end + + def destroyable? + self.class.destroyable_types.include?(list_type&.to_sym) + end + + def movable? + self.class.movable_types.include?(list_type&.to_sym) + end + + def title + if label? + label.name + elsif backlog? + _('Open') + else + list_type.humanize + end + end + + private + + def can_be_destroyed + throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow + end + end +end diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb index f9eb3fb875e..3748e77e933 100644 --- a/app/models/concerns/bulk_insert_safe.rb +++ b/app/models/concerns/bulk_insert_safe.rb @@ -53,9 +53,9 @@ module BulkInsertSafe class_methods do def set_callback(name, *args) unless _bulk_insert_callback_allowed?(name, args) - raise MethodNotAllowedError.new( + raise MethodNotAllowedError, "Not allowed to call `set_callback(#{name}, #{args})` when model extends `BulkInsertSafe`." \ - "Callbacks that fire per each record being inserted do not work with bulk-inserts.") + "Callbacks that fire per each record being inserted do not work with bulk-inserts." end super diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index 24df86dbc3c..cbe7d3b6abb 100644 --- a/app/models/concerns/ci/artifactable.rb +++ b/app/models/concerns/ci/artifactable.rb @@ -18,7 +18,8 @@ module Ci gzip: 3 }, _suffix: true - scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) } + scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) } + scope :expired, -> (limit) { expired_before(Time.current).limit(limit) } end def each_blob(&blk) diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index af5f4e30d06..a59f00d73ec 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -47,7 +47,7 @@ module EachBatch # order_hint does not affect the search results. For example, # `ORDER BY id ASC, updated_at ASC` means the same thing as `ORDER # BY id ASC`. - def each_batch(of: 1000, column: primary_key, order_hint: nil) + def each_batch(of: 1000, column: primary_key, order: :asc, order_hint: nil) unless column raise ArgumentError, 'the column: argument must be set to a column name to use for ordering rows' @@ -55,7 +55,7 @@ module EachBatch start = except(:select) .select(column) - .reorder(column => :asc) + .reorder(column => order) start = start.order(order_hint) if order_hint start = start.take @@ -66,10 +66,12 @@ module EachBatch arel_table = self.arel_table 1.step do |index| + start_cond = arel_table[column].gteq(start_id) + start_cond = arel_table[column].lteq(start_id) if order == :desc stop = except(:select) .select(column) - .where(arel_table[column].gteq(start_id)) - .reorder(column => :asc) + .where(start_cond) + .reorder(column => order) stop = stop.order(order_hint) if order_hint stop = stop @@ -77,12 +79,14 @@ module EachBatch .limit(1) .take - relation = where(arel_table[column].gteq(start_id)) + relation = where(start_cond) if stop stop_id = stop[column] start_id = stop_id - relation = relation.where(arel_table[column].lt(stop_id)) + stop_cond = arel_table[column].lt(stop_id) + stop_cond = arel_table[column].gt(stop_id) if order == :desc + relation = relation.where(stop_cond) end # Any ORDER BYs are useless for this relation and can lead to less diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb new file mode 100644 index 00000000000..48b4a402974 --- /dev/null +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +module Enums + module Ci + module CommitStatus + # Returns the Hash to use for creating the `failure_reason` enum for + # `CommitStatus`. + def self.failure_reasons + { + unknown_failure: nil, + script_failure: 1, + api_failure: 2, + stuck_or_timeout_failure: 3, + runner_system_failure: 4, + missing_dependency_failure: 5, + runner_unsupported: 6, + stale_schedule: 7, + job_execution_timeout: 8, + archived_failure: 9, + unmet_prerequisites: 10, + scheduler_failure: 11, + data_integrity_failure: 12, + forward_deployment_failure: 13, + insufficient_bridge_permissions: 1_001, + downstream_bridge_project_not_found: 1_002, + invalid_bridge_trigger: 1_003, + bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data + downstream_pipeline_creation_failed: 1_007, + secrets_provider_not_found: 1_008, + reached_max_descendant_pipelines_depth: 1_009 + } + end + end + end +end + +Enums::Ci::CommitStatus.prepend_if_ee('EE::Enums::Ci::CommitStatus') diff --git a/app/models/concerns/enums/commit_status.rb b/app/models/concerns/enums/commit_status.rb deleted file mode 100644 index faeed7276ab..00000000000 --- a/app/models/concerns/enums/commit_status.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Enums - module CommitStatus - # Returns the Hash to use for creating the `failure_reason` enum for - # `CommitStatus`. - def self.failure_reasons - { - unknown_failure: nil, - script_failure: 1, - api_failure: 2, - stuck_or_timeout_failure: 3, - runner_system_failure: 4, - missing_dependency_failure: 5, - runner_unsupported: 6, - stale_schedule: 7, - job_execution_timeout: 8, - archived_failure: 9, - unmet_prerequisites: 10, - scheduler_failure: 11, - data_integrity_failure: 12, - forward_deployment_failure: 13, - insufficient_bridge_permissions: 1_001, - downstream_bridge_project_not_found: 1_002, - invalid_bridge_trigger: 1_003, - bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data - downstream_pipeline_creation_failed: 1_007, - secrets_provider_not_found: 1_008, - reached_max_descendant_pipelines_depth: 1_009 - } - end - end -end - -Enums::CommitStatus.prepend_if_ee('EE::Enums::CommitStatus') diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb new file mode 100644 index 00000000000..4b2e9e9e0b2 --- /dev/null +++ b/app/models/concerns/enums/vulnerability.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Enums + module Vulnerability + CONFIDENCE_LEVELS = { + # undefined: 0, no longer applicable + ignore: 1, + unknown: 2, + experimental: 3, + low: 4, + medium: 5, + high: 6, + confirmed: 7 + }.with_indifferent_access.freeze + + REPORT_TYPES = { + sast: 0, + secret_detection: 4 + }.with_indifferent_access.freeze + + SEVERITY_LEVELS = { + # undefined: 0, no longer applicable + info: 1, + unknown: 2, + # experimental: 3, formerly used by confidence, no longer applicable + low: 4, + medium: 5, + high: 6, + critical: 7 + }.with_indifferent_access.freeze + + def self.confidence_levels + CONFIDENCE_LEVELS + end + + def self.report_types + REPORT_TYPES + end + + def self.severity_levels + SEVERITY_LEVELS + end + end +end + +Enums::Vulnerability.prepend_if_ee('EE::Enums::Vulnerability') diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index c3a394c1ca5..83ff5b16efe 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -196,6 +196,10 @@ module Issuable is_a?(Issue) end + def supports_assignee? + false + end + def severity return IssuableSeverity::DEFAULT unless supports_severity? @@ -216,6 +220,10 @@ module Issuable end class_methods do + def participant_includes + [:assignees, :author, { notes: [:author, :award_emoji] }] + end + # Searches for records with a matching title. # # This method uses ILIKE on PostgreSQL. @@ -344,12 +352,15 @@ module Issuable # # Returns an array of arel columns def grouping_columns(sort) + sort = sort.to_s grouping_columns = [arel_table[:id]] if %w(milestone_due_desc milestone_due_asc milestone).include?(sort) milestone_table = Milestone.arel_table grouping_columns << milestone_table[:id] grouping_columns << milestone_table[:due_date] + elsif %w(merged_at_desc merged_at_asc).include?(sort) + grouping_columns << MergeRequest::Metrics.arel_table[:merged_at] end grouping_columns diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb index 886db133a94..a5ffa959174 100644 --- a/app/models/concerns/issue_available_features.rb +++ b/app/models/concerns/issue_available_features.rb @@ -9,7 +9,10 @@ module IssueAvailableFeatures class_methods do # EE only features are listed on EE::IssueAvailableFeatures def available_features_for_issue_types - {}.with_indifferent_access + { + assignee: %w(issue incident), + confidentiality: %(issue incident) + }.with_indifferent_access end end diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index b1698bc2ee3..ccb334343ff 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -51,7 +51,7 @@ module Milestoneable # Overridden on EE module # def supports_milestone? - respond_to?(:milestone_id) && !incident? + respond_to?(:milestone_id) end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 2dbe9360d42..f3cc68e4b85 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -19,6 +19,11 @@ module Noteable def resolvable_types %w(MergeRequest DesignManagement::Design) end + + # `Noteable` class names that support creating/forwarding individual notes. + def email_creatable_types + %w(Issue) + end end # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via @@ -55,6 +60,10 @@ module Noteable supports_discussions? && self.class.replyable_types.include?(base_class_name) end + def supports_creating_notes_by_email? + self.class.email_creatable_types.include?(base_class_name) + end + def supports_suggestion? false end @@ -158,6 +167,18 @@ module Noteable def after_note_destroyed(_note) # no-op end + + # Email address that an authorized user can send/forward an email to be added directly + # to an issue or merge request. + # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue-34@localhost.com + def creatable_note_email_address(author) + return unless supports_creating_notes_by_email? + + project_email = project.new_issuable_address(author, self.class.name.underscore) + return unless project_email + + project_email.sub('@', "-#{iid}@") + end end Noteable.extend(Noteable::ClassMethods) diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb new file mode 100644 index 00000000000..4aa633e0357 --- /dev/null +++ b/app/models/concerns/packages/debian/architecture.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Packages + module Debian + module Architecture + extend ActiveSupport::Concern + + included do + belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures + + validates :distribution, + presence: true + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { scope: %i[distribution_id] }, + format: { with: Gitlab::Regex.debian_architecture_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/distribution.rb b/app/models/concerns/packages/debian/distribution.rb new file mode 100644 index 00000000000..285d293c9ee --- /dev/null +++ b/app/models/concerns/packages/debian/distribution.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +module Packages + module Debian + module Distribution + extend ActiveSupport::Concern + + included do + include FileStoreMounter + + def self.container_foreign_key + "#{container_type}_id".to_sym + end + + alias_attribute :container, container_type + alias_attribute :container_id, "#{container_type}_id" + + belongs_to container_type + belongs_to :creator, class_name: 'User' + + has_many :architectures, + class_name: "Packages::Debian::#{container_type.capitalize}Architecture", + foreign_key: :distribution_id, + inverse_of: :distribution + + validates :codename, + presence: true, + uniqueness: { scope: [container_foreign_key] }, + format: { with: Gitlab::Regex.debian_distribution_regex } + + validates :suite, + allow_nil: true, + format: { with: Gitlab::Regex.debian_distribution_regex } + validates :suite, + uniqueness: { scope: [container_foreign_key] }, + if: :suite + + validate :unique_codename_and_suite + + validates :origin, + allow_nil: true, + format: { with: Gitlab::Regex.debian_distribution_regex } + + validates :label, + allow_nil: true, + format: { with: Gitlab::Regex.debian_distribution_regex } + + validates :version, + allow_nil: true, + format: { with: Gitlab::Regex.debian_version_regex } + + # The Valid-Until field is a security measure to prevent malicious attackers to + # serve an outdated repository, with vulnerable packages + # (keeping in mind that most Debian repository are not using TLS but use GPG + # signatures instead). + # A minimum of 24 hours is simply to avoid generating indices too often + # (which generates load). + # Official Debian repositories are generated 4 times a day, and valid for 7 days. + # Full ref: https://wiki.debian.org/DebianRepository/Format#Date.2C_Valid-Until + validates :valid_time_duration_seconds, + allow_nil: true, + numericality: { greater_than_or_equal_to: 24.hours.to_i } + + validates container_type, presence: true + validates :file_store, presence: true + + validates :file_signature, absence: true + validates :signing_keys, absence: true + + scope :with_container, ->(subject) { where(container_type => subject) } + scope :with_codename, ->(codename) { where(codename: codename) } + scope :with_suite, ->(suite) { where(suite: suite) } + scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) } + + attr_encrypted :signing_keys, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm', + encode: false, + encode_iv: false + + mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader + + def needs_update? + !file.exists? || time_duration_expired? + end + + private + + def time_duration_expired? + return false unless valid_time_duration_seconds.present? + + updated_at + valid_time_duration_seconds.seconds + 6.hours < Time.current + end + + def unique_codename_and_suite + errors.add(:codename, _('has already been taken as Suite')) if codename_exists_as_suite? + errors.add(:suite, _('has already been taken as Codename')) if suite_exists_as_codename? + end + + def codename_exists_as_suite? + return false unless codename.present? + + self.class.with_container(container).with_suite(codename).exists? + end + + def suite_exists_as_codename? + return false unless suite.present? + + self.class.with_container(container).with_codename(suite).exists? + end + end + end + end +end diff --git a/app/models/concerns/repositories/can_housekeep_repository.rb b/app/models/concerns/repositories/can_housekeep_repository.rb new file mode 100644 index 00000000000..2b79851a07c --- /dev/null +++ b/app/models/concerns/repositories/can_housekeep_repository.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Repositories + module CanHousekeepRepository + extend ActiveSupport::Concern + + def pushes_since_gc + Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } + end + + def increment_pushes_since_gc + Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) } + end + + def reset_pushes_since_gc + Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) } + end + + private + + def pushes_since_gc_redis_shared_state_key + "#{self.class.name.underscore.pluralize}/#{id}/pushes_since_gc" + end + end +end diff --git a/app/models/cycle_analytics/level_base.rb b/app/models/cycle_analytics/level_base.rb deleted file mode 100644 index 901636a7263..00000000000 --- a/app/models/cycle_analytics/level_base.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module CycleAnalytics - module LevelBase - STAGES = %i[issue plan code test review staging].freeze - - # This is a temporary adapter class which makes the new value stream (cycle analytics) - # backend compatible with the old implementation. - class StageAdapter - def initialize(stage, options) - @stage = stage - @options = options - end - - # rubocop: disable CodeReuse/Presenter - def as_json(serializer: AnalyticsStageSerializer) - presenter = Analytics::CycleAnalytics::StagePresenter.new(stage) - - serializer.new.represent(OpenStruct.new( - title: presenter.title, - description: presenter.description, - legend: presenter.legend, - name: stage.name, - project_median: median, - group_median: median - )) - end - # rubocop: enable CodeReuse/Presenter - - def events - data_collector.records_fetcher.serialized_records - end - - def median - data_collector.median.seconds - end - - alias_method :project_median, :median - alias_method :group_median, :median - - private - - attr_reader :stage, :options - - def data_collector - @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options) - end - end - - def all_medians_by_stage - STAGES.each_with_object({}) do |stage_name, medians_per_stage| - medians_per_stage[stage_name] = self[stage_name].median - end - end - - def stats - @stats ||= STAGES.map do |stage_name| - self[stage_name].as_json - end - end - - def [](stage_name) - if Feature.enabled?(:new_project_level_vsa_backend, resource_parent, default_enabled: true) - StageAdapter.new(build_stage(stage_name), options) - else - Gitlab::CycleAnalytics::Stage[stage_name].new(options: options) - end - end - - def stage_params_by_name(name) - Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name) - end - end -end diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb index 26cdcc0db4b..5bd07b3f6c3 100644 --- a/app/models/cycle_analytics/project_level.rb +++ b/app/models/cycle_analytics/project_level.rb @@ -2,7 +2,6 @@ module CycleAnalytics class ProjectLevel - include LevelBase attr_reader :project, :options def initialize(project, options:) @@ -21,13 +20,29 @@ module CycleAnalytics Gitlab::CycleAnalytics::Permissions.get(user: user, project: project) end + def stats + @stats ||= default_stage_names.map do |stage_name| + self[stage_name].as_json + end + end + + def [](stage_name) + CycleAnalytics::ProjectLevelStageAdapter.new(build_stage(stage_name), options) + end + + private + def build_stage(stage_name) stage_params = stage_params_by_name(stage_name).merge(project: project) Analytics::CycleAnalytics::ProjectStage.new(stage_params) end - def resource_parent - project + def stage_params_by_name(name) + Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name) + end + + def default_stage_names + Gitlab::Analytics::CycleAnalytics::DefaultStages.symbolized_stage_names end end end diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb new file mode 100644 index 00000000000..dd4afa9b809 --- /dev/null +++ b/app/models/cycle_analytics/project_level_stage_adapter.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# This adapter class makes the new value stream (cycle analytics) backend +# compatible with the old value stream controller actions. +module CycleAnalytics + class ProjectLevelStageAdapter + def initialize(stage, options) + @stage = stage + @options = options + end + + # rubocop: disable CodeReuse/Presenter + def as_json(serializer: AnalyticsStageSerializer) + presenter = Analytics::CycleAnalytics::StagePresenter.new(stage) + + serializer.new.represent(OpenStruct.new( + title: presenter.title, + description: presenter.description, + legend: presenter.legend, + name: stage.name, + project_median: median + )) + end + # rubocop: enable CodeReuse/Presenter + + def events + data_collector.records_fetcher.serialized_records + end + + def median + data_collector.median.seconds + end + + alias_method :project_median, :median + + private + + attr_reader :stage, :options + + def data_collector + @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options) + end + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index b93b714ec8b..6f40466394a 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -109,6 +109,23 @@ class Deployment < ApplicationRecord Deployments::ExecuteHooksWorker.perform_async(id) end end + + 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) + end + end + 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 end enum status: { diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 944a64f5419..c8a0773cc5b 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -111,6 +111,10 @@ class DiffNote < Note super.merge(suggestions_filter_enabled: true) end + def multiline? + position&.multiline? + end + private def enqueue_diff_file_creation_job diff --git a/app/models/experiment.rb b/app/models/experiment.rb index a4cacab25ee..7dbc95f617a 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -16,7 +16,9 @@ class Experiment < ApplicationRecord # Create or update the recorded experiment_user row for the user in this experiment. def record_user_and_group(user, group_type, context = {}) - experiment_users.find_or_initialize_by(user: user).update!(group_type: group_type, context: context) + experiment_user = experiment_users.find_or_initialize_by(user: user) + merged_context = experiment_user.context.deep_merge(context.deep_stringify_keys) + experiment_user.update!(group_type: group_type, context: merged_context) end def record_conversion_event_for_user(user) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 995baf8565c..ca6857a14b6 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -127,3 +127,5 @@ class GpgKey < ApplicationRecord end end end + +GpgKey.prepend_if_ee('EE::GpgKey') diff --git a/app/models/group.rb b/app/models/group.rb index 739135e82dd..903d0154969 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -75,6 +75,9 @@ 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 + has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + accepts_nested_attributes_for :variables, allow_destroy: true validate :visibility_level_allowed_by_projects diff --git a/app/models/issue.rb b/app/models/issue.rb index 253f4465cd9..5da9f67f6ef 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -434,6 +434,10 @@ class Issue < ApplicationRecord moved_to || duplicated_to end + def supports_assignee? + issue_type_supports?(:assignee) + end + private def ensure_metrics diff --git a/app/models/list.rb b/app/models/list.rb index 1df565c83e6..49834af3dfb 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class List < ApplicationRecord + include Boards::Listable include Importable belongs_to :board @@ -10,30 +11,13 @@ class List < ApplicationRecord enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4, iteration: 5 } validates :board, :list_type, presence: true, unless: :importing? - validates :label, :position, presence: true, if: :label? validates :label_id, uniqueness: { scope: :board_id }, if: :label? - validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable? - - before_destroy :can_be_destroyed - - scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } - scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } scope :preload_associated_models, -> { preload(:board, label: :priorities) } - scope :ordered, -> { order(:list_type, :position) } - alias_method :preferences, :list_user_preferences class << self - def destroyable_types - [:label] - end - - def movable_types - [:label] - end - def preload_preferences_for_user(lists, user) return unless user @@ -60,18 +44,6 @@ class List < ApplicationRecord preferences_for(user).update(preferences) end - def destroyable? - self.class.destroyable_types.include?(list_type&.to_sym) - end - - def movable? - self.class.movable_types.include?(list_type&.to_sym) - end - - def title - label? ? label.name : list_type.humanize - end - def collapsed?(user) preferences = preferences_for(user) @@ -95,12 +67,6 @@ class List < ApplicationRecord end end end - - private - - def can_be_destroyed - throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow - end end List.prepend_if_ee('::EE::List') diff --git a/app/models/member.rb b/app/models/member.rb index 687830f5267..2e79b50d6b7 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -13,6 +13,8 @@ class Member < ApplicationRecord include FromUnion include UpdateHighestRole + AVATAR_SIZE = 40 + attr_accessor :raw_invite_token belongs_to :created_by, class_name: "User" diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 2bbcdbbe5ce..c30f6dc81ee 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -62,7 +62,9 @@ class GroupMember < Member end def post_create_hook - run_after_commit_or_now { notification_service.new_group_member(self) } + if send_welcome_email? + run_after_commit_or_now { notification_service.new_group_member(self) } + end super end @@ -72,6 +74,10 @@ class GroupMember < Member run_after_commit { notification_service.update_group_member(self) } end + if saved_change_to_expires_at? + run_after_commit { notification_service.updated_group_member_expiration(self) } + end + super end @@ -87,6 +93,10 @@ class GroupMember < Member super end + + def send_welcome_email? + true + end end GroupMember.prepend_if_ee('EE::GroupMember') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 043f07cf9f3..64b8223a1f0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -261,6 +261,19 @@ class MergeRequest < ApplicationRecord scope :by_merge_commit_sha, -> (sha) do where(merge_commit_sha: sha) end + scope :by_squash_commit_sha, -> (sha) do + where(squash_commit_sha: sha) + end + scope :by_related_commit_sha, -> (sha) do + from_union( + [ + 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 joins(:notes).where(notes: { commit_id: sha }) end @@ -493,6 +506,10 @@ class MergeRequest < ApplicationRecord work_in_progress?(title) ? title : "Draft: #{title}" end + def self.participant_includes + [:reviewers, :award_emoji] + super + end + def committers @committers ||= commits.committers end @@ -1639,18 +1656,6 @@ class MergeRequest < ApplicationRecord !has_commits? end - def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil) - return false unless can_be_merged_by?(current_user) - - return true if autocomplete_precheck - - return false unless mergeable?(skip_ci_check: true) - return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?) - return false if last_diff_sha != diff_head_sha - - true - end - def pipeline_coverage_delta if base_pipeline&.coverage && head_pipeline&.coverage '%.2f' % (head_pipeline.coverage.to_f - base_pipeline.coverage.to_f) @@ -1762,6 +1767,10 @@ class MergeRequest < ApplicationRecord false end + def supports_assignee? + true + end + private def with_rebase_lock diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c244150e7a3..aa4ddfede99 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -33,6 +33,7 @@ class Milestone < ApplicationRecord scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) } + scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 238e8f70778..6f7b377ee52 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -28,7 +28,7 @@ class Namespace < ApplicationRecord has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace' has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' - has_many :namespace_onboarding_actions + has_one :onboarding_progress # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. @@ -40,6 +40,7 @@ class Namespace < ApplicationRecord has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics' has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule' + has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting' validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, @@ -160,6 +161,10 @@ class Namespace < ApplicationRecord end end + def package_settings + package_setting_relation || build_package_setting_relation + end + def default_branch_protection super || Gitlab::CurrentSettings.default_branch_protection end @@ -438,6 +443,10 @@ class Namespace < ApplicationRecord end end + def root? + !has_parent? + end + private def all_projects_with_pages diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb new file mode 100644 index 00000000000..a2064e020b3 --- /dev/null +++ b/app/models/namespace/package_setting.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Namespace::PackageSetting < ApplicationRecord + self.primary_key = :namespace_id + self.table_name = 'namespace_package_settings' + + PackageSettingNotImplemented = Class.new(StandardError) + + PACKAGES_WITH_SETTINGS = %w[maven].freeze + + belongs_to :namespace, inverse_of: :package_setting_relation + + validates :namespace, presence: true + validates :maven_duplicates_allowed, inclusion: { in: [true, false] } + validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } + + class << self + def duplicates_allowed?(package) + return true unless package + raise PackageSettingNotImplemented unless PACKAGES_WITH_SETTINGS.include?(package.package_type) + + duplicates_allowed = package.package_settings["#{package.package_type}_duplicates_allowed"] + regex = ::Gitlab::UntrustedRegexp.new("\\A#{package.package_settings["#{package.package_type}_duplicate_exception_regex"]}\\z") + + duplicates_allowed || regex.match?(package.name) + end + end +end diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb index a3df82998c4..90aeee7a4f1 100644 --- a/app/models/namespace/root_storage_statistics.rb +++ b/app/models/namespace/root_storage_statistics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Namespace::RootStorageStatistics < ApplicationRecord - SNIPPETS_SIZE_STAT_NAME = 'snippets_size'.freeze + SNIPPETS_SIZE_STAT_NAME = 'snippets_size' STATISTICS_ATTRIBUTES = %W( storage_size repository_size diff --git a/app/models/namespace_onboarding_action.rb b/app/models/namespace_onboarding_action.rb deleted file mode 100644 index 43dd872673c..00000000000 --- a/app/models/namespace_onboarding_action.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class NamespaceOnboardingAction < ApplicationRecord - belongs_to :namespace, optional: false - - validates :action, presence: true - - ACTIONS = { - subscription_created: 1, - git_write: 2, - merge_request_created: 3, - git_read: 4, - user_added: 6 - }.freeze - - enum action: ACTIONS - - class << self - def completed?(namespace, action) - where(namespace: namespace, action: action).exists? - end - - def create_action(namespace, action) - NamespaceOnboardingAction.safe_find_or_create_by(namespace: namespace, action: action) - end - end -end diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb new file mode 100644 index 00000000000..419bbd595e9 --- /dev/null +++ b/app/models/onboarding_progress.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class OnboardingProgress < ApplicationRecord + belongs_to :namespace, optional: false + + validate :namespace_is_root_namespace + + ACTIONS = [ + :git_pull, + :git_write, + :merge_request_created, + :pipeline_created, + :user_added, + :trial_started, + :subscription_created, + :required_mr_approvals_enabled, + :code_owners_enabled, + :scoped_label_created, + :security_scan_enabled, + :issue_auto_closed, + :repository_imported, + :repository_mirrored + ].freeze + + class << self + def onboard(namespace) + return unless root_namespace?(namespace) + + safe_find_or_create_by(namespace: namespace) + end + + def register(namespace, action) + return unless root_namespace?(namespace) && ACTIONS.include?(action) + + action_column = column_name(action) + onboarding_progress = find_by(namespace: namespace, action_column => nil) + onboarding_progress&.update!(action_column => Time.current) + end + + def completed?(namespace, action) + return unless root_namespace?(namespace) && ACTIONS.include?(action) + + action_column = column_name(action) + where(namespace: namespace).where.not(action_column => nil).exists? + end + + private + + def column_name(action) + :"#{action}_at" + end + + def root_namespace?(namespace) + namespace && namespace.root? + end + end + + private + + def namespace_is_root_namespace + return unless namespace + + errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent? + end +end diff --git a/app/models/packages/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb index de54580e948..64ae5cd88a5 100644 --- a/app/models/packages/conan/file_metadatum.rb +++ b/app/models/packages/conan/file_metadatum.rb @@ -3,8 +3,8 @@ class Packages::Conan::FileMetadatum < ApplicationRecord belongs_to :package_file, inverse_of: :conan_file_metadatum - DEFAULT_PACKAGE_REVISION = '0'.freeze - DEFAULT_RECIPE_REVISION = '0'.freeze + DEFAULT_PACKAGE_REVISION = '0' + DEFAULT_RECIPE_REVISION = '0' validates :package_file, presence: true diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb new file mode 100644 index 00000000000..f7f7f9f95e9 --- /dev/null +++ b/app/models/packages/debian.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Packages + module Debian + def self.table_name_prefix + 'packages_debian_' + end + end +end diff --git a/app/models/packages/debian/file_metadatum.rb b/app/models/packages/debian/file_metadatum.rb new file mode 100644 index 00000000000..7c9f4f5f3f1 --- /dev/null +++ b/app/models/packages/debian/file_metadatum.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class Packages::Debian::FileMetadatum < ApplicationRecord + belongs_to :package_file, inverse_of: :debian_file_metadatum + + validates :package_file, presence: true + validate :valid_debian_package_type + + enum file_type: { + unknown: 1, source: 2, dsc: 3, deb: 4, udeb: 5, buildinfo: 6, changes: 7 + } + + validates :file_type, presence: true + validates :file_type, inclusion: { in: %w[unknown] }, if: -> { package_file&.package&.debian_incoming? } + validates :file_type, + inclusion: { in: %w[source dsc deb udeb buildinfo changes] }, + if: -> { package_file&.package&.debian_package? } + + validates :component, + presence: true, + format: { with: Gitlab::Regex.debian_component_regex }, + if: :requires_component? + validates :component, absence: true, unless: :requires_component? + + validates :architecture, + presence: true, + format: { with: Gitlab::Regex.debian_architecture_regex }, + if: :requires_architecture? + validates :architecture, absence: true, unless: :requires_architecture? + + validates :fields, + presence: true, + json_schema: { filename: "debian_fields" }, + if: :requires_fields? + validates :fields, absence: true, unless: :requires_fields? + + private + + def valid_debian_package_type + return if package_file&.package&.debian? + + errors.add(:package_file, _('Package type must be Debian')) + end + + def requires_architecture? + deb? || udeb? + end + + def requires_component? + source? || dsc? || requires_architecture? || buildinfo? + end + + def requires_fields? + dsc? || requires_architecture? || buildinfo? || changes? + end +end diff --git a/app/models/packages/debian/group_architecture.rb b/app/models/packages/debian/group_architecture.rb new file mode 100644 index 00000000000..570f6accd3c --- /dev/null +++ b/app/models/packages/debian/group_architecture.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupArchitecture < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::Architecture +end diff --git a/app/models/packages/debian/group_distribution.rb b/app/models/packages/debian/group_distribution.rb new file mode 100644 index 00000000000..eea7acacc96 --- /dev/null +++ b/app/models/packages/debian/group_distribution.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupDistribution < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::Distribution +end diff --git a/app/models/packages/debian/project_architecture.rb b/app/models/packages/debian/project_architecture.rb new file mode 100644 index 00000000000..44a38dfaf44 --- /dev/null +++ b/app/models/packages/debian/project_architecture.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectArchitecture < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::Architecture +end diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb new file mode 100644 index 00000000000..a73c12d172d --- /dev/null +++ b/app/models/packages/debian/project_distribution.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectDistribution < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::Distribution +end diff --git a/app/models/packages/dependency.rb b/app/models/packages/dependency.rb index 51b80934827..a32c3c05bb3 100644 --- a/app/models/packages/dependency.rb +++ b/app/models/packages/dependency.rb @@ -6,7 +6,7 @@ class Packages::Dependency < ApplicationRecord validates :name, uniqueness: { scope: :version_pattern } - NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'.freeze + NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)' MAX_STRING_LENGTH = 255.freeze MAX_CHUNKED_QUERIES_COUNT = 10.freeze diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb index 13da82d16d3..98c9d5246db 100644 --- a/app/models/packages/event.rb +++ b/app/models/packages/event.rb @@ -6,6 +6,8 @@ class Packages::Event < ApplicationRecord UNIQUE_EVENTS_ALLOWED = %i[push_package delete_package pull_package].freeze EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze + EVENT_PREFIX = "i_package" + enum event_scope: EVENT_SCOPES enum event_type: { @@ -24,13 +26,6 @@ class Packages::Event < ApplicationRecord enum originator_type: { user: 0, deploy_token: 1, guest: 2 } - def self.allowed_event_name(event_scope, event_type, originator) - return unless event_allowed?(event_type) - - # remove `package` from the event name to avoid issues with HLLRedisCounter class parsing - "i_package_#{event_scope}_#{originator}_#{event_type.gsub(/_packages?/, "")}" - end - # Remove some of the events, for now, so we don't hammer Redis too hard. # See: https://gitlab.com/gitlab-org/gitlab/-/issues/280770 def self.event_allowed?(event_type) @@ -38,4 +33,23 @@ class Packages::Event < ApplicationRecord false end + + # counter names for unique user tracking (for MAU) + def self.unique_counters_for(event_scope, event_type, originator_type) + return [] unless event_allowed?(event_type) + return [] if originator_type.to_s == 'guest' + + ["#{EVENT_PREFIX}_#{event_scope}_#{originator_type}"] + end + + # total counter names for tracking number of events + def self.counters_for(event_scope, event_type, originator_type) + return [] unless event_allowed?(event_type) + + [ + "#{EVENT_PREFIX}_#{event_type}", + "#{EVENT_PREFIX}_#{event_type}_by_#{originator_type}", + "#{EVENT_PREFIX}_#{event_scope}_#{event_type}" + ] + end end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 10c98f03804..2067a800ad5 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -112,6 +112,7 @@ class Packages::Package < ApplicationRecord scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') } scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') } scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') } + scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') } def self.for_projects(projects) return none unless projects.any? @@ -199,6 +200,12 @@ class Packages::Package < ApplicationRecord debian? && !version.nil? end + def package_settings + strong_memoize(:package_settings) do + project.namespace.package_settings + end + end + private def composer_tag_version? diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index e8d1dd1e8c4..389edaea392 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -5,14 +5,17 @@ class Packages::PackageFile < ApplicationRecord delegate :project, :project_id, to: :package delegate :conan_file_type, to: :conan_file_metadatum + delegate :file_type, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian belongs_to :package has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum' has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo' has_many :pipelines, through: :package_file_build_infos + has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum' accepts_nested_attributes_for :conan_file_metadatum + accepts_nested_attributes_for :debian_file_metadatum validates :package, presence: true validates :file, presence: true @@ -25,12 +28,18 @@ class Packages::PackageFile < ApplicationRecord scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) } scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) } scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) } + scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) } scope :with_conan_file_type, ->(file_type) do joins(:conan_file_metadatum) .where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] }) end + scope :with_debian_file_type, ->(file_type) do + joins(:debian_file_metadatum) + .where(packages_debian_file_metadata: { debian_file_type: ::Packages::Debian::FileMetadatum.debian_file_types[file_type] }) + end + scope :with_conan_package_reference, ->(conan_package_reference) do joins(:conan_file_metadatum) .where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference }) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 4004ea9a662..4d60489e599 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -188,7 +188,7 @@ class PagesDomain < ApplicationRecord def user_provided_key=(key) self.key = key - self.certificate_source = 'user_provided' if key_changed? + self.certificate_source = 'user_provided' if attribute_changed?(:key) end def user_provided_certificate @@ -207,7 +207,7 @@ class PagesDomain < ApplicationRecord def gitlab_provided_key=(key) self.key = key - self.certificate_source = 'gitlab_provided' if key_changed? + self.certificate_source = 'gitlab_provided' if attribute_changed?(:key) end def pages_virtual_domain diff --git a/app/models/plan.rb b/app/models/plan.rb index b4091e0a755..6a7f32a5d5f 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Plan < ApplicationRecord - DEFAULT = 'default'.freeze + DEFAULT = 'default' has_one :limits, class_name: 'PlanLimits' diff --git a/app/models/project.rb b/app/models/project.rb index daa5605c2e0..ec790798806 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -34,6 +34,7 @@ class Project < ApplicationRecord include FromUnion include IgnorableColumns include Integration + include Repositories::CanHousekeepRepository include EachBatch extend Gitlab::Cache::RequestCache extend Gitlab::Utils::Override @@ -146,7 +147,6 @@ class Project < ApplicationRecord has_many :boards # Project services - has_one :alerts_service has_one :campfire_service has_one :datadog_service has_one :discord_service @@ -200,6 +200,8 @@ 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 + 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 has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -408,6 +410,9 @@ 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 :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 delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?, :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?, @@ -831,6 +836,10 @@ class Project < ApplicationRecord webide_pipelines.running_or_pending.for_user(user) end + def latest_pipeline_locked + ci_keep_latest_artifact? ? :artifacts_locked : :unlocked + end + def autoclose_referenced_issues return true if super.nil? @@ -1331,19 +1340,11 @@ class Project < ApplicationRecord end def external_wiki - if has_external_wiki.nil? - cache_has_external_wiki - end + cache_has_external_wiki if has_external_wiki.nil? - if has_external_wiki - @external_wiki ||= services.external_wikis.first - else - nil - end - end + return unless has_external_wiki? - def cache_has_external_wiki - update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? + @external_wiki ||= services.external_wikis.first end def find_or_initialize_services @@ -1355,9 +1356,9 @@ class Project < ApplicationRecord end def disabled_services - return ['datadog'] unless Feature.enabled?(:datadog_ci_integration, self) + return %w(datadog alerts) unless Feature.enabled?(:datadog_ci_integration, self) - [] + %w(alerts) end def find_or_initialize_service(name) @@ -1829,6 +1830,15 @@ class Project < ApplicationRecord ensure_pages_metadatum.update!(pages_deployment: deployment) end + def set_first_pages_deployment!(deployment) + ensure_pages_metadatum + + # where().update_all to perform update in the single transaction with check for null + ProjectPagesMetadatum + .where(project_id: id, pages_deployment_id: nil) + .update_all(pages_deployment_id: deployment.id) + end + def write_repository_config(gl_full_path: full_path) # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using @@ -1980,6 +1990,7 @@ class Project < ApplicationRecord .append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level)) .append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase) .append(key: 'CI_DEFAULT_BRANCH', value: default_branch) + .append(key: 'CI_PROJECT_CONFIG_PATH', value: ci_config_path_or_default) end def predefined_ci_server_variables @@ -2113,18 +2124,6 @@ class Project < ApplicationRecord (auto_devops || build_auto_devops)&.predefined_variables end - def pushes_since_gc - Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } - end - - def increment_pushes_since_gc - Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) } - end - - def reset_pushes_since_gc - Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) } - end - def route_map_for(commit_sha) @route_maps_by_commit ||= Hash.new do |h, sha| h[sha] = begin @@ -2430,10 +2429,6 @@ class Project < ApplicationRecord protected_branches.limit(limit) end - def alerts_service_activated? - alerts_service&.active? - end - def self_monitoring? Gitlab::CurrentSettings.self_monitoring_project_id == id end @@ -2486,16 +2481,12 @@ class Project < ApplicationRecord end def service_desk_custom_address - return unless service_desk_custom_address_enabled? + return unless Gitlab::ServiceDeskEmail.enabled? key = service_desk_setting&.project_key return unless key.present? - ::Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}") - end - - def service_desk_custom_address_enabled? - ::Gitlab::ServiceDeskEmail.enabled? && ::Feature.enabled?(:service_desk_custom_address, self, default_enabled: true) + Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}") end def root_namespace @@ -2633,10 +2624,6 @@ class Project < ApplicationRecord from && self != from end - def pushes_since_gc_redis_shared_state_key - "projects/#{id}/pushes_since_gc" - end - def update_project_statistics stats = statistics || build_statistics stats.update(namespace_id: namespace_id) @@ -2699,6 +2686,10 @@ class Project < ApplicationRecord objects.each_batch { |relation| out.concat(relation.pluck(:oid)) } end end + + def cache_has_external_wiki + update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? + end end Project.prepend_if_ee('EE::Project') diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb index b167c2e371b..4f445758653 100644 --- a/app/models/project_feature_usage.rb +++ b/app/models/project_feature_usage.rb @@ -3,8 +3,8 @@ class ProjectFeatureUsage < ApplicationRecord self.primary_key = :project_id - JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'.freeze - JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'.freeze + JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at' + JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at' belongs_to :project validates :project, presence: true diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb index bd1919fe7ed..2bef0056732 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 + include EachBatch + self.primary_key = :project_id belongs_to :project, inverse_of: :pages_metadatum @@ -8,4 +10,5 @@ class ProjectPagesMetadatum < ApplicationRecord belongs_to :pages_deployment scope :deployed, -> { where(deployed: true) } + scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) } end diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb index 5b7d149ace1..4afce0dfe95 100644 --- a/app/models/project_services/alerts_service.rb +++ b/app/models/project_services/alerts_service.rb @@ -1,54 +1,10 @@ # frozen_string_literal: true -require 'securerandom' - +# 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 - has_one :data, class_name: 'AlertsServiceData', autosave: true, - inverse_of: :service, foreign_key: :service_id - - attribute :token, :string - delegate :token, :token=, :token_changed?, :token_was, to: :data - - validates :token, presence: true, if: :activated? - - before_validation :prevent_token_assignment - before_validation :ensure_token, if: :activated? - - after_save :update_http_integration - - def url - return if instance? || template? - - url_helpers.project_alerts_notify_url(project, format: :json) - end - - def json_fields - super + %w(token) - end - - def editable? - false - end - - def show_active_box? - false - end - - def can_test? - false - end - - def title - _('Alerts endpoint') - end - - def description - _('Authorize external services to send alerts to GitLab') - end - - def detailed_description - description - end + before_save :prevent_save def self.to_param 'alerts' @@ -58,35 +14,15 @@ class AlertsService < Service %w() end - def data - super || build_data - end - private - def prevent_token_assignment - self.token = token_was if token.present? && token_changed? - end - - def ensure_token - self.token = generate_token if token.blank? - end - - def generate_token - SecureRandom.hex - end - - def url_helpers - Gitlab::Routing.url_helpers - end - - def update_http_integration - return unless project_id && type == 'AlertsService' + 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') - AlertManagement::SyncAlertServiceDataService # rubocop: disable CodeReuse/ServiceClass - .new(self) - .execute + # 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 - -AlertsService.prepend_if_ee('EE::AlertsService') diff --git a/app/models/project_services/alerts_service_data.rb b/app/models/project_services/alerts_service_data.rb deleted file mode 100644 index 5a52ed83455..00000000000 --- a/app/models/project_services/alerts_service_data.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'securerandom' - -class AlertsServiceData < ApplicationRecord - belongs_to :service, class_name: 'AlertsService' - - validates :service, presence: true - - attr_encrypted :token, - mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, - algorithm: 'aes-256-gcm' -end diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb index 543843ab1b0..3a742bfdcda 100644 --- a/app/models/project_services/datadog_service.rb +++ b/app/models/project_services/datadog_service.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class DatadogService < Service - DEFAULT_SITE = 'datadoghq.com'.freeze - URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'.freeze - URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'.freeze - URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/".freeze + DEFAULT_SITE = 'datadoghq.com' + URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/' + URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api' + URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/" SUPPORTED_EVENTS = %w[ pipeline job diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 1f4abfc1aca..dafd3d095ec 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -157,8 +157,12 @@ class JiraService < IssueTrackerService # support any events. end + def find_issue(issue_key) + jira_request { client.Issue.find(issue_key) } + end + def close_issue(entity, external_issue) - issue = jira_request { client.Issue.find(external_issue.iid) } + issue = find_issue(external_issue.iid) return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? @@ -172,7 +176,7 @@ class JiraService < IssueTrackerService # Depending on the Jira project's workflow, a comment during transition # may or may not be allowed. Refresh the issue after transition and check # if it is closed, so we don't have one comment for every commit. - issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) + issue = find_issue(issue.key) if transition_issue(issue) add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) end @@ -181,7 +185,7 @@ class JiraService < IssueTrackerService return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) } end - jira_issue = jira_request { client.Issue.find(mentioned.id) } + jira_issue = find_issue(mentioned.id) return unless jira_issue.present? diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 0fd85e3a5a9..f39d3947e5b 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class MattermostSlashCommandsService < SlashCommandsService - include TriggersHelper + include Ci::TriggersHelper prop_accessor :token diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 01ded0495a7..548f3623504 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SlackSlashCommandsService < SlashCommandsService - include TriggersHelper + include Ci::TriggersHelper def title 'Slack slash commands' diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index 25e70ab406c..e1336be9528 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -12,6 +12,8 @@ class ProtectableDropdown # Tags/branches which are yet to be individually protected def protectable_ref_names + return [] if @project.empty_repo? + @protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names end diff --git a/app/models/release.rb b/app/models/release.rb index bebf91fb247..2b82fdc37f6 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -82,7 +82,7 @@ class Release < ApplicationRecord end def milestone_titles - self.milestones.map {|m| m.title }.sort.join(", ") + self.milestones.order_by_dates_and_title.map {|m| m.title }.join(', ') end def to_hook_data(action) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 6b8b34ce4d2..880970b72a8 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -308,7 +308,7 @@ class RemoteMirror < ApplicationRecord end def mirror_url_changed? - url_changed? || credentials_changed? + url_changed? || attribute_changed?(:credentials) end def saved_change_to_mirror_url? diff --git a/app/models/repository.rb b/app/models/repository.rb index 93f22dbe122..c19448332f8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -24,10 +24,10 @@ class Repository attr_accessor :full_path, :shard, :disk_path, :container, :repo_type - delegate :ref_name_for_sha, to: :raw_repository - delegate :bundle_to_disk, to: :raw_repository delegate :lfs_enabled?, to: :container + delegate_missing_to :raw_repository + CreateTreeError = Class.new(StandardError) AmbiguousRefError = Class.new(StandardError) @@ -386,10 +386,6 @@ class Repository raw_repository.expire_has_local_branches_cache end - def lookup_cache - @lookup_cache ||= {} - end - def expire_exists_cache expire_method_caches(%i(exists?)) end @@ -494,19 +490,12 @@ class Repository expire_branches_cache if expire_cache end - def method_missing(msg, *args, &block) - if msg == :lookup && !block_given? - lookup_cache[msg] ||= {} - lookup_cache[msg][args.join(":")] ||= raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend - else - raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend + def lookup(sha) + strong_memoize("lookup_#{sha}") do + raw_repository.lookup(sha) end end - def respond_to_missing?(method, include_private = false) - raw_repository.respond_to?(method, include_private) || super - end - def blob_at(sha, path) blob = Blob.decorate(raw_repository.blob_at(sha, path), container) diff --git a/app/models/service.rb b/app/models/service.rb index 57c099d6f04..e5626462dd3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -19,7 +19,6 @@ class Service < ApplicationRecord PROJECT_SPECIFIC_SERVICE_NAMES = %w[ jenkins - alerts ].freeze # Fake services to help with local development. @@ -48,7 +47,6 @@ class Service < ApplicationRecord after_commit :reset_updated_properties after_commit :cache_project_has_external_issue_tracker - after_commit :cache_project_has_external_wiki belongs_to :project, inverse_of: :services belongs_to :group, inverse_of: :services @@ -469,12 +467,6 @@ class Service < ApplicationRecord end end - def cache_project_has_external_wiki - if project && !project.destroyed? - project.cache_has_external_wiki - end - end - def valid_recipients? activated? && !importing? end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 817f9d014eb..c4a7c5e25dc 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -20,6 +20,7 @@ class Snippet < ApplicationRecord extend ::Gitlab::Utils::Override MAX_FILE_COUNT = 10 + MASTER_BRANCH = 'master' cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description @@ -82,6 +83,7 @@ class Snippet < ApplicationRecord scope :inc_author, -> { includes(:author) } scope :inc_relations_for_view, -> { includes(author: :status) } scope :with_statistics, -> { joins(:statistics) } + scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) } attr_mentionable :description @@ -311,13 +313,27 @@ class Snippet < ApplicationRecord override :default_branch def default_branch - super || 'master' + super || MASTER_BRANCH end def repository_storage snippet_repository&.shard_name || self.class.pick_repository_storage end + # Repositories are created by default with the `master` branch. + # This method changes the `HEAD` file to point to the existing + # default branch in case it's not master. + def change_head_to_default_branch + return unless repository.exists? + return if default_branch == MASTER_BRANCH + # All snippets must have at least 1 file. Therefore, if + # `HEAD` is empty is because it's pointing to the wrong + # default branch + return unless repository.empty? || list_files('HEAD').empty? + + repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}") + end + def create_repository return if repository_exists? && snippet_repository diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index fa25a6f8441..54dbc579d54 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class SnippetRepository < ApplicationRecord + include EachBatch include Shardable DEFAULT_EMPTY_FILE_NAME = 'snippetfile' diff --git a/app/models/snippet_repository_storage_move.rb b/app/models/snippet_repository_storage_move.rb index a365569bfa8..bb157c08995 100644 --- a/app/models/snippet_repository_storage_move.rb +++ b/app/models/snippet_repository_storage_move.rb @@ -12,7 +12,11 @@ class SnippetRepositoryStorageMove < ApplicationRecord override :schedule_repository_storage_update_worker def schedule_repository_storage_update_worker - # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/218991 + SnippetUpdateRepositoryStorageWorker.perform_async( + snippet_id, + destination_storage_name, + id + ) end private diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index 1b99f310e1a..efbbd86ae4a 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -27,6 +27,8 @@ module Terraform validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, format: { with: HEX_REGEXP, message: 'only allows hex characters' } + before_destroy :ensure_state_is_unlocked + default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) } def latest_file @@ -87,6 +89,13 @@ module Terraform new_version.save! end + def ensure_state_is_unlocked + return unless locked? + + errors.add(:base, s_("Terraform|You cannot remove the State file because it's locked. Unlock the State file first before removing it.")) + throw :abort # rubocop:disable Cop/BanCatchThrow + end + def parse_serial(file) Gitlab::Json.parse(file)["serial"] rescue JSON::ParserError diff --git a/app/models/user.rb b/app/models/user.rb index c735f20b92c..b4ec6064ff8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -31,7 +31,7 @@ class User < ApplicationRecord INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 - BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'.freeze + BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval' add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token @@ -1358,6 +1358,7 @@ class User < ApplicationRecord def hook_attrs { + id: id, name: name, username: username, avatar_url: avatar_url(only_path: false), @@ -1377,7 +1378,14 @@ class User < ApplicationRecord def set_username_errors namespace_path_errors = self.errors.delete(:"namespace.path") - self.errors[:username].concat(namespace_path_errors) if namespace_path_errors + + return unless namespace_path_errors&.any? + + if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username) + self.errors.add(:base, :username_exists_as_a_different_namespace) + else + self.errors[:username].concat(namespace_path_errors) + end end def username_changed_hook @@ -1564,6 +1572,12 @@ class User < ApplicationRecord end end + def review_requested_open_merge_requests_count(force: false) + Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: 20.minutes) do + MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count + end + end + def assigned_open_issues_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count @@ -1607,6 +1621,7 @@ class User < ApplicationRecord def invalidate_merge_request_cache_counts Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + Rails.cache.delete(['users', id, 'review_requested_open_merge_requests_count']) end def invalidate_todos_done_count diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index b49a7eb72dc..49b93ffaf66 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -8,8 +8,6 @@ class UserPreference < ApplicationRecord # extra methods that aren't really needed here. NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze - ignore_column :feature_filter_type, remove_with: '13.8', remove_after: '2021-01-22' - belongs_to :user scope :with_user, -> { joins(:user) } diff --git a/app/models/wiki.rb b/app/models/wiki.rb index e329a094319..11c10a61d18 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -3,6 +3,7 @@ class Wiki extend ::Gitlab::Utils::Override include HasRepository + include Repositories::CanHousekeepRepository include Gitlab::Utils::StrongMemoize include GlobalID::Identification -- cgit v1.2.3