diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-14 11:41:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-14 11:41:52 +0300 |
commit | 585826cb22ecea5998a2c2a4675735c94bdeedac (patch) | |
tree | 5b05f0b30d33cef48963609e8a18a4dff260eab3 /app/models | |
parent | df221d036e5d0c6c0ee4d55b9c97f481ee05dee8 (diff) |
Add latest changes from gitlab-org/gitlab@16-6-stable-eev16.6.0-rc42
Diffstat (limited to 'app/models')
146 files changed, 1224 insertions, 633 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 872dedf07b1..de6b644c536 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -139,11 +139,11 @@ class AbuseReport < ApplicationRecord def reported_content case report_type when :issue - project.issues.iid_in(route_hash[:id]).pick(:description_html) + reported_project.issues.iid_in(route_hash[:id]).pick(:description_html) when :merge_request - project.merge_requests.iid_in(route_hash[:id]).pick(:description_html) + reported_project.merge_requests.iid_in(route_hash[:id]).pick(:description_html) when :comment - project.notes.id_in(note_id_from_url).pick(:note_html) + reported_project.notes.id_in(note_id_from_url).pick(:note_html) end end @@ -157,13 +157,19 @@ class AbuseReport < ApplicationRecord user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter) end + # createNote mutation calls noteable.project, + # which in case of abuse reports is nil + def project + nil + end + private - def project + def reported_project Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/')) end - def group + def reported_group Group.find_by_full_path(route_hash[:group_id]) end diff --git a/app/models/active_session.rb b/app/models/active_session.rb index e42f9eeef23..9756e1b7dd3 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -84,7 +84,7 @@ class ActiveSession ) Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.pipelined do |pipeline| + Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline| pipeline.setex( key_name(user.id, session_private_id), expiry, @@ -135,9 +135,15 @@ class ActiveSession redis.srem(lookup_key_name(user.id), session_ids) + session_keys = rack_session_keys(session_ids) Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.del(key_names) - redis.del(rack_session_keys(session_ids)) + if Gitlab::Redis::ClusterUtil.cluster?(redis) + Gitlab::Redis::ClusterUtil.batch_unlink(key_names, redis) + Gitlab::Redis::ClusterUtil.batch_unlink(session_keys, redis) + else + redis.del(key_names) + redis.del(session_keys) + end end end @@ -206,7 +212,13 @@ class ActiveSession session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch| Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.mget(session_keys_batch).compact.map do |raw_session| + raw_sessions = if Gitlab::Redis::ClusterUtil.cluster?(redis) + Gitlab::Redis::ClusterUtil.batch_get(session_keys_batch, redis) + else + redis.mget(session_keys_batch) + end + + raw_sessions.compact.map do |raw_session| load_raw_session(raw_session) end end @@ -249,7 +261,13 @@ class ActiveSession found = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) } - session_ids.zip(redis.mget(entry_keys)).to_h + entries = if Gitlab::Redis::ClusterUtil.cluster?(redis) + Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis) + else + redis.mget(entry_keys) + end + + session_ids.zip(entries).to_h end found.compact! @@ -258,7 +276,13 @@ class ActiveSession fallbacks = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do entry_keys = missing.map { |session_id| key_name_v1(user_id, session_id) } - missing.zip(redis.mget(entry_keys)).to_h + entries = if Gitlab::Redis::ClusterUtil.cluster?(redis) + Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis) + else + redis.mget(entry_keys) + end + + missing.zip(entries).to_h end fallbacks.merge(found.compact) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb new file mode 100644 index 00000000000..9131d8be776 --- /dev/null +++ b/app/models/activity_pub.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActivityPub + def self.table_name_prefix + "activity_pub_" + end +end diff --git a/app/models/activity_pub/releases_subscription.rb b/app/models/activity_pub/releases_subscription.rb new file mode 100644 index 00000000000..a6304f1fc35 --- /dev/null +++ b/app/models/activity_pub/releases_subscription.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module ActivityPub + class ReleasesSubscription < ApplicationRecord + belongs_to :project, optional: false + + enum :status, [:requested, :accepted], default: :requested + + attribute :payload, Gitlab::Database::Type::JsonPgSafe.new + + validates :payload, json_schema: { filename: 'activity_pub_follow_payload' }, allow_blank: true + validates :subscriber_url, presence: true, uniqueness: { case_sensitive: false, scope: :project_id }, + public_url: true + validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id }, + public_url: { allow_nil: true } + validates :shared_inbox_url, public_url: { allow_nil: true } + + def self.find_by_subscriber_url(subscriber_url) + find_by('LOWER(subscriber_url) = ?', subscriber_url.downcase) + end + end +end diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb index b8a2a271976..46dfbe9078c 100644 --- a/app/models/ai/service_access_token.rb +++ b/app/models/ai/service_access_token.rb @@ -2,11 +2,13 @@ module Ai class ServiceAccessToken < ApplicationRecord + include IgnorableColumns self.table_name = 'service_access_tokens' + ignore_column :category, remove_with: '16.8', remove_after: '2024-01-22' + scope :expired, -> { where('expires_at < :now', now: Time.current) } scope :active, -> { where('expires_at > :now', now: Time.current) } - scope :for_category, ->(category) { where(category: category) } attr_encrypted :token, mode: :per_attribute_iv, @@ -16,11 +18,5 @@ module Ai encode_iv: false validates :token, :expires_at, presence: true - - enum category: { - code_suggestions: 1 - } - - validates :category, presence: true end end diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb index 7f8c6eef704..d884932072b 100644 --- a/app/models/analytics/cycle_analytics/value_stream.rb +++ b/app/models/analytics/cycle_analytics/value_stream.rb @@ -36,6 +36,12 @@ module Analytics new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, namespace: namespace) end + def project + return unless namespace.is_a?(::Namespaces::ProjectNamespace) + + namespace.project + end + private def max_value_streams_count diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 824a2bd9fa4..8d4f50de75e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -30,7 +30,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord jitsu_project_xid jitsu_administrator_email ], remove_with: '16.5', remove_after: '2023-09-22' - ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22' + ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.10', remove_after: '2024-03-22' + + ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ @@ -91,7 +93,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize - serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize # See https://gitlab.com/gitlab-org/gitlab/-/issues/300916 serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize @@ -303,8 +304,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :repository_storages, presence: true - validate :check_repository_storages validate :check_repository_storages_weighted validates :auto_devops_domain, @@ -488,7 +487,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :invisible_captcha_enabled, inclusion: { in: [true, false], message: N_('must be a boolean value') } - validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile, + validates :invitation_flow_enforcement, :can_create_group, :allow_project_creation_for_guest_and_below, :user_defaults_to_private_profile, allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 1bd15a56de5..00b093c8ac3 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -57,6 +57,7 @@ module ApplicationSettingImplementation default_artifacts_expire_in: '30 days', default_branch_name: nil, default_branch_protection: Settings.gitlab['default_branch_protection'], + default_branch_protection_defaults: Settings.gitlab['default_branch_protection_defaults'], default_ci_config_path: nil, default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_creation: Settings.gitlab['default_project_creation'], @@ -158,7 +159,6 @@ module ApplicationSettingImplementation recaptcha_enabled: false, repository_checks_enabled: true, repository_storages_weighted: { 'default' => 100 }, - repository_storages: ['default'], require_admin_approval_after_user_signup: true, require_two_factor_authentication: false, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], @@ -433,10 +433,6 @@ module ApplicationSettingImplementation read_attribute(:asset_proxy_whitelist) end - def repository_storages - Array(read_attribute(:repository_storages)) - end - def commit_email_hostname super.presence || self.class.default_commit_email_hostname end @@ -644,12 +640,6 @@ module ApplicationSettingImplementation self.uuid = SecureRandom.uuid end - def check_repository_storages - invalid = repository_storages - Gitlab.config.repositories.storages.keys - errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless - invalid.empty? - end - def coerce_repository_storages_weighted repository_storages_weighted.transform_values!(&:to_i) end diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index 437118c36e8..a075c2f7e4f 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -124,6 +124,10 @@ class BulkImports::Entity < ApplicationRecord entity_type.pluralize end + def portable_class + entity_type.classify.constantize + end + def base_resource_url_path "/#{pluralized_name}/#{encoded_source_full_path}" end diff --git a/app/models/bulk_imports/failure.rb b/app/models/bulk_imports/failure.rb index 44d16618c77..8a6077b523c 100644 --- a/app/models/bulk_imports/failure.rb +++ b/app/models/bulk_imports/failure.rb @@ -15,6 +15,10 @@ class BulkImports::Failure < ApplicationRecord pipeline_relation || default_relation end + def exception_message=(message) + super(::Projects::ImportErrorFilter.filter_message(message).truncate(255)) + end + private def pipeline_relation diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index d0ccf5c543a..cf6401dc1da 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -114,7 +114,7 @@ module Ci project = options&.dig(:trigger, :project) next unless project - scoped_variables.to_runner_variables.yield_self do |all_variables| + scoped_variables.to_runner_variables.then do |all_variables| ::ExpandVariables.expand(project, all_variables) end end @@ -199,7 +199,7 @@ module Ci branch = options&.dig(:trigger, :branch) return unless branch - scoped_variables.to_runner_variables.yield_self do |all_variables| + scoped_variables.to_runner_variables.then do |all_variables| ::ExpandVariables.expand(branch, all_variables) end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d2cf9058976..0bb93a68470 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -392,8 +392,8 @@ module Ci name == 'pages' end - # overridden on EE - def pages_path_prefix; end + # Overriden on EE + def pages; end def runnable? true @@ -729,7 +729,7 @@ module Ci end def artifacts_expired? - artifacts_expire_at && artifacts_expire_at < Time.current + artifacts_expire_at&.past? end def artifacts_expire_in @@ -745,7 +745,7 @@ module Ci def has_expired_locked_archive_artifacts? locked_artifacts? && - artifacts_expire_at.present? && artifacts_expire_at < Time.current + artifacts_expire_at&.past? end def has_expiring_archive_artifacts? @@ -921,13 +921,25 @@ module Ci # Consider this object to have a structural integrity problems def doom! transaction do - update_columns(status: :failed, failure_reason: :data_integrity_failure) + now = Time.current + attributes = { + status: :failed, + failure_reason: :data_integrity_failure, + updated_at: now + } + attributes[:finished_at] = now unless finished_at.present? + + update_columns(attributes) all_queuing_entries.delete_all all_runtime_metadata.delete_all end deployment&.sync_status_with(self) + ::Gitlab::Ci::Pipeline::Metrics + .job_failure_reason_counter + .increment(reason: :data_integrity_failure) + Gitlab::AppLogger.info( message: 'Build doomed', class: self.class.name, diff --git a/app/models/ci/build_trace_chunks/redis_base.rb b/app/models/ci/build_trace_chunks/redis_base.rb index 3b7a844d122..5f6b5c30a6a 100644 --- a/app/models/ci/build_trace_chunks/redis_base.rb +++ b/app/models/ci/build_trace_chunks/redis_base.rb @@ -71,7 +71,11 @@ module Ci with_redis do |redis| # https://gitlab.com/gitlab-org/gitlab/-/issues/224171 Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.del(keys) + if Gitlab::Redis::ClusterUtil.cluster?(redis) + Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis) + else + redis.del(keys) + end end end end diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index c5ad3d19425..525cb08f2ca 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -33,7 +33,7 @@ module Ci return false unless archival_attempts_available? return true unless last_archival_attempt_at - last_archival_attempt_at + backoff < Time.current + (last_archival_attempt_at + backoff).past? end def archival_attempts_available? diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb index 2bc33a6f050..02593d41bc2 100644 --- a/app/models/ci/catalog/components_project.rb +++ b/app/models/ci/catalog/components_project.rb @@ -9,7 +9,8 @@ module Ci TEMPLATE_FILE = 'template.yml' TEMPLATES_DIR = 'templates' - TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$' + TEMPLATE_PATH_REGEX = '^templates\/[\w-]+(?:\/template)?\.yml$' + COMPONENTS_LIMIT = 10 ComponentData = Struct.new(:content, :path, keyword_init: true) @@ -18,8 +19,8 @@ module Ci @sha = sha end - def fetch_component_paths(sha) - project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha) + def fetch_component_paths(sha, limit: COMPONENTS_LIMIT) + project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha, limit: limit) end def extract_component_name(path) diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index c3b18af8c3f..51bd85016a5 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -3,42 +3,53 @@ module Ci module Catalog class Listing - # This class is the SSoT to displaying the list of resources in the - # CI/CD Catalog given a namespace as a scope. + # This class is the SSoT to displaying the list of resources in the CI/CD Catalog. # This model is not directly backed by a table and joins catalog resources # with projects to return relevant data. - def initialize(namespace, current_user) - raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root? - @namespace = namespace + MIN_SEARCH_LENGTH = 3 + + def initialize(current_user) @current_user = current_user end - def resources(sort: nil) + def resources(namespace: nil, sort: nil, search: nil) + relation = all_resources + relation = by_namespace(relation, namespace) + relation = by_search(relation, search) + case sort.to_s - when 'name_desc' then all_resources.order_by_name_desc - when 'name_asc' then all_resources.order_by_name_asc - when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc - when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc + when 'name_desc' then relation.order_by_name_desc + when 'name_asc' then relation.order_by_name_asc + when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc + when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc + when 'created_at_asc' then relation.order_by_created_at_asc else - all_resources.order_by_created_at_desc + relation.order_by_created_at_desc end end private - attr_reader :namespace, :current_user + attr_reader :current_user def all_resources - Ci::Catalog::Resource - .joins(:project).includes(:project) - .merge(projects_in_namespace_visible_to_user) + Ci::Catalog::Resource.joins(:project).includes(:project) + .merge(Project.public_or_visible_to_user(current_user)) + end + + def by_namespace(relation, namespace) + return relation unless namespace + raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root? + + relation.merge(Project.in_namespace(namespace.self_and_descendant_ids)) end - def projects_in_namespace_visible_to_user - Project - .in_namespace(namespace.self_and_descendant_ids) - .public_or_visible_to_user(current_user, ::Gitlab::Access::DEVELOPER) + def by_search(relation, search) + return relation unless search + return relation.none if search.length < MIN_SEARCH_LENGTH + + relation.search(search) end end end diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index 8ffc0292a69..f947c5158cf 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -8,29 +8,55 @@ module Ci # dependency on the Project model and its need to join with that table # in order to generate the CI/CD catalog. class Resource < ::ApplicationRecord + include Gitlab::SQL::Pattern + self.table_name = 'catalog_resources' belongs_to :project - has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :catalog_resource - has_many :versions, class_name: 'Ci::Catalog::Resources::Version', inverse_of: :catalog_resource + has_many :components, class_name: 'Ci::Catalog::Resources::Component', foreign_key: :catalog_resource_id, + inverse_of: :catalog_resource + has_many :versions, class_name: 'Ci::Catalog::Resources::Version', foreign_key: :catalog_resource_id, + inverse_of: :catalog_resource scope :for_projects, ->(project_ids) { where(project_id: project_ids) } + scope :search, ->(query) { fuzzy_search(query, [:name, :description], use_minimum_char_limit: false) } + scope :order_by_created_at_desc, -> { reorder(created_at: :desc) } - scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) } - scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) } + scope :order_by_created_at_asc, -> { reorder(created_at: :asc) } + scope :order_by_name_desc, -> { reorder(arel_table[:name].desc.nulls_last) } + scope :order_by_name_asc, -> { reorder(arel_table[:name].asc.nulls_last) } scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) } scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) } - delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project + delegate :avatar_path, :star_count, :forks_count, to: :project enum state: { draft: 0, published: 1 } - def versions - project.releases.order_released_desc + before_create :sync_with_project + + def unpublish! + update!(state: :draft) + end + + def publish! + update!(state: :published) + end + + def sync_with_project! + sync_with_project + save! end - def latest_version - project.releases.latest + private + + # These columns are denormalized from the `projects` table. We first sync these + # columns when the catalog resource record is created. Then any updates to the + # `projects` columns will be synced to the `catalog_resources` table by a worker + # (to be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/429376.) + def sync_with_project + self.name = project.name + self.description = project.description + self.visibility_level = project.visibility_level end end end diff --git a/app/models/ci/catalog/resources/component.rb b/app/models/ci/catalog/resources/component.rb index 7b95c14ba7e..07d5404981b 100644 --- a/app/models/ci/catalog/resources/component.rb +++ b/app/models/ci/catalog/resources/component.rb @@ -6,6 +6,8 @@ module Ci # This class represents a CI/CD Catalog resource component. # The data will be used as metadata of a component. class Component < ::ApplicationRecord + include BulkInsertSafe + self.table_name = 'catalog_resource_components' belongs_to :project, inverse_of: :ci_components diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb index 68f60e6a965..bd0ebc77a6d 100644 --- a/app/models/ci/catalog/resources/version.rb +++ b/app/models/ci/catalog/resources/version.rb @@ -6,6 +6,8 @@ module Ci # This class represents a CI/CD Catalog resource version. # Only versions which contain valid CI components are included in this table. class Version < ::ApplicationRecord + include BulkInsertableAssociations + self.table_name = 'catalog_resource_versions' belongs_to :release, inverse_of: :catalog_resource_version @@ -14,6 +16,100 @@ module Ci has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :version validates :release, :catalog_resource, :project, presence: true + + scope :for_catalog_resources, ->(catalog_resources) { where(catalog_resource_id: catalog_resources) } + scope :preloaded, -> { includes(:catalog_resource, project: [:route, { namespace: :route }], release: :author) } + + scope :order_by_created_at_asc, -> { reorder(created_at: :asc) } + scope :order_by_created_at_desc, -> { reorder(created_at: :desc) } + # After we denormalize the `released_at` column, we won't need to use `joins(:release)` and keyset_order_* + scope :order_by_released_at_asc, -> { joins(:release).keyset_order_by_released_at_asc } + scope :order_by_released_at_desc, -> { joins(:release).keyset_order_by_released_at_desc } + + delegate :name, :description, :tag, :sha, :released_at, :author_id, to: :release + + class << self + # In the future, we should support semantic versioning. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/427286 + def latest + order_by_released_at_desc.first + end + + # This query uses LATERAL JOIN to find the latest version for each catalog resource. To avoid + # joining the `catalog_resources` table, we build an in-memory table using the resource ids. + # Example: + # SELECT ... + # FROM (VALUES (CATALOG_RESOURCE_ID_1),(CATALOG_RESOURCE_ID_2)) catalog_resources (id) + # INNER JOIN LATERAL (...) + def latest_for_catalog_resources(catalog_resources) + return none if catalog_resources.empty? + + catalog_resources_table = Ci::Catalog::Resource.arel_table + catalog_resources_id_list = catalog_resources.map { |resource| "(#{resource.id})" }.join(',') + + # We need to use an alias for the `releases` table here so that it does not + # conflict with `joins(:release)` in the `order_by_released_at_*` scope. + join_query = Ci::Catalog::Resources::Version + .where(catalog_resources_table[:id].eq(arel_table[:catalog_resource_id])) + .joins("INNER JOIN releases AS rel ON rel.id = #{table_name}.release_id") + .order(Arel.sql('rel.released_at DESC')) + .limit(1) + + Ci::Catalog::Resources::Version + .from("(VALUES #{catalog_resources_id_list}) #{catalog_resources_table.name} (id)") + .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{table_name} ON TRUE") + end + + def keyset_order_by_released_at_asc + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :released_at, + column_expression: Release.arel_table[:released_at], + order_expression: Release.arel_table[:released_at].asc, + nullable: :not_nullable, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Release.arel_table[:id].asc, + nullable: :not_nullable, + distinct: true + ) + ]) + + reorder(keyset_order) + end + + def keyset_order_by_released_at_desc + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :released_at, + column_expression: Release.arel_table[:released_at], + order_expression: Release.arel_table[:released_at].desc, + nullable: :not_nullable, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Release.arel_table[:id].desc, + nullable: :not_nullable, + distinct: true + ) + ]) + + reorder(keyset_order) + end + + def order_by(order) + case order.to_s + when 'created_asc' then order_by_created_at_asc + when 'created_desc' then order_by_created_at_desc + when 'released_at_asc' then order_by_released_at_asc + else + order_by_released_at_desc + end + end + end end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 2a346f97958..fe4437a4ad6 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -306,7 +306,7 @@ module Ci end def expired? - expire_at.present? && expire_at < Time.current + expire_at.present? && expire_at.past? end def expiring? diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index f389c642fd8..17809ba20d3 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -54,6 +54,11 @@ module Ci # if the setting is disabled any project is considered to be in scope. return true unless current_project.ci_outbound_job_token_scope_enabled? + if !accessed_project.private? && + Feature.enabled?(:restrict_ci_job_token_for_public_and_internal_projects, accessed_project) + return true + end + outbound_allowlist.includes?(accessed_project) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0a876d26cc9..cf3efc5998f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -30,9 +30,11 @@ module Ci PROJECT_ROUTE_AND_NAMESPACE_ROUTE = { project: [:project_feature, :route, { namespace: :route }] }.freeze - CONFIG_EXTENSION = '.gitlab-ci.yml' - DEFAULT_CONFIG_PATH = CONFIG_EXTENSION + + DEFAULT_CONFIG_PATH = '.gitlab-ci.yml' + CANCELABLE_STATUSES = (Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze + UNLOCKABLE_STATUSES = (Ci::Pipeline.completed_statuses + [:manual]).freeze paginates_per 15 @@ -189,6 +191,7 @@ module Ci # this is needed to ensure tests to be covered transition [:running] => :running + transition [:waiting_for_callback] => :waiting_for_callback end event :request_resource do @@ -203,6 +206,10 @@ module Ci transition any - [:running] => :running end + event :wait_for_callback do + transition any - [:waiting_for_callback] => :waiting_for_callback + end + event :skip do transition any - [:skipped] => :skipped end @@ -266,6 +273,32 @@ module Ci pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end + after_transition any => UNLOCKABLE_STATUSES do |pipeline| + # This is a temporary flag that we added just in case we need to totally + # stop unlocking pipelines due to unexpected issues during rollout. + next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project) + + next unless Feature.enabled?(:ci_unlock_non_successful_pipelines, pipeline.project) + + pipeline.run_after_commit do + Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id) + end + end + + # TODO: Remove this block once we've completed roll-out of ci_unlock_non_successful_pipelines + # https://gitlab.com/gitlab-org/gitlab/-/issues/428408 + after_transition any => :success do |pipeline| + # This is a temporary flag that we added just in case we need to totally + # stop unlocking pipelines due to unexpected issues during rollout. + next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project) + + next unless Feature.disabled?(:ci_unlock_non_successful_pipelines, pipeline.project) + + pipeline.run_after_commit do + Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id) + end + end + after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline| # We wait a little bit to ensure that all Ci::BuildFinishedWorkers finish first # because this is where some metrics like code coverage is parsed and stored @@ -380,7 +413,7 @@ module Ci pipeline.run_after_commit do next if pipeline.child? - next unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true) + next unless Feature.enabled?(:widget_pipeline_pass_subscription_update, project) || project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true) pipeline.all_merge_requests.opened.each do |merge_request| GraphqlTriggers.merge_request_merge_status_updated(merge_request) @@ -389,6 +422,7 @@ module Ci end end + scope :with_unlockable_status, -> { with_status(*UNLOCKABLE_STATUSES) } scope :internal, -> { where(source: internal_sources) } scope :no_child, -> { where.not(source: :parent_pipeline) } scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) } @@ -554,7 +588,7 @@ module Ci end def self.bridgeable_statuses - ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending] + ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource waiting_for_callback preparing pending] end def self.auto_devops_pipelines_completed_total @@ -850,6 +884,7 @@ module Ci when 'created' then nil when 'waiting_for_resource' then request_resource when 'preparing' then prepare + when 'waiting_for_callback' then wait_for_callback when 'pending' then enqueue when 'running' then run when 'success' then succeed @@ -1366,11 +1401,6 @@ module Ci merge_request.merge_request_diff_for(merge_request_diff_sha) end - def reduced_build_attributes_list_for_rules? - ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project) - end - strong_memoize_attr :reduced_build_attributes_list_for_rules? - private def add_message(severity, content) diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index 8655e8eb9b8..e8ce58f2de5 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -30,15 +30,6 @@ module Ci state :fixed, value: 3 state :broken, value: 4 state :still_failing, value: 5 - - after_transition any => [:fixed, :success] do |ci_ref| - # Do not try to unlock if no artifacts are locked - next unless ci_ref.artifacts_locked? - - ci_ref.run_after_commit do - Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id) - end - end end class << self @@ -75,5 +66,13 @@ module Ci self.status_name end end + + def last_successful_ci_source_pipeline + pipelines.ci_sources.success.order(id: :desc).first + end + + def last_unlockable_ci_source_pipeline + pipelines.ci_sources.with_unlockable_status.order(id: :desc).first + end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 91c919dc662..9c30beeeb59 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -123,6 +123,8 @@ module Ci joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_id }) } + scope :with_creator_id, -> (value) { where(creator_id: value) } + scope :belonging_to_group_or_project_descendants, -> (group_id) { group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id) project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id) @@ -217,6 +219,8 @@ module Ci validate :any_project, if: :project_type? validate :exactly_one_group, if: :group_type? + scope :with_version_prefix, ->(value) { joins(:runner_managers).merge(RunnerManager.with_version_prefix(value)) } + acts_as_taggable after_destroy :cleanup_runner_queue diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb index 7d8fc097f51..e6576859827 100644 --- a/app/models/ci/runner_manager.rb +++ b/app/models/ci/runner_manager.rb @@ -62,6 +62,16 @@ module Ci scope :order_id_desc, -> { order(id: :desc) } + scope :with_version_prefix, ->(value) do + regex = version_regex_expression_for_version(value) + value += '.' if regex.end_with?('\.') && !value.end_with?('.') + substring = Arel::Nodes::NamedFunction.new('substring', [ + Ci::RunnerManager.arel_table[:version], + Arel.sql("'#{regex}'::text") + ]) + where(substring.eq(sanitize_sql_like(value))) + end + scope :with_upgrade_status, ->(upgrade_status) do joins(:runner_version).where(runner_version: { status: upgrade_status }) end @@ -137,5 +147,16 @@ module Ci Ci::Runners::ProcessRunnerVersionUpdateWorker.perform_async(new_version) end + + def self.version_regex_expression_for_version(version) + case version + when /\d+\.\d+\.\d+/ + '^\d+\.\d+\.\d+' + when /\d+\.\d+(\.)?/ + '^\d+\.\d+\.' + else + '^\d+\.' + end + end end end diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb index 5b6946b04fd..475d57ee4c8 100644 --- a/app/models/ci/sources/pipeline.rb +++ b/app/models/ci/sources/pipeline.rb @@ -12,7 +12,7 @@ module Ci :pipeline_id_convert_to_bigint, :source_pipeline_id_convert_to_bigint ], remove_with: '16.6', remove_after: '2023-10-22' - columns_changing_default :partition_id + columns_changing_default :partition_id, :source_partition_id self.table_name = "ci_sources_pipelines" diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 3a498972153..3d2df9a45ef 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -78,6 +78,10 @@ module Ci transition any - [:running] => :running end + event :wait_for_callback do + transition any - [:waiting_for_callback] => :waiting_for_callback + end + event :skip do transition any - [:skipped] => :skipped end @@ -109,6 +113,7 @@ module Ci when 'created' then nil when 'waiting_for_resource' then request_resource when 'preparing' then prepare + when 'waiting_for_callback' then wait_for_callback when 'pending' then enqueue when 'running' then run when 'success' then succeed diff --git a/app/models/commit.rb b/app/models/commit.rb index 39e12b53f21..886e6e9fbd7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -372,9 +372,7 @@ class Commit strong_memoize(:raw_signature_type) do next unless @raw.instance_of?(Gitlab::Git::Commit) - if raw_commit_from_rugged? && gpg_commit.signature_text.present? - :PGP - elsif defined? @raw.raw_commit.signature_type + if defined? @raw.raw_commit.signature_type @raw.raw_commit.signature_type end end @@ -397,10 +395,6 @@ class Commit end end - def raw_commit_from_rugged? - @raw.raw_commit.is_a?(Rugged::Commit) - end - def gpg_commit @gpg_commit ||= Gitlab::Gpg::Commit.new(self) end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3761aa81bf7..9f77bd8ebe2 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -8,20 +8,24 @@ class CommitStatus < Ci::ApplicationRecord include Presentable include BulkInsertableAssociations include TaggableQueries - - def self.switch_table_names - if Gitlab::Utils.to_boolean(ENV['USE_CI_BUILDS_ROUTING_TABLE']) - :p_ci_builds - else - :ci_builds - end - end - - self.table_name = self.switch_table_names + include IgnorableColumns + + ignore_columns %i[ + auto_canceled_by_id_convert_to_bigint + commit_id_convert_to_bigint + erased_by_id_convert_to_bigint + project_id_convert_to_bigint + runner_id_convert_to_bigint + trigger_request_id_convert_to_bigint + upstream_pipeline_id_convert_to_bigint + user_id_convert_to_bigint + ], remove_with: '17.0', remove_after: '2024-04-22' + + self.table_name = :p_ci_builds self.sequence_name = :ci_builds_id_seq self.primary_key = :id - partitionable scope: :pipeline + partitionable scope: :pipeline, partitioned: true belongs_to :user belongs_to :project @@ -155,15 +159,15 @@ class CommitStatus < Ci::ApplicationRecord end event :drop do - transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :failed + transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :failed end event :success do - transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success + transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running] => :success end event :cancel do - transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :canceled + transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :canceled end before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status| diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb index 1d9cf5729cd..dfcc905b3c3 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Analytics module CycleAnalytics module StageEventModel diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb index 1132e4e79ac..1646ed3dc7c 100644 --- a/app/models/concerns/can_move_repository_storage.rb +++ b/app/models/concerns/can_move_repository_storage.rb @@ -9,6 +9,9 @@ module CanMoveRepositoryStorage # progress beforehand. Setting a repository read-only will fail if it is # already in that state. # + # It is assumed that `with_lock` is used here to ensure that no race condition + # appears between reading and writing the read-only column. + # # @return nil. Failures will raise an exception def set_repository_read_only!(skip_git_transfer_check: false) with_lock do @@ -16,10 +19,10 @@ module CanMoveRepositoryStorage !skip_git_transfer_check && git_transfer_in_progress? raise RepositoryReadOnlyError, _('Repository already read-only') if - _safe_read_repository_read_only_column + safe_read_repository_read_only_column raise ActiveRecord::RecordNotSaved, _('Database update failed') unless - _update_repository_read_only_column(true) + update_repository_read_only_column(true) nil end @@ -28,12 +31,8 @@ module CanMoveRepositoryStorage # Set repository as writable again. Unlike setting it read-only, this will # succeed if the repository is already writable. def set_repository_writable! - with_lock do - raise ActiveRecord::RecordNotSaved, _('Database update failed') unless - _update_repository_read_only_column(false) - - nil - end + raise ActiveRecord::RecordNotSaved, _('Database update failed') unless + update_repository_read_only_column(false) end def git_transfer_in_progress? @@ -49,13 +48,13 @@ module CanMoveRepositoryStorage # Not all resources that can move repositories have the `repository_read_only` # in their table, for example groups. We need these methods to override the # behavior in those classes in order to access the column. - def _safe_read_repository_read_only_column + def safe_read_repository_read_only_column # This was added originally this way because of # https://gitlab.com/gitlab-org/gitlab/-/commit/43f9b98302d3985312c9f8b66018e2835d8293d2 self.class.where(id: id).pick(:repository_read_only) end - def _update_repository_read_only_column(value) + def update_repository_read_only_column(value) update_column(:repository_read_only, value) end end diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb index 2971ecb04b8..fb2b12e5f00 100644 --- a/app/models/concerns/ci/has_status.rb +++ b/app/models/concerns/ci/has_status.rb @@ -6,19 +6,20 @@ module Ci DEFAULT_STATUS = 'created' BLOCKED_STATUS = %w[manual scheduled].freeze - AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze + AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceled skipped manual scheduled].freeze STARTED_STATUSES = %w[running success failed].freeze - ACTIVE_STATUSES = %w[waiting_for_resource preparing pending running].freeze + ACTIVE_STATUSES = %w[waiting_for_resource preparing waiting_for_callback pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze STOPPED_STATUSES = COMPLETED_STATUSES + BLOCKED_STATUS - ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze + ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceled success skipped created].freeze PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze IGNORED_STATUSES = %w[manual].to_set.freeze ALIVE_STATUSES = (ACTIVE_STATUSES + ['created']).freeze CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled']).freeze STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7, - scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze + scheduled: 8, preparing: 9, waiting_for_resource: 10, + waiting_for_callback: 11 }.freeze UnknownStatusError = Class.new(StandardError) @@ -58,6 +59,7 @@ module Ci state :created, value: 'created' state :waiting_for_resource, value: 'waiting_for_resource' state :preparing, value: 'preparing' + state :waiting_for_callback, value: 'waiting_for_callback' state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' @@ -72,6 +74,7 @@ module Ci scope :waiting_for_resource, -> { with_status(:waiting_for_resource) } scope :preparing, -> { with_status(:preparing) } scope :relevant, -> { without_status(:created) } + scope :waiting_for_callback, -> { with_status(:waiting_for_callback) } scope :running, -> { with_status(:running) } scope :pending, -> { with_status(:pending) } scope :success, -> { with_status(:success) } diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb index 5bdf6bb31bf..201994cb321 100644 --- a/app/models/concerns/commit_signature.rb +++ b/app/models/concerns/commit_signature.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module CommitSignature extend ActiveSupport::Concern diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb index 2f64129b65f..e799127d69a 100644 --- a/app/models/concerns/diff_positionable_note.rb +++ b/app/models/concerns/diff_positionable_note.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module DiffPositionableNote extend ActiveSupport::Concern diff --git a/app/models/concerns/enums/package_metadata.rb b/app/models/concerns/enums/package_metadata.rb index 3f107987ef6..352eb41829b 100644 --- a/app/models/concerns/enums/package_metadata.rb +++ b/app/models/concerns/enums/package_metadata.rb @@ -14,7 +14,8 @@ module Enums apk: 9, rpm: 10, deb: 11, - cbl_mariner: 12 + 'cbl-mariner': 12, + wolfi: 13 }.with_indifferent_access.freeze ADVISORY_SOURCES = { diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb index 59aafc32d94..af8e37b4248 100644 --- a/app/models/concerns/enums/sbom.rb +++ b/app/models/concerns/enums/sbom.rb @@ -18,7 +18,8 @@ module Enums apk: 9, rpm: 10, deb: 11, - cbl_mariner: 12 + 'cbl-mariner': 12, + wolfi: 13 }.with_indifferent_access.freeze def self.component_types diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb index 412b1da55da..e4ee6e7e58e 100644 --- a/app/models/concerns/merge_request_reviewer_state.rb +++ b/app/models/concerns/merge_request_reviewer_state.rb @@ -6,7 +6,8 @@ module MergeRequestReviewerState included do enum state: { unreviewed: 0, - reviewed: 1 + reviewed: 1, + requested_changes: 2 } validates :state, diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb index 77edabb9706..b1dbebff4fb 100644 --- a/app/models/concerns/repository_storage_movable.rb +++ b/app/models/concerns/repository_storage_movable.rb @@ -6,6 +6,9 @@ module RepositoryStorageMovable included do scope :order_created_at_desc, -> { order(created_at: :desc) } + scope :scheduled_or_started, -> do + where(state: [state_machine.states[:scheduled].value, state_machine.states[:started].value]) + end validates :container, presence: true validates :state, presence: true @@ -43,6 +46,8 @@ module RepositoryStorageMovable transition replicated: :cleanup_failed end + # An after_transition can't affect the success of the transition. + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45160#note_431071664 around_transition initial: :scheduled do |storage_move, block| block.call @@ -61,13 +66,9 @@ module RepositoryStorageMovable true end - before_transition started: :replicated do |storage_move| + after_transition started: :replicated do |storage_move| storage_move.container.set_repository_writable! - storage_move.update_repository_storage(storage_move.destination_storage_name) - end - - after_transition started: :replicated do |storage_move| # We have several scripts in place that replicate some statistics information # to other databases. Some of them depend on the updated_at column # to identify the models they need to extract. @@ -83,6 +84,13 @@ module RepositoryStorageMovable storage_move.container.set_repository_writable! end + # This callback ensures the repository is set to writable in the event of + # a connection error during the :started -> :replicated transition + # https://gitlab.com/gitlab-org/gitlab/-/issues/427254#note_1636072125 + before_transition replicated: :cleanup_failed do |storage_move| + storage_move.container.set_repository_writable! + end + state :initial, value: 1 state :scheduled, value: 2 state :started, value: 3 @@ -93,15 +101,6 @@ module RepositoryStorageMovable end end - # Projects, snippets, and group wikis has different db structure. In projects, - # we need to update some columns in this step, but we don't with the other resources. - # - # Therefore, we create this No-op method for snippets and wikis and let project - # overwrite it in their implementation. - def update_repository_storage(new_storage) - # No-op - end - def schedule_repository_storage_update_worker raise NotImplementedError end diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb index 6af9ede5e8b..87b62214529 100644 --- a/app/models/concerns/restricted_signup.rb +++ b/app/models/concerns/restricted_signup.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module RestrictedSignup extend ActiveSupport::Concern diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index d0085b60d98..b25ee434484 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -65,7 +65,7 @@ module TokenAuthenticatableStrategies return false unless expirable? && token_expiration_enforced? exp = expires_at(instance) - !!exp && Time.current > exp + !!exp && exp.past? end def expirable? diff --git a/app/models/concerns/use_sql_function_for_primary_key_lookups.rb b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb new file mode 100644 index 00000000000..c3ca3cfc038 --- /dev/null +++ b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module UseSqlFunctionForPrimaryKeyLookups + extend ActiveSupport::Concern + + class_methods do + def find(*args) + return super unless Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request) + return super unless args.one? + return super if block_given? || primary_key.nil? || scope_attributes? + + return_array = false + id = args.first + + if id.is_a?(Array) + return super if id.many? + + return_array = true + + id = id.first + end + + return super if id.nil? || (id.is_a?(String) && !id.number?) + + from_clause = "find_#{table_name}_by_id(?) #{quoted_table_name}" + filter_empty_row = "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IS NOT NULL" + query = from(from_clause).where(filter_empty_row).limit(1).to_sql + # Using find_by_sql so we get query cache working + record = find_by_sql([query, id]).first + + unless record + message = "Couldn't find #{name} with '#{primary_key}'=#{id}" + raise(ActiveRecord::RecordNotFound.new(message, name, primary_key, id)) + end + + return_array ? [record] : record + end + end +end diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb index cb8e5fdc682..029d60d61ee 100644 --- a/app/models/concerns/users/visitable.rb +++ b/app/models/concerns/users/visitable.rb @@ -13,6 +13,45 @@ module Users time = time.to_datetime where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes)) end + + scope :for_user, ->(user_id) { where(user_id: user_id) } + + scope :recently_visited, -> do + where('visited_at > ?', 3.months.ago) + .where('visited_at <= ?', Time.current) + end + + def self.grouped_by_week_start_and_entity_for_user(user_id:) + recently_visited + .for_user(user_id) + .group(:week_start, :entity_id) + .select( + :entity_id, + "COUNT(entity_id) AS week_count", + "DATE_TRUNC('week', visited_at)::date AS week_start", + "DENSE_RANK() OVER (ORDER BY DATE_TRUNC('week', visited_at)::date)" + ) + end + + def self.frecent_visits_scores(user_id:, limit:) + ranked_entity_visits_query = grouped_by_week_start_and_entity_for_user(user_id: user_id).to_sql + sql = <<~SQL + SELECT + entity_id, + SUM(week_count * dense_rank) AS score + FROM + (#{ranked_entity_visits_query}) as ranked_entity_visits + GROUP BY + entity_id + ORDER BY + score DESC + LIMIT #{limit} + SQL + + ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do + connection.execute(sql).to_a + end + end end end end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 6a52f6a0112..15ed517dc12 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -482,6 +482,24 @@ class ContainerRepository < ApplicationRecord raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES end + def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100) + raise ArgumentError, 'not a migrated repository' unless migrated? + + page = gitlab_api_client.tags( + self.path, + page_size: page_size, + before: before, + last: last, + sort: sort, + name: name + ) + + { + tags: transform_tags_page(page[:response_body]), + pagination: page[:pagination] + } + end + def tags_count return 0 unless manifest && manifest['tags'] @@ -505,15 +523,11 @@ class ContainerRepository < ApplicationRecord digests = tags.map { |tag| tag.digest }.compact.to_set - digests.map { |digest| delete_tag_by_digest(digest) }.all? - end - - def delete_tag_by_digest(digest) - client.delete_repository_tag_by_digest(self.path, digest) + digests.map { |digest| delete_tag(digest) }.all? end - def delete_tag_by_name(name) - client.delete_repository_tag_by_name(self.path, name) + def delete_tag(name_or_digest) + client.delete_repository_tag_by_digest(self.path, name_or_digest) end def start_expiration_policy! @@ -640,6 +654,9 @@ class ContainerRepository < ApplicationRecord tag = ContainerRegistry::Tag.new(self, raw_tag['name']) tag.force_created_at_from_iso8601(raw_tag['created_at']) tag.updated_at = raw_tag['updated_at'] + tag.total_size = raw_tag['size_bytes'] + tag.manifest_digest = raw_tag['digest'] + tag.revision = raw_tag['config_digest'].to_s.split(':')[1] tag end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 0bdce18bab5..f0093445ba8 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -8,12 +8,15 @@ class Deployment < ApplicationRecord include Importable include Gitlab::Utils::StrongMemoize include FastDestroyAll + include IgnorableColumns StatusUpdateError = Class.new(StandardError) StatusSyncError = Class.new(StandardError) ARCHIVABLE_OFFSET = 50_000 + ignore_column :cluster_id, remove_with: '16.8', remove_after: '2023-12-21' + belongs_to :project, optional: false belongs_to :environment, optional: false belongs_to :user diff --git a/app/models/environment.rb b/app/models/environment.rb index efdcf7174aa..4f76fae24eb 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -8,6 +8,8 @@ class Environment < ApplicationRecord include NullifyIfBlank include FromUnion + LONG_STOP = 1.week + self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 55.seconds self.reactive_cache_hard_limit = 10.megabytes @@ -89,6 +91,7 @@ class Environment < ApplicationRecord delegate :auto_rollback_enabled?, to: :project scope :available, -> { with_state(:available) } + scope :active, -> { with_state(:available, :stopping) } scope :stopped, -> { with_state(:stopped) } scope :order_by_last_deployed_at, -> do @@ -104,6 +107,7 @@ class Environment < ApplicationRecord scope :preload_project, -> { preload(:project) } 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 :long_stopping, -> { with_state(:stopping).where('updated_at < ?', LONG_STOP.ago) } scope :deployed_and_updated_before, -> (project_id, before) do # this query joins deployments and filters out any environment that has recent deployments @@ -322,6 +326,10 @@ class Environment < ApplicationRecord last_deployment.try(:created_at) end + def long_stopping? + stopping? && self.updated_at < LONG_STOP.ago + end + def ref_path "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}" end diff --git a/app/models/group.rb b/app/models/group.rb index c83dd24e98e..51c26767569 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -300,14 +300,15 @@ class Group < Namespace groups.drop(1).each { |group| group.root_ancestor = root } end - # Returns the ids of the passed group models where the `emails_disabled` - # column is set to true anywhere in the ancestor hierarchy. + # Returns the ids of the passed group models where the `emails_enabled` + # column is set to false anywhere in the ancestor hierarchy. def ids_with_disabled_email(groups) inner_groups = Group.where('id = namespaces_with_emails_disabled.id') inner_query = inner_groups .self_and_ancestors - .where(emails_disabled: true) + .joins(:namespace_settings) + .where(namespace_settings: { emails_enabled: false }) .select('1') .limit(1) @@ -593,40 +594,13 @@ class Group < Namespace end def authorizable_members_with_parents - source_ids = - if has_parent? - self_and_ancestors.reorder(nil).select(:id) - else - id - end - - group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list) - - GroupMember.from_union([group_hierarchy_members, - members_from_self_and_ancestor_group_shares]).authorizable + Members::MembersWithParents.new(self).all_members.authorizable end def members_with_parents(only_active_users: true) - # Avoids an unnecessary SELECT when the group has no parents - source_ids = - if has_parent? - self_and_ancestors.reorder(nil).select(:id) - else - id - end - - group_hierarchy_members = GroupMember.non_minimal_access - .where(source_id: source_ids) - .select(*GroupMember.cached_column_list) - - group_hierarchy_members = if only_active_users - group_hierarchy_members.active_without_invites_and_requests - else - group_hierarchy_members.without_invites_and_requests - end - - GroupMember.from_union([group_hierarchy_members, - members_from_self_and_ancestor_group_shares]) + Members::MembersWithParents + .new(self) + .members(active_users: only_active_users) end def members_from_self_and_ancestors_with_effective_access_level @@ -671,15 +645,6 @@ class Group < Namespace members.count end - # Returns all users that are members of projects - # belonging to the current group or sub-groups - def project_users_with_descendants - User - .joins(projects: :group) - .where(namespaces: { id: self_and_descendants.select(:id) }) - .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") - end - # Return the highest access level for a user # # A special case is handled here when the user is a GitLab admin @@ -996,48 +961,6 @@ class Group < Namespace errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group')) end - def members_from_self_and_ancestor_group_shares - group_group_link_table = GroupGroupLink.arel_table - group_member_table = GroupMember.arel_table - - source_ids = - if has_parent? - self_and_ancestors.reorder(nil).select(:id) - else - id - end - - group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids) - cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query) - cte_alias = cte.table.alias(GroupGroupLink.table_name) - - # Instead of members.access_level, we need to maximize that access_level at - # the respective group_group_links.group_access. - member_columns = GroupMember.attribute_names.map do |column_name| - if column_name == 'access_level' - smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level') - else - group_member_table[column_name] - end - end - - GroupMember - .with(cte.to_arel) - .select(*member_columns) - .from([group_member_table, cte.alias_to(group_group_link_table)]) - .where(group_member_table[:requested_at].eq(nil)) - .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id])) - .where(group_member_table[:source_type].eq('Namespace')) - .where(group_member_table[:state].eq(::Member::STATE_ACTIVE)) - .non_minimal_access - end - - def smallest_value_arel(args, column_alias) - Arel::Nodes::As.new( - Arel::Nodes::NamedFunction.new('LEAST', args), - Arel::Nodes::SqlLiteral.new(column_alias)) - end - def runners_token_prefix RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX end diff --git a/app/models/guest.rb b/app/models/guest.rb deleted file mode 100644 index 9c8097e1ac8..00000000000 --- a/app/models/guest.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class Guest - class << self - def can?(action, subject = :global) - Ability.allowed?(nil, action, subject) - end - end -end diff --git a/app/models/integration.rb b/app/models/integration.rb index b4408301c6d..7c14c1b1716 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -237,6 +237,18 @@ class Integration < ApplicationRecord end private_class_method :boolean_accessor + def self.title + raise NotImplementedError + end + + def self.description + raise NotImplementedError + end + + def self.help + # no-op + end + def self.to_param raise NotImplementedError end @@ -447,19 +459,18 @@ class Integration < ApplicationRecord end def title - # implement inside child + self.class.title end def description - # implement inside child + self.class.description end def help - # implement inside child + self.class.help end def to_param - # implement inside child self.class.to_param end @@ -588,7 +599,7 @@ class Integration < ApplicationRecord return if ::Gitlab::SilentMode.enabled? return unless supported_events.include?(data[:object_kind]) - Integrations::ExecuteWorker.perform_async(id, data) + Integrations::ExecuteWorker.perform_async(id, data.deep_stringify_keys) end # override if needed diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index ef12fc6bf6f..f8fddf8a457 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -37,15 +37,15 @@ module Integrations title: -> { s_('AppleAppStore|Protected branches and tags only') }, checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') } - def title + def self.title 'Apple App Store Connect' end - def description + def self.description s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.') end - def help + def self.help variable_list = [ '<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>', '<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>', diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb index 77555996cd9..39407acd6c9 100644 --- a/app/models/integrations/asana.rb +++ b/app/models/integrations/asana.rb @@ -20,15 +20,15 @@ module Integrations title: -> { s_('Integrations|Restrict to branch (optional)') }, help: -> { s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') } - def title + def self.title 'Asana' end - def description + def self.description s_('AsanaService|Add commit messages as comments to Asana tasks.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer' s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb index 1d3616b4c3b..bbdd0e183f2 100644 --- a/app/models/integrations/assembla.rb +++ b/app/models/integrations/assembla.rb @@ -15,11 +15,11 @@ module Integrations exposes_secrets: true, placeholder: '' - def title + def self.title 'Assembla' end - def description + def self.description _('Manage projects.') end diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index 9f15532a0b0..9fe73f86be3 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -38,15 +38,15 @@ module Integrations attr_accessor :response - def title + def self.title s_('BambooService|Atlassian Bamboo') end - def description + def self.description s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index b75801335bd..167bc210349 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -136,10 +136,6 @@ module Integrations raise NotImplementedError end - def help - raise NotImplementedError - end - # With some integrations the webhook is already tied to a specific channel, # for others the channels are configurable for each event. def configurable_channels? diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index 09a0c9ba361..33dd9d9d387 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -36,7 +36,7 @@ module Integrations true end - def help + def self.help # noop end diff --git a/app/models/integrations/bugzilla.rb b/app/models/integrations/bugzilla.rb index 74e282f6848..3ca348e42a1 100644 --- a/app/models/integrations/bugzilla.rb +++ b/app/models/integrations/bugzilla.rb @@ -6,15 +6,15 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def title + def self.title 'Bugzilla' end - def description + def self.description s_("IssueTracker|Use Bugzilla as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer' s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index 82a5142e8c2..aab0cdf2134 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -75,20 +75,20 @@ module Integrations "#{project_url}/builds?commit=#{sha}" end - def title + def self.title 'Buildkite' end - def description + def self.description 'Run CI/CD pipelines with Buildkite.' end - def self.to_param - 'buildkite' + def self.help + s_('ProjectService|Run CI/CD pipelines with Buildkite.') end - def help - s_('ProjectService|Run CI/CD pipelines with Buildkite.') + def self.to_param + 'buildkite' end def calculate_reactive_cache(sha, ref) diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb index 8b5797a9d24..18268ed18f4 100644 --- a/app/models/integrations/campfire.rb +++ b/app/models/integrations/campfire.rb @@ -36,15 +36,15 @@ module Integrations placeholder: '123456', help: -> { s_('CampfireService|From the end of the room URL.') } - def title + def self.title 'Campfire' end - def description + def self.description 'Send notifications about push events to Campfire chat rooms.' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'), diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb index 7cc05d41e14..25287b53300 100644 --- a/app/models/integrations/clickup.rb +++ b/app/models/integrations/clickup.rb @@ -10,15 +10,15 @@ module Integrations @reference_pattern ||= /((#|CU-)(?<issue>[a-z0-9]+)|(?<issue>[A-Z0-9_]{2,10}-\d+))\b/ end - def title + def self.title 'ClickUp' end - def description + def self.description s_("IssueTracker|Use Clickup as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/clickup'), target: '_blank', diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb index eda8c37fc72..f97f1fd25c9 100644 --- a/app/models/integrations/confluence.rb +++ b/app/models/integrations/confluence.rb @@ -22,11 +22,11 @@ module Integrations 'confluence' end - def title + def self.title s_('ConfluenceService|Confluence Workspace') end - def description + def self.description s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.') end diff --git a/app/models/integrations/custom_issue_tracker.rb b/app/models/integrations/custom_issue_tracker.rb index 3770e813eaa..fe0d01d60bd 100644 --- a/app/models/integrations/custom_issue_tracker.rb +++ b/app/models/integrations/custom_issue_tracker.rb @@ -6,15 +6,15 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def title + def self.title s_('IssueTracker|Custom issue tracker') end - def description + def self.description s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer' s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index b1f1361afcd..5682fc2b139 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -117,15 +117,15 @@ module Integrations # archive_trace is opt-in but we handle it with a more detailed field below end - def title + def self.title 'Datadog' end - def description + def self.description s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( s_('DatadogIntegration|How do I set up this integration?'), Rails.application.routes.url_helpers.help_page_url('integration/datadog'), diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index 33b2b52fa62..7ce597389f0 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -21,23 +21,23 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title s_("DiscordService|Discord Notifications") end - def description + def self.description s_("DiscordService|Send notifications about project events to a Discord channel.") end - def self.to_param - "discord" - end - - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer' s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end + def self.to_param + "discord" + end + def default_channel_placeholder s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)') end diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index f6a12c4bb1a..b59e504c98f 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -87,20 +87,20 @@ module Integrations "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}") end - def title + def self.title 'Drone' end - def description + def self.description s_('ProjectService|Run CI/CD pipelines with Drone.') end - def self.to_param - 'drone_ci' + def self.help + s_('ProjectService|Run CI/CD pipelines with Drone.') end - def help - s_('ProjectService|Run CI/CD pipelines with Drone.') + def self.to_param + 'drone_ci' end override :hook_url diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb index 144d1a07b04..77be8f5db45 100644 --- a/app/models/integrations/emails_on_push.rb +++ b/app/models/integrations/emails_on_push.rb @@ -39,11 +39,11 @@ module Integrations recipients.split.grep(Devise.email_regexp).uniq(&:downcase) end - def title + def self.title s_('EmailsOnPushService|Emails on push') end - def description + def self.description s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.') end diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb index 003c896704a..9d6f4c2a56c 100644 --- a/app/models/integrations/ewm.rb +++ b/app/models/integrations/ewm.rb @@ -10,15 +10,15 @@ module Integrations @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i end - def title + def self.title 'EWM' end - def description + def self.description s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer' s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb index acacab2528e..7408f86d231 100644 --- a/app/models/integrations/external_wiki.rb +++ b/app/models/integrations/external_wiki.rb @@ -11,24 +11,24 @@ module Integrations help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') }, required: true - def title + def self.title s_('ExternalWikiService|External wiki') end - def description + def self.description s_('ExternalWikiService|Link to an external wiki from the sidebar.') end - def self.to_param - 'external_wiki' - end - - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer' s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end + def self.to_param + 'external_wiki' + end + def sections [ { diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb index 2d520eaf7e7..d008a28a226 100644 --- a/app/models/integrations/gitlab_slack_application.rb +++ b/app/models/integrations/gitlab_slack_application.rb @@ -26,11 +26,11 @@ module Integrations update(active: !!slack_integration) end - def title + def self.title s_('Integrations|GitLab for Slack app') end - def description + def self.description s_('Integrations|Enable slash commands and notifications for a Slack workspace.') end diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb index 5389e8dfa81..746f68fdc4c 100644 --- a/app/models/integrations/google_play.rb +++ b/app/models/integrations/google_play.rb @@ -32,15 +32,15 @@ module Integrations title: -> { s_('GooglePlayStore|Protected branches and tags only') }, checkbox_label: -> { s_('GooglePlayStore|Only set variables on protected branches and tags') } - def title + def self.title s_('GooglePlay|Google Play') end - def description + def self.description s_('GooglePlay|Use GitLab to build and release an app in Google Play.') end - def help + def self.help variable_list = [ '<code>SUPPLY_PACKAGE_NAME</code>', '<code>SUPPLY_JSON_KEY_DATA</code>' diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index 6e4753470a3..6a9d603e6e5 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -17,11 +17,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Google Chat' end - def description + def self.description 'Send notifications from GitLab to a room in Google Chat.' end @@ -29,7 +29,7 @@ module Integrations 'hangouts_chat' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer') diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb index 559e48afd10..cc570e49e36 100644 --- a/app/models/integrations/harbor.rb +++ b/app/models/integrations/harbor.rb @@ -32,34 +32,32 @@ module Integrations non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') }, required: true - def title + def self.title 'Harbor' end - def description + def self.description s_("HarborIntegration|Use Harbor as this project's container registry.") end - def help + def self.help s_("HarborIntegration|After the Harbor integration is activated, global variables `$HARBOR_USERNAME`, `$HARBOR_HOST`, `$HARBOR_OCI`, `$HARBOR_PASSWORD`, `$HARBOR_URL` and `$HARBOR_PROJECT` will be created for CI/CD use.") end + def self.to_param + name.demodulize.downcase + end + def hostname Gitlab::Utils.parse_url(url).hostname end - class << self - def to_param - name.demodulize.downcase - end - - def supported_events - [] - end + def self.supported_events + [] + end - def supported_event_actions - [] - end + def self.supported_event_actions + [] end def test(*_args) diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb index a54946f074a..a1ce0877957 100644 --- a/app/models/integrations/irker.rb +++ b/app/models/integrations/irker.rb @@ -53,14 +53,31 @@ module Integrations # in the UI or API. prop_accessor :channels - def title + def self.title s_('IrkerService|irker (IRC gateway)') end - def description + def self.description s_('IrkerService|Send update messages to an irker server.') end + def self.help + docs_link = ActionController::Base.helpers.link_to( + _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url( + 'user/project/integrations/irker', + anchor: 'set-up-an-irker-daemon' + ), + target: '_blank', + rel: 'noopener noreferrer' + ) + + format(s_( + 'IrkerService|Send update messages to an irker server. ' \ + 'Before you can use this, you need to set up the irker daemon. %{docs_link}' + ).html_safe, docs_link: docs_link.html_safe) + end + def self.to_param 'irker' end @@ -85,23 +102,6 @@ module Integrations } end - def help - docs_link = ActionController::Base.helpers.link_to( - _('Learn more.'), - Rails.application.routes.url_helpers.help_page_url( - 'user/project/integrations/irker', - anchor: 'set-up-an-irker-daemon' - ), - target: '_blank', - rel: 'noopener noreferrer' - ) - - format(s_( - 'IrkerService|Send update messages to an irker server. ' \ - 'Before you can use this, you need to set up the irker daemon. %{docs_link}' - ).html_safe, docs_link: docs_link.html_safe) - end - private def get_channels diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index 0683c8408bc..a2f5667eaee 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -69,15 +69,15 @@ module Integrations %w[push merge_request tag_push] end - def title + def self.title 'Jenkins' end - def description + def self.description s_('Run CI/CD pipelines with Jenkins.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer' s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index f6e99454cb1..22367ee336d 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -184,16 +184,24 @@ module Integrations options end - def client - @client ||= JIRA::Client.new(options).tap do |client| + def client(additional_options = {}) + JIRA::Client.new(options.merge(additional_options)).tap do |client| # Replaces JIRA default http client with our implementation client.request_client = Gitlab::Jira::HttpClient.new(client.options) end end - def help + def self.title + 'Jira' + end + + def self.description + s_("JiraService|Use Jira as this project's issue tracker.") + end + + def self.help jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, - url: help_page_path('integration/jira/index')) + url: Gitlab::Routing.url_helpers.help_page_path('integration/jira/index')) format( s_("JiraService|You must configure Jira before enabling this integration. " \ "%{jira_doc_link_start}Learn more.%{link_end}"), @@ -201,14 +209,6 @@ module Integrations link_end: '</a>'.html_safe) end - def title - 'Jira' - end - - def description - s_("JiraService|Use Jira as this project's issue tracker.") - end - def self.to_param 'jira' end diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb index 7e391b11d82..361ff4afce8 100644 --- a/app/models/integrations/mattermost.rb +++ b/app/models/integrations/mattermost.rb @@ -5,11 +5,11 @@ module Integrations include SlackMattermostNotifier include SlackMattermostFields - def title + def self.title _('Mattermost notifications') end - def description + def self.description s_('Send notifications about project events to Mattermost channels.') end @@ -17,7 +17,7 @@ module Integrations 'mattermost' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer' s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb index 73cddd163e0..9554dec4168 100644 --- a/app/models/integrations/mattermost_slash_commands.rb +++ b/app/models/integrations/mattermost_slash_commands.rb @@ -14,11 +14,11 @@ module Integrations false end - def title + def self.title s_('Integrations|Mattermost slash commands') end - def description + def self.description s_('Integrations|Perform common tasks with slash commands.') end diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb index 208172d6303..3a7c848d411 100644 --- a/app/models/integrations/microsoft_teams.rb +++ b/app/models/integrations/microsoft_teams.rb @@ -18,11 +18,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Microsoft Teams notifications' end - def description + def self.description 'Send notifications about project events to Microsoft Teams.' end @@ -30,7 +30,7 @@ module Integrations 'microsoft_teams' end - def help + def self.help '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>' end diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb index 2d8e26d409f..9c129ca727c 100644 --- a/app/models/integrations/mock_ci.rb +++ b/app/models/integrations/mock_ci.rb @@ -14,11 +14,11 @@ module Integrations validates :mock_service_url, presence: true, public_url: true, if: :activated? - def title + def self.title 'MockCI' end - def description + def self.description 'Mock an external CI' end diff --git a/app/models/integrations/mock_monitoring.rb b/app/models/integrations/mock_monitoring.rb index 72bb292edaa..9e474078b28 100644 --- a/app/models/integrations/mock_monitoring.rb +++ b/app/models/integrations/mock_monitoring.rb @@ -2,11 +2,11 @@ module Integrations class MockMonitoring < BaseMonitoring - def title + def self.title 'Mock monitoring' end - def description + def self.description 'Mock monitoring service' end diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb index c0acb6c87b4..f027afe0381 100644 --- a/app/models/integrations/packagist.rb +++ b/app/models/integrations/packagist.rb @@ -29,11 +29,11 @@ module Integrations validates :username, presence: true, if: :activated? validates :token, presence: true, if: :activated? - def title + def self.title 'Packagist' end - def description + def self.description s_('Integrations|Keep your PHP dependencies updated on Packagist.') end diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb index 01efbc3e4a4..c7a93d48825 100644 --- a/app/models/integrations/pipelines_email.rb +++ b/app/models/integrations/pipelines_email.rb @@ -44,11 +44,11 @@ module Integrations end end - def title + def self.title _('Pipeline status emails') end - def description + def self.description _('Email the pipeline status to a list of recipients.') end diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb index b3cbc988dd6..97e6e3e09d1 100644 --- a/app/models/integrations/pivotaltracker.rb +++ b/app/models/integrations/pivotaltracker.rb @@ -20,15 +20,15 @@ module Integrations 'automatically inspect. Leave blank to include all branches.') end - def title + def self.title 'Pivotal Tracker' end - def description + def self.description s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pivotal_tracker'), target: '_blank', rel: 'noopener noreferrer' s_('Add commit messages as comments to Pivotal Tracker stories. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index ff8d07a1b4c..de923bbbdd5 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -51,11 +51,11 @@ module Integrations false end - def title + def self.title 'Prometheus' end - def description + def self.description s_('PrometheusService|Monitor application health with Prometheus metrics and dashboards') end diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb index 09e011023ed..36ff5189b0f 100644 --- a/app/models/integrations/pumble.rb +++ b/app/models/integrations/pumble.rb @@ -18,11 +18,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Pumble' end - def description + def self.description s_("PumbleIntegration|Send notifications about project events to Pumble.") end @@ -30,7 +30,7 @@ module Integrations 'pumble' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'), diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index 2feae29f627..b2c4e06e71f 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -71,11 +71,11 @@ module Integrations ] end - def title + def self.title 'Pushover' end - def description + def self.description s_('PushoverService|Get real-time notifications on your device.') end diff --git a/app/models/integrations/redmine.rb b/app/models/integrations/redmine.rb index bc2a64b0848..11eda7c69f7 100644 --- a/app/models/integrations/redmine.rb +++ b/app/models/integrations/redmine.rb @@ -6,15 +6,15 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def title + def self.title 'Redmine' end - def description + def self.description s_("IssueTracker|Use Redmine as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/redmine'), target: '_blank', rel: 'noopener noreferrer' s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb index 227fdca5c91..1d004356469 100644 --- a/app/models/integrations/shimo.rb +++ b/app/models/integrations/shimo.rb @@ -16,11 +16,11 @@ module Integrations valid? && activated? end - def title + def self.title s_('Shimo|Shimo') end - def description + def self.description s_('Shimo|Link to a Shimo Workspace from the sidebar.') end diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb index f70376e2f0d..9f9614a84fd 100644 --- a/app/models/integrations/slack.rb +++ b/app/models/integrations/slack.rb @@ -5,11 +5,11 @@ module Integrations include SlackMattermostNotifier include SlackMattermostFields - def title + def self.title 'Slack notifications' end - def description + def self.description 'Send notifications about project events to Slack.' end diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb index b209f37ee7c..c5ea6f22951 100644 --- a/app/models/integrations/slack_slash_commands.rb +++ b/app/models/integrations/slack_slash_commands.rb @@ -10,11 +10,11 @@ module Integrations non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, placeholder: '' - def title + def self.title 'Slack slash commands' end - def description + def self.description "Perform common operations in Slack." end diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb index bf3f391564f..1b4ab152b1d 100644 --- a/app/models/integrations/squash_tm.rb +++ b/app/models/integrations/squash_tm.rb @@ -22,15 +22,15 @@ module Integrations validates :token, length: { maximum: 255 }, allow_blank: true end - def title + def self.title 'Squash TM' end - def description + def self.description s_("SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/squash_tm'), diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index 575c3b8a334..913242ef9ac 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -47,15 +47,15 @@ module Integrations end end - def title + def self.title 'JetBrains TeamCity' end - def description + def self.description s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.') end - def help + def self.help s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.') end diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb index 71fe6f8d6ef..8eb1a7ad0ea 100644 --- a/app/models/integrations/telegram.rb +++ b/app/models/integrations/telegram.rb @@ -38,11 +38,11 @@ module Integrations before_validation :set_webhook - def title + def self.title 'Telegram' end - def description + def self.description s_("TelegramIntegration|Send notifications about project events to Telegram.") end @@ -50,7 +50,7 @@ module Integrations 'telegram' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/telegram'), diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index 3b4bcfa28d3..6ee95c1173b 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -17,11 +17,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Unify Circuit' end - def description + def self.description s_('Integrations|Send notifications about project events to Unify Circuit.') end @@ -29,7 +29,7 @@ module Integrations 'unify_circuit' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer' s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index 3ef8ab39352..5f8cc195544 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -17,11 +17,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title s_("WebexTeamsService|Webex Teams") end - def description + def self.description s_("WebexTeamsService|Send notifications about project events to Webex Teams.") end @@ -29,7 +29,7 @@ module Integrations 'webex_teams' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer' s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb index 15246a37aa7..932e588a829 100644 --- a/app/models/integrations/youtrack.rb +++ b/app/models/integrations/youtrack.rb @@ -14,15 +14,15 @@ module Integrations @reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/ end - def title + def self.title 'YouTrack' end - def description + def self.description s_("IssueTracker|Use YouTrack as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer' s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb index 58ec4abf30c..2aec0c1e871 100644 --- a/app/models/integrations/zentao.rb +++ b/app/models/integrations/zentao.rb @@ -57,18 +57,18 @@ module Integrations data_fields.api_url ||= issues_tracker['api_url'] end - def title + def self.title 'ZenTao' end - def description + def self.description s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.") end - def help + def self.help s_("ZentaoIntegration|Before you enable this integration, you must configure ZenTao. For more details, read the %{link_start}ZenTao integration documentation%{link_end}.") % { link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">' - .html_safe % { url: help_page_url('user/project/integrations/zentao') }, + .html_safe % { url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/zentao') }, link_end: '</a>'.html_safe } end diff --git a/app/models/member.rb b/app/models/member.rb index 77e283044ea..9690e16fd7d 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -135,11 +135,12 @@ class Member < ApplicationRecord .reorder(nil) end - scope :without_invites_and_requests, -> do - active_state - .non_request - .non_invite - .non_minimal_access + scope :without_invites_and_requests, ->(minimal_access: false) do + result = active_state.non_request.non_invite + + result = result.non_minimal_access unless minimal_access + + result end scope :invite, -> { where.not(invite_token: nil) } diff --git a/app/models/members/members/members_with_parents.rb b/app/models/members/members/members_with_parents.rb new file mode 100644 index 00000000000..61ce99e1f3e --- /dev/null +++ b/app/models/members/members/members_with_parents.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module Members + class MembersWithParents + attr_reader :group + + def initialize(group) + @group = group + end + + # Returns all members for group and parents, with no filters + def all_members + GroupMember.from_union([ + members_from_self_and_ancestors, + members_from_self_and_ancestor_group_shares + ]) + end + + # Returns members based on filter options: + # + # - `active_users`. DEPRECATED. If true, returns only members for active users + # - `minimal_access`. Used only in EE (GitLab Premium). If true, returns + # members which has minimal access. If false (default), does not return + # members with minimal access + # + # NOTE : this method does not return pending invites, nor requests. + def members(active_users: false, minimal_access: false) + raise ArgumentError, 'active_users: is deprecated' if active_users && minimal_access + + group_hierarchy_members = members_from_self_and_ancestors + + group_hierarchy_members = + if active_users + group_hierarchy_members.active_without_invites_and_requests + else + filter_invites_and_requests(group_hierarchy_members, minimal_access) + end + + GroupMember.from_union([ + group_hierarchy_members, + members_from_self_and_ancestor_group_shares + ]) + end + + private + + # NOTE: minimal access is Premium, so in FOSS we will not include minimal access member + def filter_invites_and_requests(members, _minimal_access) + members.without_invites_and_requests(minimal_access: false) + end + + def source_ids + # Avoids an unnecessary SELECT when the group has no parents + @source_ids ||= + if group.has_parent? + group.self_and_ancestors.reorder(nil).select(:id) + else + group.id + end + end + + def members_from_self_and_ancestors + GroupMember + .with_source_id(source_ids) + .select(*GroupMember.cached_column_list) + end + + def members_from_self_and_ancestor_group_shares + group_group_link_table = GroupGroupLink.arel_table + group_member_table = GroupMember.arel_table + + group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids) + cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query) + cte_alias = cte.table.alias(GroupGroupLink.table_name) + + # Instead of members.access_level, we need to maximize that access_level at + # the respective group_group_links.group_access. + member_columns = GroupMember.attribute_names.map do |column_name| + if column_name == 'access_level' + smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level') + else + group_member_table[column_name] + end + end + + GroupMember + .with(cte.to_arel) + .select(*member_columns) + .from([group_member_table, cte.alias_to(group_group_link_table)]) + .where(group_member_table[:requested_at].eq(nil)) + .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id])) + .where(group_member_table[:source_type].eq('Namespace')) + .where(group_member_table[:state].eq(::Member::STATE_ACTIVE)) + .non_minimal_access + end + + def smallest_value_arel(args, column_alias) + Arel::Nodes::As.new( + Arel::Nodes::NamedFunction.new('LEAST', args), + Arel::Nodes::SqlLiteral.new(column_alias)) + end + end +end + +Members::MembersWithParents.prepend_mod diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index d07e4f9e298..5e5f9ab7385 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -48,6 +48,12 @@ class ProjectMember < Member end end + def permissible_access_level_roles_for_project_access_token(current_user, project) + permissible_access_level_roles(current_user, project).filter do |_, value| + value <= project.project_authorizations.find_by(user: current_user).access_level + end + end + def access_level_roles Gitlab::Access.options end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d9726e76c4b..524a9b8074b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -384,7 +384,6 @@ class MergeRequest < ApplicationRecord } scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) } - scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) } scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker scope :by_target_branch_wildcard, ->(wildcard_branch_name) do @@ -530,6 +529,14 @@ class MergeRequest < ApplicationRecord .pluck(:target_branch) end + def self.recent_source_branches(limit: 100) + group(:source_branch) + .select(:source_branch) + .reorder(arel_table[:updated_at].maximum.desc) + .limit(limit) + .pluck(:source_branch) + end + def self.sort_by_attribute(method, excluded_labels: []) case method.to_s when 'merged_at', 'merged_at_asc' then order_merged_at_asc @@ -1235,17 +1242,14 @@ class MergeRequest < ApplicationRecord } end - def mergeable?( - skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, - skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false) - - return false unless mergeable_state?( - skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check, - skip_draft_check: skip_draft_check, - skip_approved_check: skip_approved_check, - skip_blocked_check: skip_blocked_check - ) + # mergeable_state_check_params allows a hash of merge checks to skip or not + # skip_ci_check + # skip_discussions_check + # skip_draft_check + # skip_approved_check + # skip_blocked_check + def mergeable?(check_mergeability_retry_lease: false, skip_rebase_check: false, **mergeable_state_check_params) + return false unless mergeable_state?(**mergeable_state_check_params) check_mergeability(sync_retry_lease: check_mergeability_retry_lease) mergeable_git_state?(skip_rebase_check: skip_rebase_check) @@ -1275,18 +1279,16 @@ class MergeRequest < ApplicationRecord mergeable_state_checks + mergeable_git_state_checks end - def mergeable_state?( - skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, - skip_draft_check: false, skip_blocked_check: false) + # mergeable_state_check_params allows a hash of merge checks to skip or not + # skip_ci_check + # skip_discussions_check + # skip_draft_check + # skip_approved_check + # skip_blocked_check + def mergeable_state?(**mergeable_state_check_params) additional_checks = execute_merge_checks( self.class.mergeable_state_checks, - params: { - skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check, - skip_approved_check: skip_approved_check, - skip_draft_check: skip_draft_check, - skip_blocked_check: skip_blocked_check - } + params: mergeable_state_check_params ) additional_checks.success? end @@ -1386,7 +1388,7 @@ class MergeRequest < ApplicationRecord end def mergeable_discussions_state? - return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true) + return true unless only_allow_merge_if_all_discussions_are_resolved? unresolved_notes.none?(&:to_be_resolved?) end @@ -1566,8 +1568,16 @@ class MergeRequest < ApplicationRecord access.can_push_to_branch?(target_branch) end + def only_allow_merge_if_pipeline_succeeds? + project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true) + end + + def only_allow_merge_if_all_discussions_are_resolved? + project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true) + end + def mergeable_ci_state? - return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true) + return true unless only_allow_merge_if_pipeline_succeeds? return false unless actual_head_pipeline return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped? diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb index fdf57068928..2fb995ee512 100644 --- a/app/models/merge_request_context_commit_diff_file.rb +++ b/app/models/merge_request_context_commit_diff_file.rb @@ -10,7 +10,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord belongs_to :merge_request_context_commit, inverse_of: :diff_files sha_attribute :sha - alias_attribute :id, :sha # create MergeRequestContextCommitDiffFile by given diff file record(s) def self.bulk_insert(*args) diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index fc08dd4d9c8..790520c4123 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -6,13 +6,8 @@ class MergeRequestDiffCommit < ApplicationRecord include BulkInsertSafe include ShaAttribute include CachedCommit - include IgnorableColumns include FromUnion - ignore_column %i[author_name author_email committer_name committer_email], - remove_with: '14.6', - remove_after: '2021-11-22' - belongs_to :merge_request_diff # This relation is called `commit_author` and not `author`, as the project @@ -33,7 +28,6 @@ class MergeRequestDiffCommit < ApplicationRecord belongs_to :committer, class_name: 'MergeRequest::DiffCommitUser' sha_attribute :sha - alias_attribute :id, :sha attribute :trailers, :ind_jsonb validates :trailers, json_schema: { filename: 'git_trailers' } @@ -129,4 +123,8 @@ class MergeRequestDiffCommit < ApplicationRecord def committer_email committer&.email end + + def to_hash + super.merge({ 'id' => sha }) + end end diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index 6f4728a1d98..70eaab8c0ab 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -12,12 +12,14 @@ module Ml validates :eid, :experiment, presence: true validates :status, inclusion: { in: statuses.keys } + validates :model_version_id, uniqueness: { allow_nil: true } belongs_to :experiment, class_name: 'Ml::Experiment' belongs_to :user belongs_to :package, class_name: 'Packages::Package' belongs_to :project belongs_to :ci_build, class_name: 'Ci::Build', optional: true + belongs_to :model_version, class_name: 'Ml::ModelVersion', optional: true, inverse_of: :candidate has_many :metrics, class_name: 'Ml::CandidateMetric' has_many :params, class_name: 'Ml::CandidateParam' has_many :metadata, class_name: 'Ml::CandidateMetadata' diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb index 27f03ed5857..b6f7e9a0639 100644 --- a/app/models/ml/model.rb +++ b/app/models/ml/model.rb @@ -3,6 +3,7 @@ module Ml class Model < ApplicationRecord include Presentable + include Sortable validates :project, :default_experiment, presence: true validates :name, @@ -15,15 +16,19 @@ module Ml has_one :default_experiment, class_name: 'Ml::Experiment' belongs_to :project + belongs_to :user has_many :versions, class_name: 'Ml::ModelVersion' + has_many :metadata, class_name: 'Ml::ModelMetadata' has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model scope :including_latest_version, -> { includes(:latest_version) } + scope :including_project, -> { includes(:project) } scope :with_version_count, -> { left_outer_joins(:versions) .select("ml_models.*, count(ml_model_versions.id) as version_count") .group(:id) } + scope :by_name, ->(name) { where("ml_models.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection scope :by_project, ->(project) { where(project_id: project.id) } def valid_default_experiment? @@ -33,13 +38,12 @@ module Ml errors.add(:default_experiment) unless default_experiment.project_id == project_id end - def self.find_or_create(project, name, experiment) - create_with(default_experiment: experiment) - .find_or_create_by(project: project, name: name) - end - def self.by_project_id_and_id(project_id, id) find_by(project_id: project_id, id: id) end + + def self.by_project_id_and_name(project_id, name) + find_by(project_id: project_id, name: name) + end end end diff --git a/app/models/ml/model_metadata.rb b/app/models/ml/model_metadata.rb new file mode 100644 index 00000000000..9c4273c629c --- /dev/null +++ b/app/models/ml/model_metadata.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ml + class ModelMetadata < ApplicationRecord + validates :name, + length: { maximum: 250 }, + presence: true, + uniqueness: { scope: :model, message: ->(metadata, _) { "'#{metadata.name}' already taken" } } + validates :value, length: { maximum: 5000 }, presence: true + + belongs_to :model, class_name: 'Ml::Model', optional: false + end +end diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb index e7fcde2cb5c..58da57f27d6 100644 --- a/app/models/ml/model_version.rb +++ b/app/models/ml/model_version.rb @@ -2,6 +2,8 @@ module Ml class ModelVersion < ApplicationRecord + include Presentable + validates :project, :model, presence: true validates :version, @@ -10,11 +12,15 @@ module Ml presence: true, length: { maximum: 255 } + validates :description, + length: { maximum: 500 } + validate :valid_model?, :valid_package? belongs_to :model, class_name: 'Ml::Model' belongs_to :project belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true + has_one :candidate, class_name: 'Ml::Candidate' delegate :name, to: :model @@ -22,8 +28,17 @@ module Ml scope :latest_by_model, -> { order_by_model_id_id_desc.select('DISTINCT ON (model_id) *') } class << self - def find_or_create!(model, version, package) - create_with(package: package).find_or_create_by!(project: model.project, model: model, version: version) + def find_or_create!(model, version, package, description) + create_with(package: package, description: description) + .find_or_create_by!(project: model.project, model: model, version: version) + end + + def by_project_id_and_id(project_id, id) + find_by(project_id: project_id, id: id) + end + + def by_project_id_name_and_version(project_id, name, version) + joins(:model).find_by(model: { name: name, project_id: project_id }, project_id: project_id, version: version) end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 733b89fcaf2..cd54ac1b24a 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -18,6 +18,7 @@ class Namespace < ApplicationRecord include Referable include CrossDatabaseIgnoredTables include IgnorableColumns + include UseSqlFunctionForPrimaryKeyLookups ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16' @@ -138,6 +139,8 @@ class Namespace < ApplicationRecord to: :namespace_settings delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=, to: :namespace_settings + delegate :emails_enabled, :emails_enabled=, + to: :namespace_settings, allow_nil: true delegate :allow_runner_registration_token, :allow_runner_registration_token=, to: :namespace_settings @@ -204,7 +207,7 @@ class Namespace < ApplicationRecord # Make sure that the name is same as strong_memoize name in root_ancestor # method - attr_writer :root_ancestor, :emails_disabled_memoized + attr_writer :root_ancestor, :emails_enabled_memoized class << self def sti_class_for(type_name) @@ -299,6 +302,14 @@ class Namespace < ApplicationRecord super || Gitlab::CurrentSettings.default_branch_protection end + def default_branch_protection_settings + settings = default_branch_protection_defaults + + return settings unless settings.blank? + + Gitlab::CurrentSettings.default_branch_protection_defaults + end + def visibility_level_field :visibility_level end @@ -382,17 +393,16 @@ class Namespace < ApplicationRecord # any ancestor can disable emails for all descendants def emails_disabled? - strong_memoize(:emails_disabled_memoized) do - if parent_id - self_and_ancestors.where(emails_disabled: true).exists? - else - !!emails_disabled - end - end + !emails_enabled? end def emails_enabled? - !emails_disabled? + # If no namespace_settings, we can assume it has not changed from enabled + return true unless namespace_settings + + strong_memoize(:emails_enabled_memoized) do + namespace_settings.emails_enabled? + end end def lfs_enabled? @@ -626,8 +636,7 @@ class Namespace < ApplicationRecord :route, :project_setting, :project_feature, - pages_metadatum: :pages_deployment - ) + :active_pages_deployments) end private diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 3befcdeaec5..13d2c5a62e2 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -63,6 +63,12 @@ class NamespaceSetting < ApplicationRecord namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy end + def emails_enabled? + return emails_enabled unless namespace.has_parent? + + all_ancestors_have_emails_enabled? + end + def show_diff_preview_in_email? return show_diff_preview_in_email unless namespace.has_parent? @@ -89,6 +95,10 @@ class NamespaceSetting < ApplicationRecord private + def all_ancestors_have_emails_enabled? + self.class.where(namespace_id: namespace.self_and_ancestors, emails_enabled: false).none? + end + def all_ancestors_allow_diff_preview_in_email? !self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists? end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 0f410d4810d..f60e7682418 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -2,7 +2,7 @@ module Network class Graph - attr_reader :days, :commits, :map, :notes, :repo + attr_reader :days, :commits, :map, :repo def self.max_count @max_count ||= 650 @@ -17,28 +17,10 @@ module Network @commits = collect_commits @days = index_commits - @notes = collect_notes end protected - def collect_notes - return {} if Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment) - - h = Hash.new(0) - - @project - .notes - .where(noteable_type: 'Commit') - .group('notes.commit_id') - .select('notes.commit_id, count(notes.id) as note_count') - .each do |item| - h[item.commit_id] = item.note_count.to_i - end - - h - end - # Get commits from repository # def collect_commits diff --git a/app/models/note.rb b/app/models/note.rb index eae7a40fb4e..6f4a56dd3cc 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -383,7 +383,11 @@ class Note < ApplicationRecord end def for_project_noteable? - !(for_personal_snippet? || for_abuse_report?) + !(for_personal_snippet? || for_abuse_report? || group_level_issue?) + end + + def group_level_issue? + (for_issue? || for_work_item?) && noteable&.project_id.blank? end def for_design? diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index 893b08d7872..157b851e009 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -42,6 +42,10 @@ module Organizations organization_users.exists?(user: user) end + def web_url(only_path: nil) + Gitlab::UrlBuilder.build(self, only_path: only_path) + end + private def check_if_default_organization diff --git a/app/models/packages/npm/metadata_cache.rb b/app/models/packages/npm/metadata_cache.rb index 02efeda69cb..b6ab2a88a98 100644 --- a/app/models/packages/npm/metadata_cache.rb +++ b/app/models/packages/npm/metadata_cache.rb @@ -5,6 +5,9 @@ module Packages class MetadataCache < ApplicationRecord include FileStoreMounter include Packages::Downloadable + include Packages::Destructible + + enum status: { default: 0, processing: 1, error: 3 } belongs_to :project, inverse_of: :npm_metadata_caches @@ -18,6 +21,9 @@ module Packages before_validation :set_object_storage_key attr_readonly :object_storage_key + scope :stale, -> { where(project_id: nil) } + scope :pending_destruction, -> { stale.default } + def self.find_or_build(package_name:, project_id:) find_or_initialize_by( package_name: package_name, diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb index 643b5552d84..3315f11b974 100644 --- a/app/models/packages/nuget/symbol.rb +++ b/app/models/packages/nuget/symbol.rb @@ -4,6 +4,7 @@ module Packages module Nuget class Symbol < ApplicationRecord include FileStoreMounter + include ShaAttribute belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_symbols @@ -13,6 +14,8 @@ module Packages validates :signature, uniqueness: { scope: :file_path } validates :object_storage_key, uniqueness: true + sha256_attribute :file_sha256 + mount_file_store_uploader SymbolUploader before_validation :set_object_storage_key, on: :create diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb index 582b51475c2..f13bcc6e32e 100644 --- a/app/models/packages/protection/rule.rb +++ b/app/models/packages/protection/rule.rb @@ -12,6 +12,12 @@ module Packages validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] }, length: { maximum: 255 } + validates :package_name_pattern, + format: { + with: Gitlab::Regex.protection_rules_npm_package_name_pattern_regex, + message: ->(_object, _data) { _('should be a valid NPM package name with optional wildcard characters.') } + }, + if: :npm? validates :package_type, presence: true validates :push_protected_up_to_access_level, presence: true @@ -20,7 +26,7 @@ module Packages scope :for_package_name, ->(package_name) { return none if package_name.blank? - where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name) + where(':package_name ILIKE package_name_pattern_ilike_query', package_name: package_name) } def self.push_protected_from?(access_level:, package_name:, package_type:) diff --git a/app/models/packages/pypi/metadatum.rb b/app/models/packages/pypi/metadatum.rb index ff247fedb59..f7360409507 100644 --- a/app/models/packages/pypi/metadatum.rb +++ b/app/models/packages/pypi/metadatum.rb @@ -3,10 +3,24 @@ class Packages::Pypi::Metadatum < ApplicationRecord self.primary_key = :package_id + MAX_REQUIRED_PYTHON_LENGTH = 255 + MAX_KEYWORDS_LENGTH = 255 + MAX_METADATA_VERSION_LENGTH = 16 + MAX_AUTHOR_EMAIL_LENGTH = 2048 + MAX_SUMMARY_LENGTH = 255 + MAX_DESCRIPTION_LENGTH = 4000 + MAX_DESCRIPTION_CONTENT_TYPE = 128 + belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum validates :package, presence: true - validates :required_python, length: { maximum: 255 }, allow_nil: false + validates :required_python, length: { maximum: MAX_REQUIRED_PYTHON_LENGTH }, allow_nil: false + validates :keywords, length: { maximum: MAX_KEYWORDS_LENGTH }, allow_nil: true + validates :metadata_version, length: { maximum: MAX_METADATA_VERSION_LENGTH }, allow_nil: true + validates :author_email, length: { maximum: MAX_AUTHOR_EMAIL_LENGTH }, allow_nil: true + validates :summary, length: { maximum: MAX_SUMMARY_LENGTH }, allow_nil: true + validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, allow_nil: true + validates :description_content_type, length: { maximum: MAX_DESCRIPTION_CONTENT_TYPE }, allow_nil: true validate :pypi_package_type diff --git a/app/models/packages/tag.rb b/app/models/packages/tag.rb index 9c17a147bf4..0df64bfba54 100644 --- a/app/models/packages/tag.rb +++ b/app/models/packages/tag.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true class Packages::Tag < ApplicationRecord belongs_to :package, inverse_of: :tags + belongs_to :project validates :package, :name, presence: true + before_save :ensure_project_id + FOR_PACKAGES_TAGS_LIMIT = 200 NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags @@ -15,4 +18,8 @@ class Packages::Tag < ApplicationRecord .order(updated_at: :desc) .limit(FOR_PACKAGES_TAGS_LIMIT) end + + def ensure_project_id + self.project_id ||= package.project_id + end end diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index 8a02415aef4..e5e23c3bb84 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -4,8 +4,6 @@ module Pages class LookupPath include Gitlab::Utils::StrongMemoize - LegacyStorageDisabledError = Class.new(::StandardError) - def initialize(project, trim_prefix: nil, domain: nil) @project = project @domain = domain @@ -15,6 +13,7 @@ module Pages def project_id project.id end + strong_memoize_attr :project_id def access_control project.private_pages? @@ -76,8 +75,15 @@ module Pages attr_reader :project, :trim_prefix, :domain + # project.active_pages_deployments is already loaded from the database, + # so selecting from the array to avoid N+1 + # this will change with when serving multiple versions on + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133261 def deployment - project.pages_metadatum.pages_deployment + project + .active_pages_deployments + .to_a + .find { |deployment| deployment.path_prefix.blank? } end strong_memoize_attr :deployment diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index f05ed2aac6e..2aa36a94171 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -17,7 +17,8 @@ class PagesDeployment < ApplicationRecord scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) } scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) } scope :project_id_in, ->(ids) { where(project_id: ids) } - scope :active, -> { where(deleted_at: nil) } + scope :with_path_prefix, ->(prefix) { where("COALESCE(path_prefix, '') = ?", prefix.to_s) } + scope :active, -> { where(deleted_at: nil).order(created_at: :desc) } scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) } validates :file, presence: true @@ -33,11 +34,23 @@ class PagesDeployment < ApplicationRecord skip_callback :save, :after, :store_file! after_commit :store_file_after_commit!, on: [:create, :update] + def self.latest_pipeline_id + Ci::Build.id_in(pluck(:ci_build_id)).maximum(:commit_id) + end + + def self.deactivate_all(project) + now = Time.now.utc + active + .project_id_in(project.id) + .update_all(updated_at: now, deleted_at: now) + end + def self.deactivate_deployments_older_than(deployment, time: nil) now = Time.now.utc active .older_than(deployment.id) - .where(project_id: deployment.project_id, path_prefix: deployment.path_prefix) + .project_id_in(deployment.project_id) + .with_path_prefix(deployment.path_prefix) .update_all(updated_at: now, deleted_at: time || now) end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index b86bc761cc1..cabd3924fd6 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -11,6 +11,8 @@ class PagesDomain < ApplicationRecord MAX_CERTIFICATE_KEY_LENGTH = 8192 + X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = 19 + enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages @@ -122,15 +124,23 @@ class PagesDomain < ApplicationRecord x509.check_private_key(pkey) end - def has_intermediates? + def has_valid_intermediates? return false unless x509 - # self-signed certificates doesn't have the certificate chain + # self-signed certificates don't have the certificate chain return true if x509.verify(x509.public_key) store = OpenSSL::X509::Store.new store.set_default_paths + store.verify_callback = ->(is_valid, store_ctx) { + # allow self signed certs, see https://gitlab.com/gitlab-org/gitlab/-/issues/356447 + return true if store_ctx.error == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN + + self.errors.add(:certificate, store_ctx.error_string) unless is_valid + is_valid + } + store.verify(x509, untrusted_ca_certs_bundle) rescue OpenSSL::X509::StoreError false @@ -230,9 +240,7 @@ class PagesDomain < ApplicationRecord end def pages_deployed? - return false unless project - - project.pages_metadatum&.deployed? + project&.pages_deployed? end private @@ -260,9 +268,7 @@ class PagesDomain < ApplicationRecord end def validate_intermediates - unless has_intermediates? - self.errors.add(:certificate, 'misses intermediates') - end + self.errors.add(:certificate, 'misses intermediates') unless has_valid_intermediates? end def validate_pages_domain diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 4dfe7252a0c..f2fbb5b989e 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -44,8 +44,9 @@ class PersonalAccessToken < ApplicationRecord scope :last_used_after, -> (date) { where("last_used_at >= ?", date) } validates :scopes, presence: true + validates :expires_at, presence: true, on: :create, unless: :allow_expires_at_to_be_empty? + validate :validate_scopes - validates :expires_at, presence: true, on: :create validate :expires_at_before_instance_max_expiry_date, on: :create def revoke! @@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord self.class.token_prefix end + def allow_expires_at_to_be_empty? + false + end + def expires_at_before_instance_max_expiry_date return unless expires_at diff --git a/app/models/project.rb b/app/models/project.rb index fd226d23e77..0d103094aec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -45,6 +45,7 @@ class Project < ApplicationRecord include UpdatedAtFilterable include IgnorableColumns include CrossDatabaseIgnoredTables + include UseSqlFunctionForPrimaryKeyLookups ignore_column :emails_disabled, remove_with: '16.3', remove_after: '2023-08-22' @@ -140,8 +141,14 @@ class Project < ApplicationRecord after_create -> { create_or_load_association(:pages_metadatum) } after_create :set_timestamps_for_create after_create :check_repository_absence! + + # TODO: Remove this callback after background syncing is implemented. See https://gitlab.com/gitlab-org/gitlab/-/issues/429376. + after_update :update_catalog_resource, + if: -> { (saved_change_to_name? || saved_change_to_description? || saved_change_to_visibility_level?) && catalog_resource } + before_destroy :remove_private_deploy_keys after_destroy :remove_exports + after_save :update_project_statistics, if: :saved_change_to_namespace_id? after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? } @@ -457,8 +464,10 @@ class Project < ApplicationRecord # GitLab Pages has_many :pages_domains has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project - # we need to clean up files, not only remove records - has_many :pages_deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + # rubocop:disable Cop/ActiveRecordDependent -- we need to clean up files, not only remove records + has_many :pages_deployments, dependent: :destroy, inverse_of: :project + # rubocop:enable Cop/ActiveRecordDependent + has_many :active_pages_deployments, -> { active }, class_name: 'PagesDeployment', inverse_of: :project # Can be too many records. We need to implement delete_all in batches. # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637 @@ -497,7 +506,7 @@ class Project < ApplicationRecord delegate :merge_requests_access_level, :forking_access_level, :issues_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level, :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, :monitor_access_level, :releases_access_level, :infrastructure_access_level, :model_experiments_access_level, to: :project_feature, allow_nil: true delegate :name, to: :owner, allow_nil: true, prefix: true - delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage + delegate :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage delegate :last_pipeline, to: :commit, allow_nil: true with_options to: :team do @@ -620,42 +629,6 @@ class Project < ApplicationRecord .or(arel_table[:storage_version].eq(nil))) end - scope :sorted_by_name_desc, -> { - keyset_order = Gitlab::Pagination::Keyset::Order.build([ - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: :name, - column_expression: Project.arel_table[:name], - order_expression: Project.arel_table[:name].desc, - distinct: false, - nullable: :nulls_last - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: :id, - order_expression: Project.arel_table[:id].desc - ) - ]) - - reorder(keyset_order) - } - - scope :sorted_by_name_asc, -> { - keyset_order = Gitlab::Pagination::Keyset::Order.build([ - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: :name, - column_expression: Project.arel_table[:name], - order_expression: Project.arel_table[:name].asc, - distinct: false, - nullable: :nulls_last - ), - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: :id, - order_expression: Project.arel_table[:id].asc - ) - ]) - - reorder(keyset_order) - } - scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) } scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) } scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) } @@ -769,7 +742,7 @@ class Project < ApplicationRecord end scope :with_pages_deployed, -> do - joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed) + where_exists(PagesDeployment.active.where('pages_deployments.project_id = projects.id')) end scope :pages_metadata_not_migrated, -> do @@ -1476,12 +1449,10 @@ class Project < ApplicationRecord end def build_or_assign_import_data(data: nil, credentials: nil) - return if data.nil? && credentials.nil? - project_import_data = import_data || build_import_data - project_import_data.merge_data(data.to_h) - project_import_data.merge_credentials(credentials.to_h) + project_import_data.merge_data(data.to_h) if data + project_import_data.merge_credentials(credentials.to_h) if credentials project_import_data end @@ -1564,9 +1535,9 @@ class Project < ApplicationRecord limit = creator.projects_limit error = if limit == 0 - _('Personal project creation is not allowed. Please contact your administrator with questions') + _('You cannot create projects in your personal namespace. Contact your GitLab administrator.') else - _('Your project limit is %{limit} projects! Please contact your administrator to increase it') + _("You've reached your limit of %{limit} projects created. Contact your GitLab administrator.") end self.errors.add(:limit_reached, error % { limit: limit }) @@ -2236,11 +2207,11 @@ class Project < ApplicationRecord end def pages_deployed? - pages_metadatum&.deployed? + active_pages_deployments.exists? end def pages_show_onboarding? - !(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed) + !(pages_metadatum&.onboarding_complete || pages_deployed?) end def remove_private_deploy_keys @@ -2262,27 +2233,6 @@ class Project < ApplicationRecord ensure_pages_metadatum.update!(onboarding_complete: true) end - def mark_pages_as_deployed - ensure_pages_metadatum.update!(deployed: true) - end - - def mark_pages_as_not_deployed - ensure_pages_metadatum.update!(deployed: false) - end - - def update_pages_deployment!(deployment) - ensure_pages_metadatum.update!(pages_deployment: deployment) - end - - def set_first_pages_deployment!(deployment) - ensure_pages_metadatum - - # where().update_all to perform update in the single transaction with check for null - ProjectPagesMetadatum - .where(project_id: id, pages_deployment_id: nil) - .update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id) - end - def set_full_path(gl_full_path: full_path) # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using @@ -2875,7 +2825,7 @@ class Project < ApplicationRecord end def uses_default_ci_config? - ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] + ci_config_path.blank? || Gitlab::FileDetector.type_of(ci_config_path) == :gitlab_ci end def limited_protected_branches(limit) @@ -3026,7 +2976,7 @@ class Project < ApplicationRecord end def ci_config_for(sha) - repository.gitlab_ci_yml_for(sha, ci_config_path_or_default) + repository.blob_data_at(sha, ci_config_path_or_default) end def enabled_group_deploy_keys @@ -3530,6 +3480,10 @@ class Project < ApplicationRecord pool_repository_shard == repository_storage end + + def update_catalog_resource + catalog_resource.sync_with_project! + end end Project.prepend_mod_with('Project') diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb index dba81a6cb60..5e47ec6310d 100644 --- a/app/models/project_feature_usage.rb +++ b/app/models/project_feature_usage.rb @@ -19,19 +19,6 @@ class ProjectFeatureUsage < ApplicationRecord end end - def log_jira_dvcs_integration_usage(cloud: true) - ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do - integration_field = self.class.jira_dvcs_integration_field(cloud: cloud) - - # The feature usage is used only once later to query the feature usage in a - # long date range. Therefore, we just need to update the timestamp once per - # day - break if persisted? && updated_today?(integration_field) - - persist_jira_dvcs_usage(integration_field) - end - end - private def updated_today?(integration_field) diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb index eca2e5a740e..87cff4f2715 100644 --- a/app/models/project_pages_metadatum.rb +++ b/app/models/project_pages_metadatum.rb @@ -10,7 +10,4 @@ class ProjectPagesMetadatum < ApplicationRecord belongs_to :project, inverse_of: :pages_metadatum belongs_to :pages_deployment - - scope :deployed, -> { where(deployed: true) } - scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) } end diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index ffb08e10f1f..7a80ad33d68 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -5,4 +5,6 @@ class ProjectSnippet < Snippet validates :project, presence: true validates :secret, inclusion: { in: [false] } + + scope :by_project, ->(project) { where(project: project) } end diff --git a/app/models/projects/repository_storage_move.rb b/app/models/projects/repository_storage_move.rb index f4411e0b4fd..e2c6d1853a9 100644 --- a/app/models/projects/repository_storage_move.rb +++ b/app/models/projects/repository_storage_move.rb @@ -14,11 +14,6 @@ module Projects alias_attribute :project, :container scope :with_projects, -> { includes(container: :route) } - override :update_repository_storage - def update_repository_storage(new_storage) - container.update_column(:repository_storage, new_storage) - end - override :schedule_repository_storage_update_worker def schedule_repository_storage_update_worker Projects::UpdateRepositoryStorageWorker.perform_async( diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index aebce59a040..40a1a4392dd 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,6 +5,7 @@ class ProtectedBranch < ApplicationRecord include Gitlab::SQL::Pattern include FromUnion include EachBatch + include Presentable belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches diff --git a/app/models/repository.rb b/app/models/repository.rb index e565de9c4ba..e639a389e0a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1102,10 +1102,6 @@ class Repository blob_data_at(sha, '.gitlab/route-map.yml') end - def gitlab_ci_yml_for(sha, path = '.gitlab-ci.yml') - blob_data_at(sha, path) - end - def lfsconfig_for(sha) blob_data_at(sha, '.lfsconfig') end @@ -1245,6 +1241,10 @@ class Repository def get_patch_id(old_revision, new_revision) raw_repository.get_patch_id(old_revision, new_revision) rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e + # This is expected when there are no differences between the old_revision and the new_revision. + # It's not ideal, but is simpler to handle this here than making breaking changes to gitaly. + return if e.message.match?(/no difference between old and new revision./) + Gitlab::ErrorTracking.track_exception( e, project_id: project.id, diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index d5c839724d4..ad1ce740c89 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -112,7 +112,7 @@ class ResourceLabelEvent < ResourceEvent end def resource_parent - issuable.project || issuable.group + issuable.try(:resource_parent) || issuable.project || issuable.group end def discussion_id_key diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb index 5986ac8a43f..82bda673491 100644 --- a/app/models/service_desk/custom_email_credential.rb +++ b/app/models/service_desk/custom_email_credential.rb @@ -2,6 +2,14 @@ module ServiceDesk class CustomEmailCredential < ApplicationRecord + # Used to explicitly set the SMTP AUTH method. + # If nil Net::SMTP will choose one of methods listed by the SMTP server. + enum smtp_authentication: { + plain: 0, + login: 1, + cram_md5: 2 + } + attr_encrypted :smtp_username, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', @@ -44,7 +52,8 @@ module ServiceDesk password: smtp_password, address: smtp_address, domain: Mail::Address.new(service_desk_setting.custom_email).domain, - port: smtp_port || 587 + port: smtp_port || 587, + authentication: smtp_authentication } end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 78b0c0849e3..3e075fdaa9e 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -77,6 +77,7 @@ class Snippet < ApplicationRecord scope :inc_relations_for_view, -> { includes(author: :status) } scope :inc_statistics, -> { includes(:statistics) } scope :with_statistics, -> { joins(:statistics) } + scope :with_repository_storage_moves, -> { joins(:repository_storage_moves) } scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) } scope :without_created_by_banned_user, -> do diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index a262802c8af..6b2fa99d547 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -31,6 +31,11 @@ class SnippetRepository < ApplicationRecord options[:actions] = transform_file_entries(files) + # The Gitaly calls perform HTTP requests for permissions check + # Stick to the primary in order to make those requests aware that + # primary database must be used to fetch the data + self.class.sticking.stick(:user, user.id) + capture_git_error { repository.commit_files(user, **options) } ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb index 06f0115ade6..d959a6339a4 100644 --- a/app/models/system/broadcast_message.rb +++ b/app/models/system/broadcast_message.rb @@ -117,7 +117,7 @@ module System end def ended? - ends_at < Time.current + ends_at.past? end def now? diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index dc93decce5e..8624a1a9463 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -4,6 +4,7 @@ class SystemNoteMetadata < ApplicationRecord include Importable include IgnorableColumns + ignore_column :id_convert_to_bigint, remove_with: '16.9', remove_after: '2024-01-13' ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' # These notes's action text might contain a reference that is external. diff --git a/app/models/upload.rb b/app/models/upload.rb index 59ce9a1f37a..745a6174931 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -174,7 +174,7 @@ class Upload < ApplicationRecord end def update_project_statistics - ProjectCacheWorker.perform_async(model_id, [], [:uploads_size]) + ProjectCacheWorker.perform_async(model_id, [], ['uploads_size']) end end diff --git a/app/models/user.rb b/app/models/user.rb index 4034677509f..25f22563136 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,6 +32,7 @@ class User < MainClusterwide::ApplicationRecord include EachBatch include CrossDatabaseIgnoredTables include IgnorableColumns + include UseSqlFunctionForPrimaryKeyLookups ignore_column %i[ email_opted_in @@ -48,7 +49,7 @@ class User < MainClusterwide::ApplicationRecord # Associations with dependent: option cross_database_ignore_tables( - %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests], + %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests events], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424285', on: :destroy ) @@ -390,6 +391,7 @@ class User < MainClusterwide::ApplicationRecord :first_day_of_week, :first_day_of_week=, :timezone, :timezone=, :time_display_relative, :time_display_relative=, + :time_display_format, :time_display_format=, :show_whitespace_in_diffs, :show_whitespace_in_diffs=, :view_diffs_file_by_file, :view_diffs_file_by_file=, :pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=, @@ -417,6 +419,7 @@ class User < MainClusterwide::ApplicationRecord delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true + delegate :mastodon, :mastodon=, 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 @@ -425,6 +428,7 @@ class User < MainClusterwide::ApplicationRecord delegate :organization, :organization=, to: :user_detail, allow_nil: true delegate :discord, :discord=, to: :user_detail, allow_nil: true delegate :email_reset_offered_at, :email_reset_offered_at=, to: :user_detail, allow_nil: true + delegate :project_authorizations_recalculated_at, :project_authorizations_recalculated_at=, to: :user_detail, allow_nil: true accepts_nested_attributes_for :user_preference, update_only: true accepts_nested_attributes_for :user_detail, update_only: true @@ -600,6 +604,12 @@ class User < MainClusterwide::ApplicationRecord scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) } scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) } scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) } + scope :trusted, -> do + where('EXISTS (?)', ::UserCustomAttribute + .select(1) + .where('user_id = users.id') + .trusted_with_spam) + end strip_attributes! :name @@ -768,6 +778,8 @@ class User < MainClusterwide::ApplicationRecord external when 'deactivated' deactivated + when "trusted" + trusted else active_without_ghosts end @@ -791,9 +803,9 @@ class User < MainClusterwide::ApplicationRecord order = <<~SQL CASE - WHEN LOWER(users.name) = :query THEN 0 + WHEN LOWER(users.public_email) = :query THEN 0 WHEN LOWER(users.username) = :query THEN 1 - WHEN LOWER(users.public_email) = :query THEN 2 + WHEN LOWER(users.name) = :query THEN 2 ELSE 3 END SQL @@ -1081,7 +1093,7 @@ class User < MainClusterwide::ApplicationRecord def otp_secret_expired? return true unless otp_secret_expires_at - otp_secret_expires_at < Time.current + otp_secret_expires_at.past? end def update_otp_secret! @@ -1446,7 +1458,7 @@ class User < MainClusterwide::ApplicationRecord if !Gitlab.config.ldap.enabled false elsif ldap_user? - !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current + !last_credential_check_at || (last_credential_check_at + ldap_sync_time).past? else false end @@ -2087,7 +2099,7 @@ class User < MainClusterwide::ApplicationRecord end def password_expired? - !!(password_expires_at && password_expires_at < Time.current) + !!(password_expires_at && password_expires_at.past?) end def password_expired_if_applicable? diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 728c1f4844a..5a592b425df 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -20,6 +20,7 @@ class UserCustomAttribute < ApplicationRecord TRUSTED_BY = 'trusted_by' AUTO_BANNED_BY = 'auto_banned_by' IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt' + IDENTITY_VERIFICATION_EXEMPT = 'identity_verification_exempt' class << self def upsert_custom_attributes(custom_attributes) diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index 9ac814eebda..bbb08ed5774 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -17,10 +17,24 @@ class UserDetail < MainClusterwide::ApplicationRecord DEFAULT_FIELD_LENGTH = 500 + MASTODON_VALIDATION_REGEX = / + \A # beginning of string + @?\b # optional leading at + ([\w\d.%+-]+) # character group to pick up words in user portion of username + @ # separator between user and host + ( # beginning of charagter group for host portion + [\w\d.-]+ # character group to pick up words in host portion of username + \.\w{2,} # pick up tld of host domain, 2 chars or more + )\b # end of character group to pick up words in host portion of username + \z # end of string + /x + validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validate :discord_format validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true + validates :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true + validate :mastodon_format validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true @@ -32,7 +46,7 @@ class UserDetail < MainClusterwide::ApplicationRecord enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true def sanitize_attrs - %i[discord linkedin skype twitter website_url].each do |attr| + %i[discord linkedin mastodon skype twitter website_url].each do |attr| value = self[attr] self[attr] = Sanitize.clean(value) if value.present? end @@ -49,6 +63,7 @@ class UserDetail < MainClusterwide::ApplicationRecord self.discord = '' if discord.nil? self.linkedin = '' if linkedin.nil? self.location = '' if location.nil? + self.mastodon = '' if mastodon.nil? self.organization = '' if organization.nil? self.skype = '' if skype.nil? self.twitter = '' if twitter.nil? @@ -62,4 +77,10 @@ def discord_format errors.add(:discord, _('must contain only a discord user ID.')) end +def mastodon_format + return if mastodon.blank? || mastodon =~ UserDetail::MASTODON_VALIDATION_REGEX + + errors.add(:mastodon, _('must contain only a mastodon username.')) +end + UserDetail.prepend_mod_with('UserDetail') diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 8fc9f4617d0..59cfe9a8426 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -7,6 +7,7 @@ class UserPreference < MainClusterwide::ApplicationRecord # enum options with same name for multiple fields, also it creates # extra methods that aren't really needed here. NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze + TIME_DISPLAY_FORMATS = { system: 0, non_iso_format: 1, iso_format: 2 }.freeze belongs_to :user @@ -27,12 +28,15 @@ class UserPreference < MainClusterwide::ApplicationRecord validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' } + validates :time_display_format, inclusion: { in: TIME_DISPLAY_FORMATS.values }, presence: true + ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' # 2023-06-22 is after 16.1 release and during 16.2 release https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#ignoring-the-column-release-m ignore_columns :use_legacy_web_ide, remove_with: '16.2', remove_after: '2023-06-22' attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT } attribute :time_display_relative, default: true + attribute :time_display_format, default: 0 attribute :render_whitespace_in_code, default: false attribute :project_shortcut_buttons, default: true attribute :keyboard_shortcuts_enabled, default: true @@ -80,6 +84,16 @@ class UserPreference < MainClusterwide::ApplicationRecord end end + class << self + def time_display_formats + { + s_('Time Display|System') => TIME_DISPLAY_FORMATS[:system], + s_('Time Display|12-hour: 2:34 PM') => TIME_DISPLAY_FORMATS[:non_iso_format], + s_('Time Display|24-hour: 14:34') => TIME_DISPLAY_FORMATS[:iso_format] + } + end + end + def time_display_relative value = read_attribute(:time_display_relative) return value unless value.nil? diff --git a/app/models/users/anonymous.rb b/app/models/users/anonymous.rb new file mode 100644 index 00000000000..b4a182ba203 --- /dev/null +++ b/app/models/users/anonymous.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Users + class Anonymous + class << self + def can?(action, subject = :global) + Ability.allowed?(nil, action, subject) + end + end + end +end diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 60dd89c3ee7..a9880e56e8c 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -65,18 +65,19 @@ module Users # 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314 # 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233 branch_rules_info_callout: 65, - create_runner_workflow_banner: 66, + # 66 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135470/ # 67 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920 project_repository_limit_alert_warning_threshold: 68, # EE-only project_repository_limit_alert_alert_threshold: 69, # EE-only project_repository_limit_alert_error_threshold: 70, # EE-only - new_navigation_callout: 71, + # 71 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134432 # 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022 namespace_over_storage_users_combined_alert: 73, # EE-only # 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751 vsd_feedback_banner: 75, # EE-only security_policy_protected_branch_modification: 76, # EE-only - vulnerability_report_grouping: 77 # EE-only + vulnerability_report_grouping: 77, # EE-only + new_nav_for_everyone_callout: 78 } validates :feature_name, diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 276d549006f..6d0a22c8b0a 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -2,10 +2,16 @@ module Users class CreditCardValidation < ApplicationRecord + include IgnorableColumns + RELEASE_DAY = Date.new(2021, 5, 17) self.table_name = 'user_credit_card_validations' + ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.8', remove_after: '2023-12-22' + + attr_accessor :last_digits, :network, :holder_name, :expiration_date + belongs_to :user belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id, inverse_of: :credit_card_validation diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb index 0bcfda049fc..d7c76e2ee2c 100644 --- a/app/models/users/group_visit.rb +++ b/app/models/users/group_visit.rb @@ -13,5 +13,12 @@ module Users validates :entity_id, presence: true validates :user_id, presence: true validates :visited_at, presence: true + + MAX_FRECENT_ITEMS = 3 + + def self.frecent_groups(user_id:) + ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id") + Group.find(ids) + end end end diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb index e033445d76b..2256eb8ddc4 100644 --- a/app/models/users/phone_number_validation.rb +++ b/app/models/users/phone_number_validation.rb @@ -41,6 +41,10 @@ module Users ).exists? end + def self.by_reference_id(ref_id) + find_by(telesign_reference_xid: ref_id) + end + def validated? validated_at.present? end diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb index 1d076e0be56..9ff3d8d2c91 100644 --- a/app/models/users/project_visit.rb +++ b/app/models/users/project_visit.rb @@ -13,5 +13,12 @@ module Users validates :entity_id, presence: true validates :user_id, presence: true validates :visited_at, presence: true + + MAX_FRECENT_ITEMS = 5 + + def self.frecent_projects(user_id:) + ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id") + Project.find(ids) + end end end diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb index e55d958d2b4..1401ce82045 100644 --- a/app/models/vs_code/settings/vs_code_setting.rb +++ b/app/models/vs_code/settings/vs_code_setting.rb @@ -5,7 +5,9 @@ module VsCode class VsCodeSetting < ApplicationRecord belongs_to :user, inverse_of: :vscode_settings - validates :setting_type, presence: true + validates :setting_type, presence: true, + inclusion: { in: SETTINGS_TYPES }, + uniqueness: { scope: :user_id } validates :content, presence: true scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) } diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 2eed693ca76..3dd8f334a68 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -80,6 +80,7 @@ class WikiPage alias_method :to_param, :slug def human_title + return front_matter_title if Feature.enabled?(:wiki_front_matter_title, container) && front_matter_title.present? return 'Home' if title == Wiki::HOMEPAGE title @@ -95,6 +96,10 @@ class WikiPage attributes[:title] = new_title end + def front_matter_title + front_matter[:title] + end + def raw_content attributes[:content] ||= page&.text_data end @@ -320,7 +325,7 @@ class WikiPage def serialize_front_matter(hash) return '' unless hash.present? - YAML.dump(hash.transform_keys(&:to_s)) + "---\n" + YAML.dump(hash.to_h.transform_keys(&:to_s)) + "---\n" end def update_front_matter(attrs) diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 0761a213532..a62d77939bf 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -73,6 +73,19 @@ class WorkItem < Issue includes(:parent_link).order(keyset_order) end + def linked_items_keyset_order + ::Gitlab::Pagination::Keyset::Order.build( + [ + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'issue_link_id', + column_expression: IssueLink.arel_table[:id], + order_expression: IssueLink.arel_table[:id].asc, + nullable: :not_nullable, + distinct: true + ) + ]) + end + override :related_link_class def related_link_class WorkItems::RelatedWorkItemLink @@ -150,7 +163,9 @@ class WorkItem < Issue def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil) return [] if new_record? - linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id') + linked_work_items = linked_work_items_query(link_type) + .preload(preload) + .reorder(self.class.linked_items_keyset_order) return linked_work_items unless authorize cross_project_filter = ->(work_items) { work_items.where(project: project) } |