diff options
Diffstat (limited to 'app/models')
96 files changed, 996 insertions, 379 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index f1f22d94061..ee0c23ef31e 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -12,14 +12,49 @@ class AbuseReport < ApplicationRecord validates :reporter, presence: true validates :user, presence: true validates :message, presence: true - validates :user_id, uniqueness: { message: 'has already been reported' } + validates :category, presence: true + validates :user_id, + uniqueness: { + scope: [:reporter_id, :category], + message: ->(object, data) do + _('You have already reported this user') + end + } + + validates :reported_from_url, + allow_blank: true, + length: { maximum: 512 }, + addressable_url: { + dns_rebind_protection: true, + blocked_message: 'is an invalid URL. You can try reporting the abuse again, ' \ + 'or contact a GitLab administrator for help.' + } scope :by_user, ->(user) { where(user_id: user) } scope :with_users, -> { includes(:reporter, :user) } + enum category: { + spam: 1, + offensive: 2, + phishing: 3, + crypto: 4, + credentials: 5, + copyright: 6, + malware: 7, + other: 8 + } + # For CacheMarkdownField alias_method :author, :reporter + HUMANIZED_ATTRIBUTES = { + reported_from_url: "Reported from" + }.freeze + + def self.human_attribute_name(attr, options = {}) + HUMANIZED_ATTRIBUTES[attr.to_sym] || super + end + def remove_user(deleted_by:) user.delete_async(deleted_by: deleted_by, params: { hard_delete: true }) end diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb index 904961491b5..a436e32b35b 100644 --- a/app/models/achievements/achievement.rb +++ b/app/models/achievements/achievement.rb @@ -7,6 +7,9 @@ module Achievements belongs_to :namespace, inverse_of: :achievements, optional: false + has_many :user_achievements, inverse_of: :achievement + has_many :users, through: :user_achievements, inverse_of: :achievements + strip_attributes! :name, :description validates :name, diff --git a/app/models/achievements/user_achievement.rb b/app/models/achievements/user_achievement.rb new file mode 100644 index 00000000000..885ec660cc9 --- /dev/null +++ b/app/models/achievements/user_achievement.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Achievements + class UserAchievement < ApplicationRecord + belongs_to :achievement, inverse_of: :user_achievements, optional: false + belongs_to :user, inverse_of: :user_achievements, optional: false + + belongs_to :awarded_by_user, + class_name: 'User', + inverse_of: :awarded_user_achievements, + optional: true + belongs_to :revoked_by_user, + class_name: 'User', + inverse_of: :revoked_user_achievements, + optional: true + end +end diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb index a888422a6b4..b432955ad88 100644 --- a/app/models/analytics/cycle_analytics/aggregation.rb +++ b/app/models/analytics/cycle_analytics/aggregation.rb @@ -2,8 +2,7 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord include FromUnion - - belongs_to :group, optional: false + include Analytics::CycleAnalytics::Parentable validates :incremental_runtimes_in_seconds, :incremental_processed_records, :full_runtimes_in_seconds, :full_processed_records, presence: true, length: { maximum: 10 }, allow_blank: true @@ -58,7 +57,10 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord estimation < 1 ? nil : estimation.from_now end - def self.safe_create_for_group(group) + def self.safe_create_for_namespace(group_or_project_namespace) + # Namespaces::ProjectNamespace has no root_ancestor + # Related: https://gitlab.com/gitlab-org/gitlab/-/issues/386124 + group = group_or_project_namespace.is_a?(Group) ? group_or_project_namespace : group_or_project_namespace.parent top_level_group = group.root_ancestor aggregation = find_by(group_id: top_level_group.id) return aggregation if aggregation.present? diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb index 8d3a032812e..8a80514333f 100644 --- a/app/models/analytics/cycle_analytics/project_stage.rb +++ b/app/models/analytics/cycle_analytics/project_stage.rb @@ -3,10 +3,9 @@ module Analytics module CycleAnalytics class ProjectStage < ApplicationRecord - include Analytics::CycleAnalytics::Stage + include Analytics::CycleAnalytics::Stageable - validates :project, presence: true - belongs_to :project + belongs_to :project, optional: false belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id alias_attribute :parent, :project diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 4a046b3ab20..3a5e06e9a1c 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -6,7 +6,7 @@ class Appearance < ApplicationRecord include WithUploads attribute :title, default: '' - attribute :short_title, default: '' + attribute :pwa_short_name, default: '' attribute :description, default: '' attribute :new_project_guidelines, default: '' attribute :profile_image_guidelines, default: '' @@ -23,6 +23,7 @@ class Appearance < ApplicationRecord cache_markdown_field :footer_message, pipeline: :broadcast_message validates :logo, file_size: { maximum: 1.megabyte } + validates :pwa_icon, file_size: { maximum: 1.megabyte } validates :header_logo, file_size: { maximum: 1.megabyte } validates :message_background_color, allow_blank: true, color: true validates :message_font_color, allow_blank: true, color: true @@ -31,6 +32,7 @@ class Appearance < ApplicationRecord validate :single_appearance_row, on: :create mount_uploader :logo, AttachmentUploader + mount_uploader :pwa_icon, AttachmentUploader mount_uploader :header_logo, AttachmentUploader mount_uploader :favicon, FaviconUploader @@ -49,6 +51,10 @@ class Appearance < ApplicationRecord logo_system_path(logo, 'logo') end + def pwa_icon_path + logo_system_path(pwa_icon, 'pwa_icon') + end + def header_logo_path logo_system_path(header_logo, 'header_logo') end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3fb1f58f3e0..59ad0650eb3 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -35,6 +35,7 @@ class ApplicationSetting < ApplicationRecord belongs_to :instance_group, class_name: "Group", foreign_key: 'instance_administrators_group_id' alias_attribute :instance_group_id, :instance_administrators_group_id alias_attribute :instance_administrators_group, :instance_group + alias_attribute :housekeeping_optimize_repository_period, :housekeeping_incremental_repack_period sanitizes! :default_branch_name @@ -256,18 +257,10 @@ class ApplicationSetting < ApplicationRecord presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' }, if: :domain_denylist_enabled? - validates :housekeeping_incremental_repack_period, + validates :housekeeping_optimize_repository_period, presence: true, numericality: { only_integer: true, greater_than: 0 } - validates :housekeeping_full_repack_period, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period } - - validates :housekeeping_gc_period, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period } - validates :terminal_max_session_time, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -413,7 +406,7 @@ class ApplicationSetting < ApplicationRecord validates :invisible_captcha_enabled, inclusion: { in: [true, false], message: N_('must be a boolean value') } - validates :invitation_flow_enforcement, :can_create_group, + validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile, allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } @@ -694,6 +687,10 @@ class ApplicationSetting < ApplicationRecord allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } + validates :allow_runner_registration_token, + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } + before_validation :ensure_uuid! before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed? before_validation :normalize_default_branch_name diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 229c4e68d79..8ef7e9a92a8 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -245,7 +245,9 @@ module ApplicationSettingImplementation users_get_by_id_limit: 300, users_get_by_id_limit_allowlist: [], can_create_group: true, - bulk_import_enabled: false + bulk_import_enabled: false, + allow_runner_registration_token: true, + user_defaults_to_private_profile: false } end diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index e49c4e09a50..ebca5e90313 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -152,6 +152,10 @@ class BulkImports::Entity < ApplicationRecord "::#{pluralized_name.capitalize}::UpdateService".constantize end + def full_path + project? ? project&.full_path : group&.full_path + end + private def validate_parent_is_a_group diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb index 60370c525d5..9bd618c1008 100644 --- a/app/models/chat_name.rb +++ b/app/models/chat_name.rb @@ -7,12 +7,10 @@ class ChatName < ApplicationRecord belongs_to :user validates :user, presence: true - validates :integration, presence: true validates :team_id, presence: true validates :chat_id, presence: true - validates :user_id, uniqueness: { scope: [:integration_id] } - validates :chat_id, uniqueness: { scope: [:integration_id, :team_id] } + validates :chat_id, uniqueness: { scope: :team_id } # Updates the "last_used_timestamp" but only if it wasn't already updated # recently. diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index 76d4b9d6206..f87b18d516f 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -46,7 +46,7 @@ module Ci 'artifacts', path ].join('/') - "#{project.pages_group_url}/#{artifact_path}" + "#{project.pages_namespace_url}/#{artifact_path}" end def external_link?(job) diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 662fb3cffa8..4af31fd37f2 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -19,11 +19,6 @@ module Ci belongs_to :project belongs_to :trigger_request - # To be removed upon :ci_bridge_remove_sourced_pipelines feature flag removal - has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", - foreign_key: :source_job_id, - inverse_of: :source_bridge - has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline validates :ref, presence: true @@ -89,20 +84,8 @@ module Ci end end - def sourced_pipelines - if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project) - raise 'Ci::Bridge does not have sourced_pipelines association' - end - - super - end - def has_downstream_pipeline? - if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project) - sourced_pipeline.present? - else - sourced_pipelines.exists? - end + sourced_pipeline.present? end def downstream_pipeline_params @@ -298,7 +281,7 @@ module Ci return [] unless forward_yaml_variables? yaml_variables.to_a.map do |hash| - if hash[:raw] && ci_raw_variables_in_yaml_config_enabled? + if hash[:raw] { key: hash[:key], value: hash[:value], raw: true } else { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) } @@ -310,7 +293,7 @@ module Ci return [] unless forward_pipeline_variables? pipeline.variables.to_a.map do |variable| - if variable.raw? && ci_raw_variables_in_yaml_config_enabled? + if variable.raw? { key: variable.key, value: variable.value, raw: true } else { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } @@ -323,7 +306,7 @@ module Ci return [] unless pipeline.pipeline_schedule pipeline.pipeline_schedule.variables.to_a.map do |variable| - if variable.raw? && ci_raw_variables_in_yaml_config_enabled? + if variable.raw? { key: variable.key, value: variable.value, raw: true } else { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } @@ -346,12 +329,6 @@ module Ci result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result end end - - def ci_raw_variables_in_yaml_config_enabled? - strong_memoize(:ci_raw_variables_in_yaml_config_enabled) do - ::Feature.enabled?(:ci_raw_variables_in_yaml_config, project) - end - end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7f42b21bc87..0139b025d98 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -68,6 +68,7 @@ module Ci delegate :service_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project delegate :harbor_integration, to: :project + delegate :apple_app_store_integration, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true delegate :ensure_persistent_ref, to: :pipeline delegate :enable_debug_trace!, to: :metadata @@ -587,6 +588,7 @@ module Ci .append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false) .concat(deploy_token_variables) .concat(harbor_variables) + .concat(apple_app_store_variables) end end @@ -630,6 +632,13 @@ module Ci Gitlab::Ci::Variables::Collection.new(harbor_integration.ci_variables) end + def apple_app_store_variables + return [] unless apple_app_store_integration.try(:activated?) + return [] unless pipeline.protected_ref? + + Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables) + end + def features { trace_sections: true, @@ -736,6 +745,12 @@ module Ci self.token && token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token) end + def remove_token! + if Feature.enabled?(:remove_job_token_on_completion, project) + update!(token_encrypted: nil) + end + end + # acts_as_taggable uses this method create/remove tags with contexts # defined by taggings and to get those contexts it executes a query. # We don't use any other contexts except `tags`, so we don't need it. @@ -884,8 +899,9 @@ module Ci return cache unless project.ci_separated_caches - type_suffix = pipeline.protected_ref? ? 'protected' : 'non_protected' cache.map do |entry| + type_suffix = !entry[:unprotect] && pipeline.protected_ref? ? 'protected' : 'non_protected' + entry.merge(key: "#{entry[:key]}-#{type_suffix}") end end @@ -1135,15 +1151,9 @@ module Ci end end - def partition_id_token_prefix - partition_id.to_s(16) if Feature.enabled?(:ci_build_partition_id_token_prefix, project) - end - override :format_token def format_token(token) - return token if partition_id_token_prefix.nil? - - "#{partition_id_token_prefix}_#{token}" + "#{partition_id.to_s(16)}_#{token}" end protected diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 9b4794abb2e..1dcb9190f11 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -71,7 +71,7 @@ module Ci end def timeout_with_highest_precedence - [(job_timeout || project_timeout), runner_timeout].compact.min_by { |timeout| timeout.value } + [(job_timeout || project_timeout), runner_timeout].compact.min_by(&:value) end def project_timeout diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 57d8b9ba368..c5f6e54c336 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -166,7 +166,7 @@ module Ci raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save? self.class.with_read_consistency(build) do - self.reset.then { |chunk| chunk.unsafe_persist_data! } + self.reset.then(&:unsafe_persist_data!) end end rescue FailedToObtainLockError diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 53c358f4eba..0dca5b18a24 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -14,6 +14,8 @@ module Ci include EachBatch include Gitlab::Utils::StrongMemoize + enum accessibility: { public: 0, private: 1 }, _suffix: true + NON_ERASABLE_FILE_TYPES = %w[trace].freeze REPORT_FILE_TYPES = { @@ -346,6 +348,12 @@ module Ci end end + def public_access? + return true unless Feature.enabled?(:non_public_artifacts, type: :development) + + public_accessibility? + end + private def store_file_in_transaction! diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 05207fb1ca0..eab2ab69e44 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -919,8 +919,12 @@ module Ci Gitlab::Ci::Variables::Collection.new.tap do |variables| next variables unless tag? + git_tag = project.repository.find_tag(ref) + + next variables unless git_tag + variables.append(key: 'CI_COMMIT_TAG', value: ref) - variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: project.repository.find_tag(ref).message) + variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: git_tag.message) # legacy variable variables.append(key: 'CI_BUILD_TAG', value: ref) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index a7f3ff938c3..bac85b6095e 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -13,6 +13,7 @@ module Ci include TaggableQueries include Presentable include EachBatch + include Ci::HasRunnerExecutor add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration @@ -27,21 +28,6 @@ module Ci project_type: 3 } - enum executor_type: { - unknown: 0, - custom: 1, - shell: 2, - docker: 3, - docker_windows: 4, - docker_ssh: 5, - ssh: 6, - parallels: 7, - virtualbox: 8, - docker_machine: 9, - docker_ssh_machine: 10, - kubernetes: 11 - }, _suffix: true - # This `ONLINE_CONTACT_TIMEOUT` needs to be larger than # `RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY` # @@ -68,6 +54,7 @@ module Ci TAG_LIST_MAX_LENGTH = 50 + has_many :runner_machines, inverse_of: :runner has_many :builds has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :runner_projects, disable_joins: true @@ -77,6 +64,8 @@ module Ci has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build' has_one :runner_version, primary_key: :version, foreign_key: :version, class_name: 'Ci::RunnerVersion' + belongs_to :creator, class_name: 'User', optional: true + before_save :ensure_token scope :active, -> (value = true) { where(active: value) } @@ -440,7 +429,9 @@ module Ci ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {} values[:contacted_at] = Time.current - values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown) + if values.include?(:executor) + values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown) + end cache_attributes(values) diff --git a/app/models/ci/runner_machine.rb b/app/models/ci/runner_machine.rb new file mode 100644 index 00000000000..1dd997a8ee1 --- /dev/null +++ b/app/models/ci/runner_machine.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Ci + class RunnerMachine < Ci::ApplicationRecord + include FromUnion + include Ci::HasRunnerExecutor + + belongs_to :runner + + validates :runner, presence: true + validates :machine_xid, presence: true, length: { maximum: 64 } + validates :version, length: { maximum: 2048 } + validates :revision, length: { maximum: 255 } + validates :platform, length: { maximum: 255 } + validates :architecture, length: { maximum: 255 } + validates :ip_address, length: { maximum: 1024 } + validates :config, json_schema: { filename: 'ci_runner_config' } + + # The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner machine + # will be considered stale + STALE_TIMEOUT = 7.days + + scope :stale, -> do + created_some_time_ago = arel_table[:created_at].lteq(STALE_TIMEOUT.ago) + contacted_some_time_ago = arel_table[:contacted_at].lteq(STALE_TIMEOUT.ago) + + from_union( + where(contacted_at: nil), + where(contacted_some_time_ago), + remove_duplicates: false).where(created_some_time_ago) + end + end +end diff --git a/app/models/clusters/concerns/provider_status.rb b/app/models/clusters/concerns/provider_status.rb index 2da1ee7aabb..44da840bec3 100644 --- a/app/models/clusters/concerns/provider_status.rb +++ b/app/models/clusters/concerns/provider_status.rb @@ -24,7 +24,7 @@ module Clusters transition any - [:errored] => :errored end - before_transition any => [:errored, :created] do |provider| + before_transition any => [:errored, :created] do |provider, _| provider.nullify_credentials end diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb index f0f56d9ebd9..969820459e3 100644 --- a/app/models/clusters/providers/aws.rb +++ b/app/models/clusters/providers/aws.rb @@ -45,18 +45,6 @@ module Clusters ) end - def api_client - strong_memoize(:api_client) do - ::Aws::CloudFormation::Client.new(credentials: credentials, region: region) - end - end - - def credentials - strong_memoize(:credentials) do - ::Aws::Credentials.new(access_key_id, secret_access_key, session_token) - end - end - def has_rbac_enabled? true end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index fde5ed592cb..6f39037b947 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -37,12 +37,6 @@ module Clusters greater_than: 0 } - def api_client - return unless access_token - - @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil) - end - def nullify_credentials assign_attributes( access_token: nil, diff --git a/app/models/commit.rb b/app/models/commit.rb index 5175842e5de..a95ab756600 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -359,10 +359,6 @@ class Commit end def has_signature? - if signature_type == :SSH && !ssh_signatures_enabled? - return false - end - signature_type && signature_type != :NONE end @@ -382,10 +378,6 @@ class Commit @signature_type ||= raw_signature_type || :NONE end - def ssh_signatures_enabled? - Feature.enabled?(:ssh_commit_signatures, project) - end - def signature strong_memoize(:signature) do case signature_type @@ -394,7 +386,7 @@ class Commit when :X509 Gitlab::X509::Commit.new(self).signature when :SSH - Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled? + Gitlab::Ssh::Commit.new(self).signature else nil end @@ -584,9 +576,7 @@ class Commit private def expire_note_etag_cache_for_related_mrs - MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each do |mr| - mr.expire_note_etag_cache - end + MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:expire_note_etag_cache) end def commit_reference(from, referable_commit_id, full: false) diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb index 7d89ddde0cb..47ecdfa8574 100644 --- a/app/models/commit_collection.rb +++ b/app/models/commit_collection.rb @@ -25,7 +25,7 @@ class CommitCollection end def committers - emails = without_merge_commits.map(&:committer_email).uniq + emails = without_merge_commits.filter_map(&:committer_email).uniq User.by_any_email(emails) end diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb index 1e64e2b2978..e9e16651d1c 100644 --- a/app/models/commit_signatures/ssh_signature.rb +++ b/app/models/commit_signatures/ssh_signature.rb @@ -6,13 +6,18 @@ module CommitSignatures include SignatureType belongs_to :key, optional: true + belongs_to :user, optional: true def type :ssh end def signed_by_user - key&.user + user || key&.user + end + + def key_fingerprint_sha256 + super || key&.fingerprint_sha256 end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2470eada62e..64e585bae14 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -71,6 +71,7 @@ class CommitStatus < Ci::ApplicationRecord scope :scheduled_at_before, ->(date) { where('ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?', date) } + scope :with_when_executed, ->(when_executed) { where(when: when_executed) } # The scope applies `pluck` to split the queries. Use with care. scope :for_project_paths, -> (paths) do diff --git a/app/models/concerns/analytics/cycle_analytics/parentable.rb b/app/models/concerns/analytics/cycle_analytics/parentable.rb new file mode 100644 index 00000000000..785f6eea6bf --- /dev/null +++ b/app/models/concerns/analytics/cycle_analytics/parentable.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Parentable + extend ActiveSupport::Concern + + included do + belongs_to :namespace, class_name: 'Namespace', foreign_key: :group_id, optional: false # rubocop: disable Rails/InverseOf + + validate :ensure_namespace_type + + def ensure_namespace_type + return if namespace.nil? + return if namespace.is_a?(::Namespaces::ProjectNamespace) || namespace.is_a?(::Group) + + errors.add(:namespace, s_('CycleAnalytics|the assigned object is not supported')) + end + end + end + end +end diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stageable.rb index d9e6756ab86..d1f948d1366 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stageable.rb @@ -2,7 +2,7 @@ module Analytics module CycleAnalytics - module Stage + module Stageable extend ActiveSupport::Concern include RelativePositioning include Gitlab::Utils::StrongMemoize @@ -10,7 +10,7 @@ module Analytics included do belongs_to :start_event_label, class_name: 'GroupLabel', optional: true belongs_to :end_event_label, class_name: 'GroupLabel', optional: true - belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', foreign_key: :stage_event_hash_id, optional: true + belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', optional: true validates :name, presence: true validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? @@ -21,39 +21,31 @@ module Analytics validate :validate_stage_event_pairs validate :validate_labels - enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier - enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier + enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, + _prefix: :start_event_identifier + enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, + _prefix: :end_event_identifier alias_attribute :custom_stage?, :custom scope :default_stages, -> { where(custom: false) } scope :ordered, -> { order(:relative_position, :id) } scope :with_preloaded_labels, -> { includes(:start_event_label, :end_event_label) } scope :for_list, -> { with_preloaded_labels.ordered } - scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) } + scope :by_value_stream, ->(value_stream) { where(value_stream_id: value_stream.id) } before_save :ensure_stage_event_hash_id after_commit :cleanup_old_stage_event_hash end - def parent=(_) - raise NotImplementedError - end - - def parent - raise NotImplementedError - end - def start_event - strong_memoize(:start_event) do - Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event) - end + Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event) end + strong_memoize_attr :start_event def end_event - strong_memoize(:end_event) do - Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event) - end + Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event) end + strong_memoize_attr :end_event def events_hash_code Digest::SHA256.hexdigest("#{start_event.hash_code}-#{end_event.hash_code}") @@ -109,9 +101,9 @@ module Analytics def validate_stage_event_pairs return if start_event_identifier.nil? || end_event_identifier.nil? - unless pairing_rules.fetch(start_event.class, []).include?(end_event.class) - errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event')) - end + return if pairing_rules.fetch(start_event.class, []).include?(end_event.class) + + errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event')) end def pairing_rules @@ -119,21 +111,23 @@ module Analytics end def validate_labels - validate_label_within_group(:start_event_label_id, start_event_label_id) if start_event_label_id_changed? - validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed? + validate_label_within_namespace(:start_event_label_id, start_event_label_id) if start_event_label_id_changed? + validate_label_within_namespace(:end_event_label_id, end_event_label_id) if end_event_label_id_changed? end - def validate_label_within_group(association_name, label_id) + def validate_label_within_namespace(association_name, label_id) return unless label_id - return unless group - unless label_available_for_group?(label_id) - errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group')) - end + return if label_available_for_namespace?(label_id) + + errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group')) end - def label_available_for_group?(label_id) - LabelsFinder.new(nil, { group_id: group.id, include_ancestor_groups: true, only_group_labels: true }) + def label_available_for_namespace?(label_id) + subject = is_a?(::Analytics::CycleAnalytics::Stage) ? namespace : project.group + return unless subject + + LabelsFinder.new(nil, { group_id: subject.id, include_ancestor_groups: true, only_group_labels: true }) .execute(skip_authorization: true) .id_in(label_id) .exists? @@ -142,15 +136,15 @@ module Analytics def ensure_stage_event_hash_id previous_stage_event_hash = stage_event_hash&.hash_sha256 - if previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash - self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code) - end + return unless previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash + + self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code) end def cleanup_old_stage_event_hash - if stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was - Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was) - end + return unless stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was + + Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was) end end end diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb index fd4d574ac58..c1c8307500e 100644 --- a/app/models/concerns/board_recent_visit.rb +++ b/app/models/concerns/board_recent_visit.rb @@ -9,9 +9,7 @@ module BoardRecentVisit "user" => user, board_parent_relation => board.resource_parent, board_relation => board - ).tap do |visit| - visit.touch - end + ).tap(&:touch) rescue ActiveRecord::RecordNotUnique retry end diff --git a/app/models/concerns/ci/has_runner_executor.rb b/app/models/concerns/ci/has_runner_executor.rb new file mode 100644 index 00000000000..dc70cdb2018 --- /dev/null +++ b/app/models/concerns/ci/has_runner_executor.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ci + module HasRunnerExecutor + extend ActiveSupport::Concern + + included do + enum executor_type: { + unknown: 0, + custom: 1, + shell: 2, + docker: 3, + docker_windows: 4, + docker_ssh: 5, + ssh: 6, + parallels: 7, + virtualbox: 8, + docker_machine: 9, + docker_ssh_machine: 10, + kubernetes: 11 + }, _suffix: true + end + end +end diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb index f1efbba67e1..784afd1f231 100644 --- a/app/models/concerns/counter_attribute.rb +++ b/app/models/concerns/counter_attribute.rb @@ -88,12 +88,20 @@ module CounterAttribute end def increment_counter(attribute, increment) - return if increment == 0 + return if increment.amount == 0 run_after_commit_or_now do new_value = counter(attribute).increment(increment) - log_increment_counter(attribute, increment, new_value) + log_increment_counter(attribute, increment.amount, new_value) + end + end + + def bulk_increment_counter(attribute, increments) + run_after_commit_or_now do + new_value = counter(attribute).bulk_increment(increments) + + log_increment_counter(attribute, increments.sum(&:amount), new_value) end end @@ -103,14 +111,22 @@ module CounterAttribute end end - def reset_counter!(attribute) + def initiate_refresh!(attribute) + raise ArgumentError, %(attribute "#{attribute}" cannot be refreshed) unless counter_attribute_enabled?(attribute) + detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do - counter(attribute).reset! + counter(attribute).initiate_refresh! end log_clear_counter(attribute) end + def finalize_refresh(attribute) + raise ArgumentError, %(attribute "#{attribute}" cannot be refreshed) unless counter_attribute_enabled?(attribute) + + counter(attribute).finalize_refresh + end + def execute_after_commit_callbacks self.class.after_commit_callbacks.each do |callback| callback.call(self.reset) @@ -122,11 +138,17 @@ module CounterAttribute def build_counter_for(attribute) raise ArgumentError, %(attribute "#{attribute}" does not exist) unless has_attribute?(attribute) - if counter_attribute_enabled?(attribute) - Gitlab::Counters::BufferedCounter.new(self, attribute) - else - Gitlab::Counters::LegacyCounter.new(self, attribute) - end + return legacy_counter(attribute) unless counter_attribute_enabled?(attribute) + + buffered_counter(attribute) + end + + def legacy_counter(attribute) + Gitlab::Counters::LegacyCounter.new(self, attribute) + end + + def buffered_counter(attribute) + Gitlab::Counters::BufferedCounter.new(self, attribute) end def database_lock_key diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index 1af655277b8..b02c95c9662 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -14,20 +14,32 @@ module HasUserType migration_bot: 7, security_bot: 8, automation_bot: 9, - admin_bot: 11 + admin_bot: 11, + suggested_reviewers_bot: 12 }.with_indifferent_access.freeze - BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot admin_bot].freeze + BOT_USER_TYPES = %w[ + alert_bot + project_bot + support_bot + visual_review_bot + migration_bot + security_bot + automation_bot + admin_bot + suggested_reviewers_bot + ].freeze + NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze included do scope :humans, -> { where(user_type: :human) } scope :bots, -> { where(user_type: BOT_USER_TYPES) } - scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) } + scope :without_bots, -> { humans.or(where(user_type: USER_TYPES.keys - BOT_USER_TYPES)) } scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) } - scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) } - scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) } + scope :without_ghosts, -> { humans.or(where(user_type: USER_TYPES.keys - ['ghost'])) } + scope :without_project_bot, -> { humans.or(where(user_type: USER_TYPES.keys - ['project_bot'])) } scope :human_or_service_user, -> { humans.or(where(user_type: :service_user)) } enum user_type: USER_TYPES diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 492d55c74e2..eed396f785b 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -88,7 +88,7 @@ module Noteable def discussions @discussions ||= discussion_notes - .inc_relations_for_view + .inc_relations_for_view(self) .discussions(self) end @@ -126,7 +126,7 @@ module Noteable def grouped_diff_discussions(*args) # Doesn't use `discussion_notes`, because this may include commit diff notes # besides MR diff notes, that we do not want to display on the MR Changes tab. - notes.inc_relations_for_view.grouped_diff_discussions(*args) + notes.inc_relations_for_view(self).grouped_diff_discussions(*args) end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -205,6 +205,14 @@ module Noteable model_name.singular end + def commenters(user: nil) + eligable_notes = notes.user + + eligable_notes = eligable_notes.not_internal unless user&.can?(:read_internal_note, self) + + User.where(id: eligable_notes.select(:author_id).distinct) + end + private # Synthetic system notes don't have discussion IDs because these are generated dynamically diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index d37f20e2e7c..b910c0ab5c2 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -124,8 +124,13 @@ module ProjectFeaturesCompatibility private def write_feature_attribute_boolean(field, value) - access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - write_feature_attribute_raw(field, access_level) + value_type = Gitlab::Utils.to_boolean(value) + if value_type.in?([true, false]) + access_level = value_type ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + write_feature_attribute_raw(field, access_level) + else + write_feature_attribute_string(field, value) + end end def write_feature_attribute_string(field, value) diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index 92a88d2f7c8..141c480ea1f 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -96,7 +96,7 @@ module ResolvableDiscussion def unresolve! return unless resolvable? - update { |notes| notes.unresolve! } + update(&:unresolve!) end def clear_memoized_values diff --git a/app/models/concerns/safely_change_column_default.rb b/app/models/concerns/safely_change_column_default.rb new file mode 100644 index 00000000000..567f690d950 --- /dev/null +++ b/app/models/concerns/safely_change_column_default.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# == SafelyChangeColumnDefault concern. +# +# Contains functionality that allows safely changing a column default without downtime. +# Without this concern, Rails can mutate the old default value to the new default value if the old default is explicitly +# specified. +# +# Usage: +# +# class SomeModel < ApplicationRecord +# include SafelyChangeColumnDefault +# +# columns_changing_default :value +# end +# +# # Assume a default of 100 for value +# SomeModel.create!(value: 100) # INSERT INTO some_model (value) VALUES (100) +# change_column_default('some_model', 'value', from: 100, to: 101) +# SomeModel.create!(value: 100) # INSERT INTO some_model (value) VALUES (100) +# # Without this concern, would be INSERT INTO some_model (value) DEFAULT VALUES and would insert 101. +module SafelyChangeColumnDefault + extend ActiveSupport::Concern + + class_methods do + # Indicate that one or more columns will have their database default change. + # + # By indicating those columns here, this helper prevents a case where explicitly writing the old database default + # will be mutated to the new database default. + def columns_changing_default(*columns) + self.columns_with_changing_default = columns.map(&:to_s) + end + end + + included do + class_attribute :columns_with_changing_default, default: [] + + before_create do + columns_with_changing_default.to_a.each do |attr_name| + attr = @attributes[attr_name] + + attribute_will_change!(attr_name) if !attr.changed? && attr.came_from_user? + end + end + end +end diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb index 586f1dbb65c..89398537e0a 100644 --- a/app/models/concerns/update_project_statistics.rb +++ b/app/models/concerns/update_project_statistics.rb @@ -78,9 +78,10 @@ module UpdateProjectStatistics return if delta == 0 return if project.nil? + increment = Gitlab::Counters::Increment.new(amount: delta, ref: id) + run_after_commit do - ProjectStatistics.increment_statistic( - project, self.class.project_statistics_name, delta) + ProjectStatistics.increment_statistic(project, self.class.project_statistics_name, increment) end end end diff --git a/app/models/concerns/work_item_resource_event.rb b/app/models/concerns/work_item_resource_event.rb new file mode 100644 index 00000000000..d0323feb029 --- /dev/null +++ b/app/models/concerns/work_item_resource_event.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module WorkItemResourceEvent + extend ActiveSupport::Concern + + included do + belongs_to :work_item, foreign_key: 'issue_id' + end + + def work_item_synthetic_system_note(events: nil) + # System notes for label resource events are handled in batches, so that we have single system note for multiple + # label changes. + if is_a?(ResourceLabelEvent) && events.present? + return synthetic_note_class.from_events(events, resource: work_item, resource_parent: work_item.project) + end + + synthetic_note_class.from_event(self, resource: work_item, resource_parent: work_item.project) + end + + def synthetic_note_class + raise NoMethodError, 'must implement `synthetic_note_class` method' + end +end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 2563fd484f1..aaafa396337 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -19,7 +19,7 @@ class DeployKey < Key scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) } scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) } - accepts_nested_attributes_for :deploy_keys_projects + accepts_nested_attributes_for :deploy_keys_projects, reject_if: :reject_deploy_keys_projects? def private? !public? @@ -72,4 +72,10 @@ class DeployKey < Key def impersonated? false end + + private + + def reject_deploy_keys_projects? + !self.valid? + end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1254ce1c90a..1ae7d9925a5 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -103,15 +103,6 @@ class Deployment < ApplicationRecord deployment.finished_at = Time.current end - after_transition any => :running do |deployment| - next unless deployment.project.ci_forward_deployment_enabled? - next if Feature.enabled?(:prevent_outdated_deployment_jobs, deployment.project) - - deployment.run_after_commit do - Deployments::DropOlderDeploymentsWorker.perform_async(id) - end - end - after_transition any => :running do |deployment, transition| deployment.run_after_commit do Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current) @@ -303,7 +294,7 @@ class Deployment < ApplicationRecord end def older_than_last_successful_deployment? - last_deployment_id = environment.last_deployment&.id + last_deployment_id = environment&.last_deployment&.id return false unless last_deployment_id.present? return false if self.id == last_deployment_id diff --git a/app/models/description_version.rb b/app/models/description_version.rb index 96c8553c101..fb61b7f5fde 100644 --- a/app/models/description_version.rb +++ b/app/models/description_version.rb @@ -6,6 +6,8 @@ class DescriptionVersion < ApplicationRecord validate :exactly_one_issuable + delegate :resource_parent, to: :issuable + def self.issuable_attrs %i(issue merge_request).freeze end diff --git a/app/models/environment.rb b/app/models/environment.rb index f1edfb3a34b..7d99f10822d 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -98,6 +98,27 @@ class Environment < ApplicationRecord scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) } scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) } + scope :deployed_and_updated_before, -> (project_id, before) do + # this query joins deployments and filters out any environment that has recent deployments + joins = %{ + LEFT JOIN "deployments" on "deployments".environment_id = "environments".id + AND "deployments".project_id = #{project_id} + AND "deployments".updated_at >= #{connection.quote(before)} + } + Environment.joins(joins) + .where(project_id: project_id, updated_at: ...before) + .group('id', 'deployments.id') + .having('deployments.id IS NULL') + end + scope :without_protected, -> (project) {} # no-op when not in EE mode + + scope :without_names, -> (names) do + where.not(name: names) + end + scope :without_tiers, -> (tiers) do + where.not(tier: tiers) + end + ## # Search environments which have names like the given query. # Do not set a large limit unless you've confirmed that it works on gitlab.com scale. diff --git a/app/models/event.rb b/app/models/event.rb index ed65b367b8a..333841b1f90 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -31,6 +31,7 @@ class Event < ApplicationRecord DESIGN_ACTIONS = [:created, :updated, :destroyed].freeze TEAM_ACTIONS = [:joined, :left, :expired].freeze ISSUE_ACTIONS = [:created, :updated, :closed, :reopened].freeze + ISSUE_TYPES = [Issue.name, WorkItem.name].freeze TARGET_TYPES = HashWithIndifferentAccess.new( issue: Issue, @@ -83,6 +84,7 @@ class Event < ApplicationRecord scope :recent, -> { reorder(id: :desc) } scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') } scope :for_design, -> { where(target_type: 'DesignManagement::Design') } + scope :for_issue, -> { where(target_type: ISSUE_TYPES) } scope :for_fingerprint, ->(fingerprint) do fingerprint.present? ? where(fingerprint: fingerprint) : none end diff --git a/app/models/group.rb b/app/models/group.rb index 0cdd7dd8596..c7ad4d61ddb 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -30,6 +30,8 @@ class Group < Namespace has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent + has_many :namespace_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }).unscope(where: %i[source_id source_type]) }, + foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember' alias_method :members, :group_members has_many :users, through: :group_members @@ -39,6 +41,8 @@ class Group < Namespace source: :user has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent + has_many :namespace_requesters, -> { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) }, + foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember' has_many :members_and_requesters, as: :source, class_name: 'GroupMember' has_many :milestones @@ -815,7 +819,7 @@ class Group < Namespace case state when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override - when SR_DISABLED_WITH_OVERRIDE then disable_shared_runners_and_allow_override! + when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE then disable_shared_runners_and_allow_override! when SR_ENABLED then enable_shared_runners! # set both to true end end @@ -846,7 +850,7 @@ class Group < Namespace def has_project_with_service_desk_enabled? Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists? end - strong_memoize_attr :has_project_with_service_desk_enabled?, :has_project_with_service_desk_enabled + strong_memoize_attr :has_project_with_service_desk_enabled? def activity_path Gitlab::Routing.url_helpers.activity_group_path(self) @@ -915,6 +919,10 @@ class Group < Namespace feature_flag_enabled_for_self_or_ancestor?(:work_items_create_from_markdown) end + def usage_quotas_enabled? + ::Feature.enabled?(:usage_quotas_for_all_editions, self) && root? + end + # Check for enabled features, similar to `Project#feature_available?` # NOTE: We still want to keep this after removing `Namespace#feature_available?`. override :feature_available? @@ -1055,7 +1063,7 @@ class Group < Namespace end def disable_shared_runners_and_allow_override! - # enabled -> disabled_with_override + # enabled -> disabled_and_overridable if shared_runners_enabled? update!( shared_runners_enabled: false, @@ -1068,7 +1076,7 @@ class Group < Namespace all_projects.update_all(shared_runners_enabled: false) - # disabled_and_unoverridable -> disabled_with_override + # disabled_and_unoverridable -> disabled_and_overridable else update!(allow_descendants_override_disabled_shared_runners: true) end diff --git a/app/models/integration.rb b/app/models/integration.rb index a630a6dee11..54eeab10360 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -27,7 +27,7 @@ class Integration < ApplicationRecord # TODO Shimo is temporary disabled on group and instance-levels. # See: https://gitlab.com/gitlab-org/gitlab/-/issues/345677 PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[ - jenkins shimo + apple_app_store jenkins shimo ].freeze # Fake integrations to help with local development. @@ -75,6 +75,7 @@ class Integration < ApplicationRecord attribute :active, default: false attribute :alert_events, default: true + attribute :incident_events, default: false attribute :category, default: 'common' attribute :commit_events, default: true attribute :confidential_issues_events, default: true @@ -132,6 +133,7 @@ class Integration < ApplicationRecord scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :deployment_hooks, -> { where(deployment_events: true, active: true) } scope :alert_hooks, -> { where(alert_events: true, active: true) } + scope :incident_hooks, -> { where(incident_events: true, active: true) } scope :deployment, -> { where(category: 'deployment') } class << self diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb new file mode 100644 index 00000000000..84185542939 --- /dev/null +++ b/app/models/integrations/apple_app_store.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'app_store_connect' + +module Integrations + class AppleAppStore < Integration + ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze + KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze + + with_options if: :activated? do + validates :app_store_issuer_id, presence: true, format: { with: ISSUER_ID_REGEX } + validates :app_store_key_id, presence: true, format: { with: KEY_ID_REGEX } + validates :app_store_private_key, presence: true, certificate_key: true + end + + field :app_store_issuer_id, + section: SECTION_TYPE_CONNECTION, + required: true, + title: -> { s_('AppleAppStore|The Apple App Store Connect Issuer ID.') } + + field :app_store_key_id, + section: SECTION_TYPE_CONNECTION, + required: true, + title: -> { s_('AppleAppStore|The Apple App Store Connect Key ID.') }, + is_secret: false + + field :app_store_private_key, + section: SECTION_TYPE_CONNECTION, + required: true, + type: 'textarea', + title: -> { s_('AppleAppStore|The Apple App Store Connect Private Key.') }, + is_secret: false + + def title + 'Apple App Store Connect' + end + + def description + s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.') + end + + def help + variable_list = [ + '<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>', + '<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>', + '<code>APP_STORE_CONNECT_API_KEY_KEY</code>' + ] + + # rubocop:disable Layout/LineLength + texts = [ + s_("Use the Apple App Store Connect integration to easily connect to the Apple App Store with Fastlane in CI/CD pipelines."), + s_("After the Apple App Store Connect integration is activated, the following protected variables will be created for CI/CD use."), + variable_list.join('<br>'), + s_(format("To get started, see the <a href='%{url}' target='_blank'>integration documentation</a> for instructions on how to generate App Store Connect credentials, and how to use this integration.", url: "https://docs.gitlab.com/ee/integration/apple_app_store.html")).html_safe + ] + # rubocop:enable Layout/LineLength + + texts.join('<br><br>'.html_safe) + end + + def self.to_param + 'apple_app_store' + end + + def self.supported_events + [] + end + + def sections + [ + { + type: SECTION_TYPE_CONNECTION, + title: s_('Integrations|Integration details'), + description: help + } + ] + end + + def test(*_args) + response = client.apps + if response.has_key?(:errors) + { success: false, message: response[:errors].first[:title] } + else + { success: true } + end + end + + def ci_variables + return [] unless activated? + + [ + { key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: app_store_issuer_id, masked: true, public: false }, + { key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(app_store_private_key), masked: true, + public: false }, + { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false } + ] + end + + private + + def client + config = { + issuer_id: app_store_issuer_id, + key_id: app_store_key_id, + private_key: app_store_private_key + } + + AppStoreConnect::Client.new(config) + end + end +end diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index f2a707c2214..8700b673370 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -10,7 +10,7 @@ module Integrations SUPPORTED_EVENTS = %w[ push issue confidential_issue merge_request note confidential_note - tag_push pipeline wiki_page deployment + tag_push pipeline wiki_page deployment incident ].freeze SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze @@ -76,21 +76,29 @@ module Integrations def default_fields [ - { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze, + { + type: 'checkbox', + section: SECTION_TYPE_CONFIGURATION, + name: 'notify_only_broken_pipelines', + help: 'Do not send notifications for successful pipelines.' + }.freeze, { type: 'select', + section: SECTION_TYPE_CONFIGURATION, name: 'branches_to_be_notified', title: s_('Integrations|Branches for which notifications are to be sent'), choices: self.class.branch_choices }.freeze, { type: 'text', + section: SECTION_TYPE_CONFIGURATION, name: 'labels_to_be_notified', placeholder: '~backend,~frontend', help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' }.freeze, { type: 'select', + section: SECTION_TYPE_CONFIGURATION, name: 'labels_to_be_notified_behavior', choices: [ ['Match any of the labels', MATCH_ANY_LABEL], @@ -224,6 +232,7 @@ module Integrations data.merge(project_url: project_url, project_name: project_name).with_indifferent_access end + # rubocop:disable Metrics/CyclomaticComplexity def get_message(object_kind, data) case object_kind when "push", "tag_push" @@ -240,8 +249,11 @@ module Integrations Integrations::ChatMessage::WikiPageMessage.new(data) when "deployment" Integrations::ChatMessage::DeploymentMessage.new(data) if notify_for_ref?(data) + when "incident" + Integrations::ChatMessage::IssueMessage.new(data) unless update?(data) end end + # rubocop:enable Metrics/CyclomaticComplexity def build_event_channels event_channel_names.map do |channel_field| diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb index 11ff7547325..619579a543a 100644 --- a/app/models/integrations/base_slash_commands.rb +++ b/app/models/integrations/base_slash_commands.rb @@ -66,7 +66,7 @@ module Integrations # rubocop: disable CodeReuse/ServiceClass def authorize_chat_name_url(params) - ChatNames::AuthorizeUserService.new(self, params).execute + ChatNames::AuthorizeUserService.new(params).execute end # rubocop: enable CodeReuse/ServiceClass end diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb index ca8ef670e67..1c234630370 100644 --- a/app/models/integrations/chat_message/issue_message.rb +++ b/app/models/integrations/chat_message/issue_message.rb @@ -9,6 +9,7 @@ module Integrations attr_reader :action attr_reader :state attr_reader :description + attr_reader :object_kind def initialize(params) super @@ -21,6 +22,7 @@ module Integrations @action = obj_attr[:action] @state = obj_attr[:state] @description = obj_attr[:description] || '' + @object_kind = params[:object_kind] end def attachments @@ -32,7 +34,7 @@ module Integrations def activity { - title: "Issue #{state} by #{strip_markup(user_combined_name)}", + title: "#{issue_type} #{state} by #{strip_markup(user_combined_name)}", subtitle: "in #{project_link}", text: issue_link, image: user_avatar @@ -42,7 +44,7 @@ module Integrations private def message - "[#{project_link}] Issue #{issue_link} #{state} by #{strip_markup(user_combined_name)}" + "[#{project_link}] #{issue_type} #{issue_link} #{state} by #{strip_markup(user_combined_name)}" end def opened_issue? @@ -69,6 +71,10 @@ module Integrations def issue_title "#{Issue.reference_prefix}#{issue_iid} #{strip_markup(title)}" end + + def issue_type + @issue_type ||= object_kind == 'incident' ? 'Incident' : 'Issue' + end end end end diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb index 88db40bea7f..f8a634be336 100644 --- a/app/models/integrations/chat_message/pipeline_message.rb +++ b/app/models/integrations/chat_message/pipeline_message.rb @@ -151,7 +151,7 @@ module Integrations fields << failed_stages_field if failed_stages.any? fields << failed_jobs_field if failed_jobs.any? fields << yaml_error_field if pipeline.has_yaml_errors? - fields << pipeline_name_field if Feature.enabled?(:pipeline_name, project) && pipeline.name.present? + fields << pipeline_name_field if pipeline.name.present? fields end diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb index 53c8f5f623e..329c046075f 100644 --- a/app/models/integrations/field.rb +++ b/app/models/integrations/field.rb @@ -4,7 +4,7 @@ module Integrations class Field SECRET_NAME = %r/token|key|password|passphrase|secret/.freeze - BOOLEAN_ATTRIBUTES = %i[required api_only exposes_secrets].freeze + BOOLEAN_ATTRIBUTES = %i[required api_only is_secret exposes_secrets].freeze ATTRIBUTES = %i[ section type placeholder choices value checkbox_label @@ -17,12 +17,13 @@ module Integrations attr_reader :name, :integration_class - def initialize(name:, integration_class:, type: 'text', api_only: false, **attributes) + def initialize(name:, integration_class:, type: 'text', is_secret: true, api_only: false, **attributes) @name = name.to_s.freeze @integration_class = integration_class - attributes[:type] = SECRET_NAME.match?(@name) ? 'password' : type + attributes[:type] = SECRET_NAME.match?(@name) && is_secret ? 'password' : type attributes[:api_only] = api_only + attributes[:is_secret] = is_secret @attributes = attributes.freeze invalid_attributes = attributes.keys - ATTRIBUTES diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb deleted file mode 100644 index d7625cfb3d2..00000000000 --- a/app/models/integrations/flowdock.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -# This integration is scheduled for removal. -# All records must be deleted before the class can be removed. -# https://gitlab.com/gitlab-org/gitlab/-/issues/379197 -module Integrations - class Flowdock < Integration - def readonly? - true - end - - def self.to_param - 'flowdock' - end - - def self.supported_events - %w[] - end - end -end diff --git a/app/models/issue.rb b/app/models/issue.rb index 1dd11ff8315..6744ee230b0 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -46,7 +46,7 @@ class Issue < ApplicationRecord # # This should be kept consistent with the enums used for the GraphQL issue list query in # https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158 - TYPES_FOR_LIST = %w(issue incident test_case task objective).freeze + TYPES_FOR_LIST = %w(issue incident test_case task objective key_result).freeze # Types of issues that should be displayed on issue board lists TYPES_FOR_BOARD_LIST = %w(issue incident).freeze @@ -663,11 +663,6 @@ class Issue < ApplicationRecord author&.banned? end - # Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB - def work_item_type - super || WorkItems::Type.default_by_type(issue_type) - end - def expire_etag_cache key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self) Gitlab::EtagCaching::Store.new.touch(key) diff --git a/app/models/label_note.rb b/app/models/label_note.rb index 19dede36abd..eda650f2fa2 100644 --- a/app/models/label_note.rb +++ b/app/models/label_note.rb @@ -4,12 +4,19 @@ class LabelNote < SyntheticNote attr_accessor :resource_parent attr_reader :events + def self.from_event(event, resource: nil, resource_parent: nil) + attrs = note_attributes('label', event, resource, resource_parent).merge(events: [event]) + + LabelNote.new(attrs) + end + def self.from_events(events, resource: nil, resource_parent: nil) resource ||= events.first.issuable - attrs = note_attributes('label', events.first, resource, resource_parent).merge(events: events) + label_note = from_event(events.first, resource: resource, resource_parent: resource_parent) + label_note.events = events - LabelNote.new(attrs) + label_note end def events=(events) @@ -37,8 +44,8 @@ class LabelNote < SyntheticNote end def note_text(html: false) - added = labels_str(label_refs_by_action('add', html), prefix: 'added', suffix: added_suffix) - removed = labels_str(label_refs_by_action('remove', html), prefix: removed_prefix) + added = labels_str(label_refs_by_action('add', html).uniq, prefix: 'added', suffix: added_suffix) + removed = labels_str(label_refs_by_action('remove', html).uniq, prefix: removed_prefix) [added, removed].compact.join(' and ') end diff --git a/app/models/member.rb b/app/models/member.rb index 107530daf51..ecf9013f197 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -386,10 +386,10 @@ class Member < ApplicationRecord user.present? end - def accept_request + def accept_request(current_user) return false unless request? - updated = self.update(requested_at: nil) + updated = self.update(requested_at: nil, created_by: current_user) after_accept_request if updated updated @@ -531,7 +531,7 @@ class Member < ApplicationRecord def send_request notification_service.new_access_request(self) - todo_service.create_member_access_request(self) if source_type != 'Project' + todo_service.create_member_access_request_todos(self) end def post_create_hook diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb index e9d7b1d3f80..36cbc97d049 100644 --- a/app/models/members/member_role.rb +++ b/app/models/members/member_role.rb @@ -11,6 +11,7 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass validates :base_access_level, presence: true validate :belongs_to_top_level_namespace validate :validate_namespace_locked, on: :update + validate :attributes_locked_after_member_associated, on: :update validates_associated :members @@ -27,4 +28,11 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass errors.add(:namespace, s_("MemberRole|can't be changed")) end + + def attributes_locked_after_member_associated + return unless members.present? + + errors.add(:base, s_("MemberRole|cannot be changed because it is already assigned to a user. "\ + "Please create a new Member Role instead")) + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 78c6d983a3d..0012f098ab2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -193,6 +193,12 @@ class MergeRequest < ApplicationRecord merge_request.merge_error = nil end + before_transition any => :merged do |merge_request| + if ::Feature.enabled?(:reset_merge_error_on_transition, merge_request.project) + merge_request.merge_error = nil + end + end + after_transition any => :opened do |merge_request| merge_request.run_after_commit do UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) @@ -436,6 +442,14 @@ class MergeRequest < ApplicationRecord ) end + scope :without_hidden, -> { + if Feature.enabled?(:hide_merge_requests_from_banned_users) + where_not_exists(Users::BannedUser.where('merge_requests.author_id = banned_users.user_id')) + else + all + end + } + def self.total_time_to_merge join_metrics .merge(MergeRequest::Metrics.with_valid_time_to_merge) @@ -2001,6 +2015,10 @@ class MergeRequest < ApplicationRecord false # overridden in EE end + def hidden? + Feature.enabled?(:hide_merge_requests_from_banned_users) && author&.banned? + end + private attr_accessor :skip_fetch_ref diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index cff8911d84b..1395b8ff162 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -392,8 +392,13 @@ class MergeRequestDiff < ApplicationRecord def diffs_in_batch(batch_page, batch_size, diff_options:) fetching_repository_diffs(diff_options) do |comparison| - reorder_diff_files! - diffs_batch = diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options) + Gitlab::Metrics.measure(:diffs_reorder) do + reorder_diff_files! + end + + diffs_batch = Gitlab::Metrics.measure(:diffs_collection) do + diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options) + end if comparison if diff_options[:paths].blank? && !without_files? @@ -406,7 +411,9 @@ class MergeRequestDiff < ApplicationRecord ) end - comparison.diffs(diff_options) + Gitlab::Metrics.measure(:diffs_comparison) do + comparison.diffs(diff_options) + end else diffs_batch end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index da07d8dd9fc..b0676c25f8e 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -166,8 +166,6 @@ class Milestone < ApplicationRecord end def self.states_count(projects, groups = nil) - return STATE_COUNT_HASH unless projects || groups - counts = Milestone .for_projects_and_groups(projects, groups) .reorder(nil) diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index f24161d598f..3ea46a8b703 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -2,6 +2,8 @@ module Ml class Candidate < ApplicationRecord + PACKAGE_PREFIX = 'ml_candidate_' + enum status: { running: 0, scheduled: 1, finished: 2, failed: 3, killed: 4 } validates :iid, :experiment, presence: true @@ -16,20 +18,31 @@ module Ml attribute :iid, default: -> { SecureRandom.uuid } - scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) } + scope :including_relationships, -> { includes(:latest_metrics, :params, :user) } + + delegate :project_id, :project, to: :experiment def artifact_root "/#{package_name}/#{package_version}/" end def artifact - ::Packages::Generic::PackageFinder.new(experiment.project).execute!(package_name, package_version) - rescue ActiveRecord::RecordNotFound - nil + artifact_lazy&.itself + end + + def artifact_lazy + BatchLoader.for(id).batch do |candidate_ids, loader| + Packages::Package + .joins("INNER JOIN ml_candidates ON packages_packages.name=(concat('#{PACKAGE_PREFIX}', ml_candidates.id))") + .where(ml_candidates: { id: candidate_ids }) + .find_each do |package| + loader.call(package.name.delete_prefix(PACKAGE_PREFIX).to_i, package) + end + end end def package_name - "ml_candidate_#{iid}" + "#{PACKAGE_PREFIX}#{id}" end def package_version diff --git a/app/models/namespace.rb b/app/models/namespace.rb index d7d53956656..cf638f9b16c 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,17 +11,12 @@ class Namespace < ApplicationRecord include FeatureGate include FromUnion include Gitlab::Utils::StrongMemoize - include IgnorableColumns include Namespaces::Traversal::Recursive include Namespaces::Traversal::Linear include EachBatch include BlocksUnsafeSerialization include Ci::NamespaceSettings - # Temporary column used for back-filling project namespaces. - # Remove it once the back-filling of all project namespaces is done. - ignore_column :tmp_project_id, remove_with: '14.7', remove_after: '2022-01-22' - # Tells ActiveRecord not to store the full class name, in order to save some space # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794 self.store_full_sti_class = false @@ -33,9 +28,11 @@ class Namespace < ApplicationRecord NUMBER_OF_ANCESTORS_ALLOWED = 20 SR_DISABLED_AND_UNOVERRIDABLE = 'disabled_and_unoverridable' + # DISABLED_WITH_OVERRIDE is deprecated in favour of DISABLED_AND_OVERRIDABLE. SR_DISABLED_WITH_OVERRIDE = 'disabled_with_override' + SR_DISABLED_AND_OVERRIDABLE = 'disabled_and_overridable' SR_ENABLED = 'enabled' - SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_ENABLED].freeze + SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE, SR_ENABLED].freeze URL_MAX_LENGTH = 255 PATH_TRAILING_VIOLATIONS = %w[.git .atom .].freeze @@ -87,6 +84,7 @@ class Namespace < ApplicationRecord has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory' has_many :achievements, class_name: 'Achievements::Achievement' + has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail' validates :owner, presence: true, if: ->(n) { n.owner_required? } validates :name, @@ -134,6 +132,10 @@ class Namespace < ApplicationRecord to: :namespace_settings delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=, to: :namespace_settings + delegate :allow_runner_registration_token, + :allow_runner_registration_token?, + :allow_runner_registration_token=, + to: :namespace_settings delegate :maven_package_requests_forwarding, :pypi_package_requests_forwarding, :npm_package_requests_forwarding, @@ -556,7 +558,7 @@ class Namespace < ApplicationRecord if shared_runners_enabled SR_ENABLED elsif allow_descendants_override_disabled_shared_runners - SR_DISABLED_WITH_OVERRIDE + SR_DISABLED_AND_OVERRIDABLE else SR_DISABLED_AND_UNOVERRIDABLE end @@ -566,10 +568,10 @@ class Namespace < ApplicationRecord case other_setting when SR_ENABLED false - when SR_DISABLED_WITH_OVERRIDE + when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE shared_runners_setting == SR_ENABLED when SR_DISABLED_AND_UNOVERRIDABLE - shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE + shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_AND_OVERRIDABLE || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE else raise ArgumentError end diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 5081d5cdafe..7f65fb3a378 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -69,6 +69,12 @@ class NamespaceSetting < ApplicationRecord !self.class.where(namespace_id: namespace.ancestors, runner_registration_enabled: false).exists? end + def allow_runner_registration_token? + settings = Gitlab::CurrentSettings.current_application_settings + + settings.allow_runner_registration_token && namespace.root_ancestor.allow_runner_registration_token + end + private def all_ancestors_allow_diff_preview_in_email? diff --git a/app/models/note.rb b/app/models/note.rb index 052df6142c5..73c8e72d8b0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -125,6 +125,7 @@ class Note < ApplicationRecord scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :system, -> { where(system: true) } scope :user, -> { where(system: false) } + scope :not_internal, -> { where(internal: false) } scope :common, -> { where(noteable_type: ["", nil]) } scope :fresh, -> { order_created_asc.with_order_id_asc } scope :updated_after, ->(time) { where('updated_at > ?', time) } @@ -133,9 +134,16 @@ class Note < ApplicationRecord scope :inc_author, -> { includes(:author) } scope :inc_note_diff_file, -> { includes(:note_diff_file) } scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) } - scope :inc_relations_for_view, -> do - includes({ project: :group }, { author: :status }, :updated_by, :resolved_by, :award_emoji, - { system_note_metadata: :description_version }, :note_diff_file, :diff_note_positions, :suggestions) + scope :inc_relations_for_view, ->(noteable = nil) do + relations = [{ project: :group }, { author: :status }, :updated_by, :resolved_by, + :award_emoji, { system_note_metadata: :description_version }, :suggestions] + + if noteable.nil? || DiffNote.noteable_types.include?(noteable.class.name) || + Feature.disabled?(:skip_notes_diff_include) + relations += [:note_diff_file, :diff_note_positions] + end + + includes(relations) end scope :with_notes_filter, -> (notes_filter) do diff --git a/app/models/packages/nuget.rb b/app/models/packages/nuget.rb index 6bedd488c8a..9a9e5b6605a 100644 --- a/app/models/packages/nuget.rb +++ b/app/models/packages/nuget.rb @@ -3,6 +3,7 @@ module Packages module Nuget TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package' TEMPORARY_SYMBOL_PACKAGE_NAME = 'NuGet.Temporary.SymbolPackage' + FORMAT = 'nupkg' def self.table_name_prefix 'packages_nuget_' diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 17c5415939c..966165f9ad7 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -36,6 +36,7 @@ class Packages::Package < ApplicationRecord # TODO: put the installable default scope on the :package_files association once the dependent: :destroy is removed # See https://gitlab.com/gitlab-org/gitlab/-/issues/349191 has_many :installable_package_files, -> { installable }, class_name: 'Packages::PackageFile', inverse_of: :package + has_many :installable_nuget_package_files, -> { installable.with_nuget_format }, class_name: 'Packages::PackageFile', inverse_of: :package has_many :dependency_links, inverse_of: :package, class_name: 'Packages::DependencyLink' has_many :tags, inverse_of: :package, class_name: 'Packages::Tag' has_one :conan_metadatum, inverse_of: :package, class_name: 'Packages::Conan::Metadatum' @@ -128,6 +129,7 @@ class Packages::Package < ApplicationRecord scope :including_project_namespace_route, -> { includes(project: { namespace: :route }) } scope :including_tags, -> { includes(:tags) } scope :including_dependency_links, -> { includes(dependency_links: :dependency) } + scope :including_dependency_links_with_nuget_metadatum, -> { includes(dependency_links: [:dependency, :nuget_metadatum]) } scope :with_conan_channel, ->(package_channel) do joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel }) @@ -149,12 +151,14 @@ class Packages::Package < ApplicationRecord end scope :preload_composer, -> { preload(:composer_metadatum) } scope :preload_npm_metadatum, -> { preload(:npm_metadatum) } + scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) } scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) } scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } scope :has_version, -> { where.not(version: nil) } scope :preload_files, -> { preload(:installable_package_files) } + scope :preload_nuget_files, -> { preload(:installable_nuget_package_files) } scope :preload_pipelines, -> { preload(pipelines: :user) } scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) } scope :limit_recent, ->(limit) { order_created_desc.limit(limit) } diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index 3d56c563ec8..e1486c11298 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -44,6 +44,7 @@ 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 :with_format, ->(format) { where(::Packages::PackageFile.arel_table[:file_name].matches("%.#{format}")) } + scope :with_nuget_format, -> { with_format(Packages::Nuget::FORMAT) } scope :preload_package, -> { preload(:package) } scope :preload_pipelines, -> { preload(pipelines: :user) } diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index cf0f0f9e92f..a1ba48f3ab0 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -46,7 +46,7 @@ module Pages strong_memoize_attr :source def prefix - if project.pages_group_root? + if project.pages_namespace_url == project.pages_url '/' else project.full_path.delete_prefix(trim_prefix) + '/' diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 4e3f4b0c328..909658214fd 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -78,6 +78,10 @@ class PagesDomain < ApplicationRecord find_by("LOWER(domain) = LOWER(?)", domain) end + def self.ids_for_project(project_id) + where(project_id: project_id).ids + end + def verified? !!verified_at end @@ -209,7 +213,7 @@ class PagesDomain < ApplicationRecord return unless pages_deployed? cache = if Feature.enabled?(:cache_pages_domain_api, project.root_namespace) - ::Gitlab::Pages::CacheControl.for_project(project.id) + ::Gitlab::Pages::CacheControl.for_domain(id) end Pages::VirtualDomain.new( diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 887ef36cc17..0da205f86a5 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -21,6 +21,11 @@ class PersonalAccessToken < ApplicationRecord after_initialize :set_default_scopes, if: :persisted? before_save :ensure_token + # During the implementation of Admin Mode for API, tokens of + # administrators should automatically get the `admin_mode` scope as well + # See https://gitlab.com/gitlab-org/gitlab/-/issues/42692 + before_create :add_admin_mode_scope, if: :user_admin? + scope :active, -> { not_revoked.not_expired } scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) } scope :expired_today_and_not_notified, -> { where(["revoked = false AND expires_at = CURRENT_DATE AND after_expiry_notification_delivered = false"]) } @@ -79,7 +84,12 @@ class PersonalAccessToken < ApplicationRecord protected def validate_scopes - unless revoked || scopes.all? { |scope| Gitlab::Auth.all_available_scopes.include?(scope.to_sym) } + # During the implementation of Admin Mode for API, + # the `admin_mode` scope is not yet part of `all_available_scopes` but still valid. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/42692 + valid_scopes = Gitlab::Auth.all_available_scopes + [Gitlab::Auth::ADMIN_MODE_SCOPE] + + unless revoked || scopes.all? { |scope| valid_scopes.include?(scope.to_sym) } errors.add :scopes, "can only contain available scopes" end end @@ -91,6 +101,14 @@ class PersonalAccessToken < ApplicationRecord self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? end + + def user_admin? + user.admin? # rubocop: disable Cop/UserAdmin + end + + def add_admin_mode_scope + self.scopes += [Gitlab::Auth::ADMIN_MODE_SCOPE.to_s] + end end PersonalAccessToken.prepend_mod_with('PersonalAccessToken') diff --git a/app/models/project.rb b/app/models/project.rb index 73dbb55a07b..561a842f23a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -170,6 +170,7 @@ class Project < ApplicationRecord end # Project integrations + has_one :apple_app_store_integration, class_name: 'Integrations::AppleAppStore' has_one :asana_integration, class_name: 'Integrations::Asana' has_one :assembla_integration, class_name: 'Integrations::Assembla' has_one :bamboo_integration, class_name: 'Integrations::Bamboo' @@ -269,6 +270,7 @@ class Project < ApplicationRecord has_many :integrations has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration' + has_many :incident_hooks_integrations, -> { incident_hooks }, class_name: 'Integration' has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration' has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration' has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration' @@ -291,18 +293,24 @@ class Project < ApplicationRecord has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' + has_many :project_members, -> { where(requested_at: nil) }, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - - has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id - alias_method :members, :project_members - has_many :users, through: :project_members + has_many :namespace_members, ->(project) { where(requested_at: nil).unscope(where: %i[source_id source_type]) }, + primary_key: :project_namespace_id, foreign_key: :member_namespace_id, inverse_of: :project, class_name: 'ProjectMember' has_many :requesters, -> { where.not(requested_at: nil) }, as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :namespace_requesters, ->(project) { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) }, + primary_key: :project_namespace_id, foreign_key: :member_namespace_id, inverse_of: :project, class_name: 'ProjectMember' + has_many :members_and_requesters, as: :source, class_name: 'ProjectMember' + has_many :users, through: :project_members + + has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id + has_many :deploy_keys_projects, inverse_of: :project has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects @@ -750,16 +758,13 @@ class Project < ApplicationRecord end end - # Defines instance methods: + # Define two instance methods: # - # - only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: false) - # - allow_merge_on_skipped_pipeline?(inherit_group_setting: false) - # - only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: false) - # - only_allow_merge_if_pipeline_succeeds_locked? - # - allow_merge_on_skipped_pipeline_locked? - # - only_allow_merge_if_all_discussions_are_resolved_locked? + # - [attribute]?(inherit_group_setting) Returns the final value after inheriting the parent group + # - [attribute]_locked? Returns true if the value is inherited from the parent group + # + # These functions will be overridden in EE to make sense afterwards def self.cascading_with_parent_namespace(attribute) - # method overriden in EE define_method("#{attribute}?") do |inherit_group_setting: false| self.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend end @@ -1610,7 +1615,9 @@ class Project < ApplicationRecord end def disabled_integrations - [] + disabled_integrations = [] + disabled_integrations << 'apple_app_store' unless Feature.enabled?(:apple_app_store_integration, self) + disabled_integrations end def find_or_initialize_integration(name) @@ -1722,14 +1729,8 @@ class Project < ApplicationRecord def execute_integrations(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope run_after_commit_or_now do - if use_integration_relations? - association("#{hooks_scope}_integrations").reader.each do |integration| - integration.async_execute(data) - end - else - integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend - integration.async_execute(data) - end + association("#{hooks_scope}_integrations").reader.each do |integration| + integration.async_execute(data) end end end @@ -2100,7 +2101,7 @@ class Project < ApplicationRecord pages_metadatum&.deployed? end - def pages_group_url + def pages_namespace_url # The host in URL always needs to be downcased Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix| "#{prefix}#{pages_subdomain}." @@ -2108,19 +2109,23 @@ class Project < ApplicationRecord end def pages_url - url = pages_group_url + url = pages_namespace_url url_path = full_path.partition('/').last + namespace_url = "#{Settings.pages.protocol}://#{url_path}".downcase + + if Rails.env.development? + url_without_port = URI.parse(url) + url_without_port.port = nil + + return url if url_without_port.to_s == namespace_url + end # If the project path is the same as host, we serve it as group page - return url if url == "#{Settings.pages.protocol}://#{url_path}".downcase + return url if url == namespace_url "#{url}/#{url_path}" end - def pages_group_root? - pages_group_url == pages_url - end - def pages_subdomain full_path.partition('/').first end @@ -2920,12 +2925,6 @@ class Project < ApplicationRecord Gitlab::Routing.url_helpers.activity_project_path(self) end - def increment_statistic_value(statistic, delta) - return if pending_delete? - - ProjectStatistics.increment_statistic(self, statistic, delta) - end - def ci_forward_deployment_enabled? return false unless ci_cd_settings @@ -3369,12 +3368,6 @@ class Project < ApplicationRecord ProjectFeature::PRIVATE end end - - def use_integration_relations? - strong_memoize(:use_integration_relations) do - Feature.enabled?(:cache_project_integrations, self) - end - end end Project.prepend_mod_with('Project') diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 7116ccd9824..db86bb5e1fb 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -59,7 +59,7 @@ class ProjectSetting < ApplicationRecord !!super end end - strong_memoize_attr :show_diff_preview_in_email?, :show_diff_preview_in_email + strong_memoize_attr :show_diff_preview_in_email? private diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 506f6305791..732dadc03d9 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -123,16 +123,37 @@ class ProjectStatistics < ApplicationRecord # through counter_attribute_after_commit # # For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS - def self.increment_statistic(project, key, amount) + def self.increment_statistic(project, key, increment) + return if project.pending_delete? + + project.statistics.try do |project_statistics| + project_statistics.increment_statistic(key, increment) + end + end + + def self.bulk_increment_statistic(project, key, increments) + unless Feature.enabled?(:project_statistics_bulk_increment, type: :development) + total_amount = Gitlab::Counters::Increment.new(amount: increments.sum(&:amount)) + return increment_statistic(project, key, total_amount) + end + + return if project.pending_delete? + project.statistics.try do |project_statistics| - project_statistics.increment_statistic(key, amount) + project_statistics.bulk_increment_statistic(key, increments) end end - def increment_statistic(key, amount) + def increment_statistic(key, increment) + raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key) + + increment_counter(key, increment) + end + + def bulk_increment_statistic(key, increments) raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key) - increment_counter(key, amount) + bulk_increment_counter(key, increments) end private diff --git a/app/models/projects/branch_rule.rb b/app/models/projects/branch_rule.rb new file mode 100644 index 00000000000..ae59d24e557 --- /dev/null +++ b/app/models/projects/branch_rule.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Projects + class BranchRule + extend Forwardable + + attr_reader :project, :protected_branch + + def_delegators(:protected_branch, :name, :group, :default_branch?, :created_at, :updated_at) + + def initialize(project, protected_branch) + @protected_branch = protected_branch + @project = project + end + + def protected? + true + end + + def matching_branches_count + branch_names = project.repository.branch_names + matching_branches = protected_branch.matching(branch_names) + matching_branches.count + end + + def branch_protection + protected_branch + end + end +end + +Projects::BranchRule.prepend_mod diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb index 2ffc7478178..b791cb1254c 100644 --- a/app/models/projects/build_artifacts_size_refresh.rb +++ b/app/models/projects/build_artifacts_size_refresh.rb @@ -7,16 +7,34 @@ module Projects STALE_WINDOW = 2.hours + # This delay is set to 10 minutes to accommodate any ongoing + # deletion that might have happened. + # The delete on the database may have been committed before + # the refresh completed its batching. If the resulting decrement is + # pushed into Redis after the refresh has ended, it would result in net negative value. + # The delay is needed to ensure this negative value is ignored. + FINALIZE_DELAY = 10.minutes + self.table_name = 'project_build_artifacts_size_refreshes' + COUNTER_ATTRIBUTE_NAME = :build_artifacts_size + belongs_to :project validates :project, presence: true + # The refresh of the project statistics counter is performed in 4 stages: + # 1. created - The refresh is on the queue to be processed by Projects::RefreshBuildArtifactsSizeStatisticsWorker + # 2. running - The refresh is ongoing. The project statistics counter switches to the temporary refresh counter key. + # Counter increments are deduplicated. + # 3. pending - The refresh is pending to be picked up by Projects::RefreshBuildArtifactsSizeStatisticsWorker again. + # 4. finalizing - The refresh has finished summing existing job artifact size into the refresh counter key. + # The sum will need to be moved into the counter key. STATES = { created: 1, running: 2, - pending: 3 + pending: 3, + finalizing: 4 }.freeze state_machine :state, initial: :created do @@ -24,6 +42,7 @@ module Projects state :created, value: STATES[:created] state :running, value: STATES[:running] state :pending, value: STATES[:pending] + state :finalizing, value: STATES[:finalizing] event :process do transition [:created, :pending, :running] => :running @@ -33,7 +52,10 @@ module Projects transition running: :pending end - # set it only the first time we execute the refresh + event :schedule_finalize do + transition running: :finalizing + end + before_transition created: :running do |refresh| refresh.reset_project_statistics! refresh.refresh_started_at = Time.zone.now @@ -47,6 +69,10 @@ module Projects before_transition running: :pending do |refresh, transition| refresh.last_job_artifact_id = transition.args.first end + + before_transition running: :finalizing do |refresh, transition| + refresh.schedule_finalize_worker + end end scope :stale, -> { with_state(:running).where('updated_at < ?', STALE_WINDOW.ago) } @@ -80,7 +106,7 @@ module Projects end def reset_project_statistics! - project.statistics.reset_counter!(:build_artifacts_size) + project.statistics.initiate_refresh!(COUNTER_ATTRIBUTE_NAME) end def next_batch(limit:) @@ -95,6 +121,18 @@ module Projects !created? end + def finalize! + project.statistics.finalize_refresh(COUNTER_ATTRIBUTE_NAME) + + destroy! + end + + def schedule_finalize_worker + run_after_commit do + Projects::FinalizeProjectStatisticsRefreshWorker.perform_in(FINALIZE_DELAY, self.class.to_s, id) + end + end + private def schedule_namespace_aggregation_worker diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index c59ef4cd80b..050db3b6870 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -8,11 +8,9 @@ class ProtectedBranch < ApplicationRecord validate :validate_either_project_or_top_group - scope :requiring_code_owner_approval, - -> { where(code_owner_approval_required: true) } - - scope :allowing_force_push, - -> { where(allow_force_push: true) } + scope :requiring_code_owner_approval, -> { where(code_owner_approval_required: true) } + scope :allowing_force_push, -> { where(allow_force_push: true) } + scope :sorted_by_name, -> { order(name: :asc) } protected_ref_access_levels :merge, :push @@ -106,6 +104,10 @@ class ProtectedBranch < ApplicationRecord name == project.default_branch end + def entity + group || project + end + private def validate_either_project_or_top_group @@ -113,7 +115,7 @@ class ProtectedBranch < ApplicationRecord errors.add(:base, _('must be associated with a Group or a Project')) elsif project && group errors.add(:base, _('cannot be associated with both a Group and a Project')) - elsif group && group.root_ancestor != group + elsif group && group.subgroup? errors.add(:base, _('cannot be associated with a subgroup')) end end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index df75c557717..76e620aa3bf 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ProtectedBranch::MergeAccessLevel < ApplicationRecord + include Importable include ProtectedBranchAccess # default value for the access_level column GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 6076fab20b7..66fe57be25f 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ProtectedBranch::PushAccessLevel < ApplicationRecord + include Importable include ProtectedBranchAccess # default value for the access_level column GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb index 9fcfa7646a2..5d8b1fb4f71 100644 --- a/app/models/protected_tag/create_access_level.rb +++ b/app/models/protected_tag/create_access_level.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ProtectedTag::CreateAccessLevel < ApplicationRecord + include Importable include ProtectedTagAccess def check_access(user) diff --git a/app/models/release.rb b/app/models/release.rb index 5ef3ff1bc6c..b770f3934ef 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -26,12 +26,13 @@ class Release < ApplicationRecord before_create :set_released_at validates :project, :tag, presence: true + validates :author_id, presence: true, if: :validate_release_with_author? + validates :tag, uniqueness: { scope: :project_id } validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed? validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] } - validates :author_id, presence: true, on: :create, if: :validate_release_with_author? scope :sorted, -> { order(released_at: :desc) } scope :preloaded, -> { diff --git a/app/models/repository.rb b/app/models/repository.rb index 90e87de4a5b..cedfed16b20 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -189,6 +189,8 @@ class Repository return [] end + query = Feature.enabled?(:commit_search_trailing_spaces) ? query.strip : query + commits = raw_repository.find_commits_by_message(query, ref, path, limit, offset).map do |c| commit(c) end @@ -631,7 +633,11 @@ class Repository end def readme_path - head_tree&.readme_path + if Feature.enabled?(:readme_from_gitaly) + readme_path_gitaly + else + head_tree&.readme_path + end end cache_method :readme_path @@ -1239,6 +1245,29 @@ class Repository container.full_path, container: container) end + + def readme_path_gitaly + return if empty? || root_ref.nil? + + # (?i) to enable case-insensitive mode + # + # Note: `Gitlab::FileDetector::PATTERNS[:readme]#to_s` won't work because of + # incompatibility of regex engines between Rails and Gitaly. + regex = "(?i)#{Gitlab::FileDetector::PATTERNS[:readme].source}" + + readmes = search_files_by_regexp(regex, root_ref) + + choose_readme_to_display(readmes) + end + + # Extracted from Tree#readme_path + def choose_readme_to_display(readmes) + previewable_readme = readmes.find { |name| Gitlab::MarkupHelper.previewable?(name) } + + return previewable_readme if previewable_readme + + readmes.find { |name| Gitlab::MarkupHelper.plain?(name) } + end end Repository.prepend_mod_with('Repository') diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb index 8b82e0f343c..551ea984132 100644 --- a/app/models/resource_event.rb +++ b/app/models/resource_event.rb @@ -3,6 +3,8 @@ class ResourceEvent < ApplicationRecord include Gitlab::Utils::StrongMemoize include Importable + include IssueResourceEvent + include WorkItemResourceEvent self.abstract_class = true @@ -18,6 +20,10 @@ class ResourceEvent < ApplicationRecord end end + def issuable + raise NoMethodError, 'must implement `issuable` method' + end + private def discussion_id_key diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index a1426540cf5..efffc1bd6dc 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -2,7 +2,6 @@ class ResourceLabelEvent < ResourceEvent include CacheMarkdownField - include IssueResourceEvent include MergeRequestResourceEvent cache_markdown_field :reference @@ -39,6 +38,10 @@ class ResourceLabelEvent < ResourceEvent issue || merge_request end + def synthetic_note_class + LabelNote + end + def project issuable.project end diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb index 5fd71612de0..def7e91af3f 100644 --- a/app/models/resource_milestone_event.rb +++ b/app/models/resource_milestone_event.rb @@ -19,4 +19,8 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent def milestone_parent milestone&.parent end + + def synthetic_note_class + MilestoneNote + end end diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb index 6ebb9d5f176..134f71e35ad 100644 --- a/app/models/resource_state_event.rb +++ b/app/models/resource_state_event.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ResourceStateEvent < ResourceEvent - include IssueResourceEvent include MergeRequestResourceEvent include Importable @@ -26,6 +25,10 @@ class ResourceStateEvent < ResourceEvent issue_id.present? end + def synthetic_note_class + StateNote + end + private def issue_usage_metrics diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb index 26bf2a225d4..dddd4d0fe84 100644 --- a/app/models/resource_timebox_event.rb +++ b/app/models/resource_timebox_event.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true class ResourceTimeboxEvent < ResourceEvent - self.abstract_class = true - - include IssueResourceEvent include MergeRequestResourceEvent include Importable + self.abstract_class = true + validate :exactly_one_issuable, unless: :importing? enum action: { diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb index a60c0d2f3bc..f88fa052665 100644 --- a/app/models/synthetic_note.rb +++ b/app/models/synthetic_note.rb @@ -14,7 +14,7 @@ class SyntheticNote < Note discussion_id: event.discussion_id, noteable: resource, event: event, - system_note_metadata: ::SystemNoteMetadata.new(action: action), + system_note_metadata: ::SystemNoteMetadata.new(action: action, id: event.discussion_id), resource_parent: resource_parent } diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 4e86036952b..36166bdbc9a 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -34,6 +34,12 @@ class SystemNoteMetadata < ApplicationRecord belongs_to :note belongs_to :description_version + delegate_missing_to :note + + def declarative_policy_delegate + note + end + def icon_types ICON_TYPES end diff --git a/app/models/timelog.rb b/app/models/timelog.rb index 7c394736560..07c61f64f29 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -35,10 +35,21 @@ class Timelog < ApplicationRecord where('spent_at <= ?', end_time) end + scope :order_scope_asc, ->(field) { order(arel_table[field].asc.nulls_last) } + scope :order_scope_desc, ->(field) { order(arel_table[field].desc.nulls_last) } + def issuable issue || merge_request end + def self.sort_by_field(field, direction) + if direction == :asc + order_scope_asc(field) + else + order_scope_desc(field) + end + end + private def issuable_id_is_present diff --git a/app/models/todo.rb b/app/models/todo.rb index 32ec4accb4b..7bbdf321269 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -204,10 +204,18 @@ class Todo < ApplicationRecord action == MEMBER_ACCESS_REQUESTED end - def access_request_url - return "" unless self.target_type == 'Namespace' + def member_access_type + target.class.name.downcase + end - Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests') + def access_request_url(only_path: false) + if target.instance_of? Group + Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests', only_path: only_path) + elsif target.instance_of? Project + Gitlab::Routing.url_helpers.project_project_members_url(self.target, tab: 'access_requests', only_path: only_path) + else + "" + end end def done? diff --git a/app/models/user.rb b/app/models/user.rb index ba3f7922c9c..da6e1abad07 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,12 +63,13 @@ class User < ApplicationRecord attribute :admin, default: false attribute :external, default: -> { Gitlab::CurrentSettings.user_default_external } attribute :can_create_group, default: -> { Gitlab::CurrentSettings.can_create_group } + attribute :private_profile, default: -> { Gitlab::CurrentSettings.user_defaults_to_private_profile } attribute :can_create_team, default: false attribute :hide_no_ssh_key, default: false attribute :hide_no_password, default: false attribute :project_view, default: :files attribute :notified_of_own_activity, default: false - attribute :preferred_language, default: -> { I18n.default_locale } + attribute :preferred_language, default: -> { Gitlab::CurrentSettings.default_preferred_language } attribute :theme_id, default: -> { gitlab_config.default_theme } attr_encrypted :otp_secret, @@ -100,6 +101,8 @@ class User < ApplicationRecord MINIMUM_DAYS_CREATED = 7 + ignore_columns %i[linkedin twitter skype website_url location organization], remove_with: '15.8', remove_after: '2023-01-22' + # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour # rubocop: disable CodeReuse/ServiceClass @@ -214,7 +217,7 @@ class User < ApplicationRecord has_many :releases, dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent + has_many :abuse_reports, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :builds, class_name: 'Ci::Build' @@ -262,8 +265,11 @@ class User < ApplicationRecord has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :resource_state_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :authored_events, class_name: 'Event', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent - - has_many :namespace_commit_emails + has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail' + has_many :user_achievements, class_name: 'Achievements::UserAchievement', inverse_of: :user + has_many :awarded_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'awarded_by_user_id', inverse_of: :awarded_by_user + has_many :revoked_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'revoked_by_user_id', inverse_of: :revoked_by_user + has_many :achievements, through: :user_achievements, class_name: 'Achievements::Achievement', inverse_of: :users # # Validations @@ -298,19 +304,15 @@ class User < ApplicationRecord validates :color_scheme_id, allow_nil: true, inclusion: { in: Gitlab::ColorSchemes.valid_ids, message: ->(*) { _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } } } - validates :website_url, allow_blank: true, url: true, if: :website_url_changed? - after_initialize :set_projects_limit before_validation :sanitize_attrs before_validation :ensure_namespace_correct after_validation :set_username_errors - before_save :default_private_profile_to_false before_save :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } before_save :ensure_namespace_correct # in case validation is skipped - before_save :ensure_user_detail_assigned after_update :username_changed_hook, if: :saved_change_to_username? after_destroy :post_destroy_hook after_destroy :remove_key_cache @@ -372,6 +374,12 @@ class User < ApplicationRecord delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true delegate :requires_credit_card_verification, :requires_credit_card_verification=, to: :user_detail, allow_nil: true + delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true + delegate :twitter, :twitter=, to: :user_detail, allow_nil: true + delegate :skype, :skype=, to: :user_detail, allow_nil: true + delegate :website_url, :website_url=, to: :user_detail, allow_nil: true + delegate :location, :location=, to: :user_detail, allow_nil: true + delegate :organization, :organization=, to: :user_detail, allow_nil: true accepts_nested_attributes_for :user_preference, update_only: true accepts_nested_attributes_for :user_detail, update_only: true @@ -531,9 +539,7 @@ class User < ApplicationRecord strip_attributes! :name def preferred_language - read_attribute('preferred_language') || - I18n.default_locale.to_s.presence_in(Gitlab::I18n.available_locales) || - default_preferred_language + read_attribute('preferred_language').presence || Gitlab::CurrentSettings.default_preferred_language end def active_for_authentication? @@ -1401,17 +1407,9 @@ class User < ApplicationRecord end def sanitize_attrs - sanitize_links sanitize_name end - def sanitize_links - %i[skype linkedin twitter].each do |attr| - value = self[attr] - self[attr] = Sanitize.clean(value) if value.present? - end - end - def sanitize_name return unless self.name @@ -1595,11 +1593,6 @@ class User < ApplicationRecord end end - # Temporary, will be removed when user_detail fields are fully migrated - def ensure_user_detail_assigned - user_detail.assign_changed_fields_from_user if UserDetail.user_fields_changed?(self) - end - def set_username_errors namespace_path_errors = self.errors.delete(:"namespace.path") @@ -1890,7 +1883,7 @@ class User < ApplicationRecord def invalidate_issue_cache_counts Rails.cache.delete(['users', id, 'assigned_open_issues_count']) - Rails.cache.delete(['users', id, 'max_assigned_open_issues_count']) if Feature.enabled?(:limit_assigned_issues_count) + Rails.cache.delete(['users', id, 'max_assigned_open_issues_count']) end def invalidate_merge_request_cache_counts @@ -2189,6 +2182,13 @@ class User < ApplicationRecord public_email.presence || _('[REDACTED]') end + def namespace_commit_email_for_project(project) + return if project.nil? + + namespace_commit_emails.find_by(namespace: project.project_namespace) || + namespace_commit_emails.find_by(namespace: project.root_namespace) + end + protected # override, from Devise::Validatable @@ -2230,11 +2230,6 @@ class User < ApplicationRecord otp_backup_codes.first.start_with?("$pbkdf2-sha512$") end - # To enable JiHu repository to modify the default language options - def default_preferred_language - 'en' - end - # rubocop: disable CodeReuse/ServiceClass def add_primary_email_to_emails! Emails::CreateService.new(self, user: self, email: self.email).execute(confirmed_at: self.confirmed_at) @@ -2299,12 +2294,6 @@ class User < ApplicationRecord ]) end - def default_private_profile_to_false - return unless private_profile_changed? && private_profile.nil? - - self.private_profile = false - end - def has_current_license? false end diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 559e93be360..4ebb8ba9f00 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -11,6 +11,9 @@ class UserCustomAttribute < ApplicationRecord scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) } scope :arkose_sessions, -> { by_key('arkose_session') } + BLOCKED_BY = 'blocked_by' + UNBLOCKED_BY = 'unblocked_by' + class << self def upsert_custom_attributes(custom_attributes) created_at = DateTime.now diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index 0570bc2f395..b6765cb0285 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -22,14 +22,10 @@ class UserDetail < ApplicationRecord validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed? before_validation :sanitize_attrs - before_save :prevent_nil_bio + before_save :prevent_nil_fields enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true - def self.user_fields_changed?(user) - (%w[linkedin skype twitter website_url location organization] & user.changed).any? - end - def sanitize_attrs %i[linkedin skype twitter website_url].each do |attr| value = self[attr] @@ -41,25 +37,16 @@ class UserDetail < ApplicationRecord end end - def assign_changed_fields_from_user - self.linkedin = trim_field(user.linkedin) if user.linkedin_changed? - self.twitter = trim_field(user.twitter) if user.twitter_changed? - self.skype = trim_field(user.skype) if user.skype_changed? - self.website_url = trim_field(user.website_url) if user.website_url_changed? - self.location = trim_field(user.location) if user.location_changed? - self.organization = trim_field(user.organization) if user.organization_changed? - end - private - def prevent_nil_bio - self.bio = '' if bio_changed? && bio.nil? - end - - def trim_field(value) - return '' unless value - - value.first(DEFAULT_FIELD_LENGTH) + def prevent_nil_fields + self.bio = '' if bio.nil? + self.linkedin = '' if linkedin.nil? + self.twitter = '' if twitter.nil? + self.skype = '' if skype.nil? + self.location = '' if location.nil? + self.organization = '' if organization.nil? + self.website_url = '' if website_url.nil? end end diff --git a/app/models/users/namespace_commit_email.rb b/app/models/users/namespace_commit_email.rb index 4ec02f12717..883b17187ca 100644 --- a/app/models/users/namespace_commit_email.rb +++ b/app/models/users/namespace_commit_email.rb @@ -9,6 +9,22 @@ module Users validates :user, presence: true validates :namespace, presence: true validates :email, presence: true - validates :user_id, uniqueness: { scope: [:namespace_id] } + validates :user, uniqueness: { scope: :namespace_id } + validate :validate_root_group + + def self.delete_for_namespace(namespace) + where(namespace: namespace).delete_all + end + + private + + def validate_root_group + # Due to the way Rails validations are invoked all at once, + # namespace sometimes won't exist when this is ran even though we have a validation for presence first. + return unless namespace&.group_namespace? + return if namespace.root? + + errors.add(:namespace, _('must be a root group.')) + end end end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 0810c520f7e..f94e831437a 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -13,6 +13,8 @@ class WorkItem < Issue has_many :child_links, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_parent_id has_many :work_item_children, through: :child_links, class_name: 'WorkItem', foreign_key: :work_item_id, source: :work_item + has_many :work_item_children_by_created_at, -> { order(:created_at) }, through: :child_links, class_name: 'WorkItem', + foreign_key: :work_item_id, source: :work_item scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) } diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb index 33857fb08c2..21e31980fda 100644 --- a/app/models/work_items/parent_link.rb +++ b/app/models/work_items/parent_link.rb @@ -2,6 +2,8 @@ module WorkItems class ParentLink < ApplicationRecord + include RelativePositioning + self.table_name = 'work_item_parent_links' MAX_CHILDREN = 100 @@ -31,6 +33,14 @@ module WorkItems link.work_item_parent.confidential? end + + def relative_positioning_query_base(parent_link) + where(work_item_parent_id: parent_link.work_item_parent_id) + end + + def relative_positioning_parent_column + :work_item_parent_id + end end private diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb index d0819076efd..ee10c631bcc 100644 --- a/app/models/work_items/widgets/hierarchy.rb +++ b/app/models/work_items/widgets/hierarchy.rb @@ -8,7 +8,7 @@ module WorkItems end def children - work_item.work_item_children + work_item.work_item_children_by_created_at end end end |