summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/analytics/cycle_analytics/issue_stage_event.rb13
-rw-r--r--app/models/analytics/cycle_analytics/merge_request_stage_event.rb13
-rw-r--r--app/models/application_record.rb8
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/application_setting_implementation.rb7
-rw-r--r--app/models/audit_event.rb6
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/blob_viewer/package_json.rb14
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/chat_name.rb4
-rw-r--r--app/models/ci/application_record.rb5
-rw-r--r--app/models/ci/build.rb58
-rw-r--r--app/models/ci/build_metadata.rb12
-rw-r--r--app/models/ci/build_need.rb8
-rw-r--r--app/models/ci/build_runner_session.rb2
-rw-r--r--app/models/ci/build_trace_chunk.rb1
-rw-r--r--app/models/ci/ci_database_record.rb17
-rw-r--r--app/models/ci/instance_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb22
-rw-r--r--app/models/ci/pipeline.rb110
-rw-r--r--app/models/ci/runner.rb34
-rw-r--r--app/models/ci/sources/pipeline.rb3
-rw-r--r--app/models/ci/stage.rb1
-rw-r--r--app/models/ci/trigger.rb4
-rw-r--r--app/models/clusters/agents/group_authorization.rb2
-rw-r--r--app/models/clusters/agents/project_authorization.rb2
-rw-r--r--app/models/clusters/applications/runner.rb4
-rw-r--r--app/models/clusters/cluster.rb8
-rw-r--r--app/models/clusters/integrations/prometheus.rb7
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/alert_event_lifecycle.rb2
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb28
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb6
-rw-r--r--app/models/concerns/ci/contextable.rb33
-rw-r--r--app/models/concerns/ci/has_status.rb17
-rw-r--r--app/models/concerns/ci/metadatable.rb3
-rw-r--r--app/models/concerns/clusters/agents/authorization_config_scopes.rb25
-rw-r--r--app/models/concerns/database_reflection.rb21
-rw-r--r--app/models/concerns/enums/vulnerability.rb13
-rw-r--r--app/models/concerns/file_store_mounter.rb8
-rw-r--r--app/models/concerns/has_integrations.rb19
-rw-r--r--app/models/concerns/has_user_type.rb1
-rw-r--r--app/models/concerns/integrations/push_data_validations.rb44
-rw-r--r--app/models/concerns/integrations/reactively_cached.rb15
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/legacy_bulk_insert.rb54
-rw-r--r--app/models/concerns/loaded_in_group_list.rb2
-rw-r--r--app/models/concerns/loose_foreign_key.rb22
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb25
-rw-r--r--app/models/concerns/milestoneable.rb3
-rw-r--r--app/models/concerns/noteable.rb33
-rw-r--r--app/models/concerns/reactive_service.rb13
-rw-r--r--app/models/concerns/security/latest_pipeline_information.rb36
-rw-r--r--app/models/concerns/service_push_data_validations.rb43
-rw-r--r--app/models/concerns/sha256_attribute.rb2
-rw-r--r--app/models/concerns/sha_attribute.rb2
-rw-r--r--app/models/concerns/strip_attribute.rb3
-rw-r--r--app/models/concerns/timebox.rb16
-rw-r--r--app/models/concerns/transactions.rb28
-rw-r--r--app/models/concerns/ttl_expirable.rb7
-rw-r--r--app/models/concerns/update_highest_role.rb2
-rw-r--r--app/models/concerns/x509_serial_number_attribute.rb2
-rw-r--r--app/models/custom_emoji.rb2
-rw-r--r--app/models/customer_relations/contact.rb3
-rw-r--r--app/models/customer_relations/issue_contact.rb20
-rw-r--r--app/models/data_list.rb14
-rw-r--r--app/models/dependency_proxy/blob.rb2
-rw-r--r--app/models/dependency_proxy/manifest.rb12
-rw-r--r--app/models/deploy_key.rb8
-rw-r--r--app/models/deployment.rb20
-rw-r--r--app/models/design_management/version.rb2
-rw-r--r--app/models/email.rb12
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/error_tracking/error.rb7
-rw-r--r--app/models/error_tracking/error_event.rb8
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/gpg_signature.rb3
-rw-r--r--app/models/group.rb30
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb7
-rw-r--r--app/models/integrations/buildkite.rb2
-rw-r--r--app/models/integrations/discord.rb7
-rw-r--r--app/models/integrations/drone_ci.rb4
-rw-r--r--app/models/integrations/emails_on_push.rb9
-rw-r--r--app/models/integrations/hangouts_chat.rb7
-rw-r--r--app/models/integrations/jira.rb15
-rw-r--r--app/models/integrations/microsoft_teams.rb7
-rw-r--r--app/models/integrations/pipelines_email.rb18
-rw-r--r--app/models/integrations/shimo.rb47
-rw-r--r--app/models/integrations/teamcity.rb4
-rw-r--r--app/models/integrations/unify_circuit.rb7
-rw-r--r--app/models/integrations/webex_teams.rb7
-rw-r--r--app/models/integrations/zentao.rb34
-rw-r--r--app/models/issue.rb10
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/label_link.rb12
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb29
-rw-r--r--app/models/loose_foreign_keys/modification_tracker.rb51
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/member_task.rb43
-rw-r--r--app/models/members/project_member.rb1
-rw-r--r--app/models/merge_request.rb27
-rw-r--r--app/models/merge_request_assignee.rb6
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb8
-rw-r--r--app/models/merge_request_diff_commit.rb25
-rw-r--r--app/models/merge_request_reviewer.rb13
-rw-r--r--app/models/namespace.rb59
-rw-r--r--app/models/namespaces/project_namespace.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb3
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb78
-rw-r--r--app/models/namespaces/traversal/recursive_scopes.rb7
-rw-r--r--app/models/namespaces/user_namespace.rb20
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/packages/npm.rb4
-rw-r--r--app/models/packages/npm/metadatum.rb25
-rw-r--r--app/models/packages/package.rb8
-rw-r--r--app/models/packages/package_file.rb3
-rw-r--r--app/models/preloaders/group_policy_preloader.rb23
-rw-r--r--app/models/preloaders/group_root_ancestor_preloader.rb32
-rw-r--r--app/models/preloaders/user_max_access_level_in_groups_preloader.rb29
-rw-r--r--app/models/project.rb80
-rw-r--r--app/models/project_authorization.rb5
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_team.rb6
-rw-r--r--app/models/projects/topic.rb2
-rw-r--r--app/models/push_event_payload.rb3
-rw-r--r--app/models/release.rb1
-rw-r--r--app/models/repository.rb41
-rw-r--r--app/models/suggestion.rb1
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/upload.rb11
-rw-r--r--app/models/uploads/fog.rb14
-rw-r--r--app/models/uploads/local.rb2
-rw-r--r--app/models/user.rb103
-rw-r--r--app/models/user_status.rb4
-rw-r--r--app/models/users/credit_card_validation.rb3
-rw-r--r--app/models/users/in_product_marketing_email.rb3
-rw-r--r--app/models/users_statistics.rb36
-rw-r--r--app/models/webauthn_registration.rb3
142 files changed, 1523 insertions, 592 deletions
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
index 3e6ed86d534..837eb35c839 100644
--- a/app/models/analytics/cycle_analytics/issue_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -8,9 +8,22 @@ module Analytics
validates(*%i[stage_event_hash_id issue_id group_id project_id start_event_timestamp], presence: true)
+ alias_attribute :state, :state_id
+ enum state: Issue.available_states, _suffix: true
+
+ scope :assigned_to, ->(user) do
+ assignees_class = IssueAssignee
+ condition = assignees_class.where(user_id: user).where(arel_table[:issue_id].eq(assignees_class.arel_table[:issue_id]))
+ where(condition.arel.exists)
+ end
+
def self.issuable_id_column
:issue_id
end
+
+ def self.issuable_model
+ ::Issue
+ end
end
end
end
diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
index d0ec3c4e8b9..0dfa322b2c3 100644
--- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -8,9 +8,22 @@ module Analytics
validates(*%i[stage_event_hash_id merge_request_id group_id project_id start_event_timestamp], presence: true)
+ alias_attribute :state, :state_id
+ enum state: MergeRequest.available_states, _suffix: true
+
+ scope :assigned_to, ->(user) do
+ assignees_class = MergeRequestAssignee
+ condition = assignees_class.where(user_id: user).where(arel_table[:merge_request_id].eq(assignees_class.arel_table[:merge_request_id]))
+ where(condition.arel.exists)
+ end
+
def self.issuable_id_column
:merge_request_id
end
+
+ def self.issuable_model
+ ::MergeRequest
+ end
end
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index d2757d8c17d..bcd8bdd6638 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
- self.gitlab_schema = :gitlab_main
+ include DatabaseReflection
+ include Transactions
+ include LegacyBulkInsert
+
self.abstract_class = true
alias_method :reset, :reload
@@ -92,8 +95,7 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.declarative_enum(enum_mod)
- values = enum_mod.definition.transform_values { |v| v[:value] }
- enum(enum_mod.key => values)
+ enum(enum_mod.key => enum_mod.values)
end
def self.cached_column_list
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5a8cbd8d71c..af5796d682f 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -536,6 +536,18 @@ class ApplicationSetting < ApplicationRecord
validates :sidekiq_job_limiter_limit_bytes,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :sentry_enabled,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+ validates :sentry_dsn,
+ addressable_url: true, presence: true, length: { maximum: 255 },
+ if: :sentry_enabled?
+ validates :sentry_clientside_dsn,
+ addressable_url: true, allow_blank: true, length: { maximum: 255 },
+ if: :sentry_enabled?
+ validates :sentry_environment,
+ presence: true, length: { maximum: 255 },
+ if: :sentry_enabled?
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 7bdea36bb8a..54ec8b2c3e4 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -146,6 +146,9 @@ module ApplicationSettingImplementation
session_expire_delay: Settings.gitlab['session_expire_delay'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
+ sidekiq_job_limiter_mode: Gitlab::SidekiqMiddleware::SizeLimiter::Validator::COMPRESS_MODE,
+ sidekiq_job_limiter_compression_threshold_bytes: Gitlab::SidekiqMiddleware::SizeLimiter::Validator::DEFAULT_COMPRESSION_THRESHOLD_BYTES,
+ sidekiq_job_limiter_limit_bytes: Gitlab::SidekiqMiddleware::SizeLimiter::Validator::DEFAULT_SIZE_LIMIT,
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
snippet_size_limit: 50.megabytes,
@@ -241,11 +244,11 @@ module ApplicationSettingImplementation
end
def home_page_url_column_exists?
- ::Gitlab::Database.main.cached_column_exists?(:application_settings, :home_page_url)
+ ApplicationSetting.database.cached_column_exists?(:home_page_url)
end
def help_page_support_url_column_exists?
- ::Gitlab::Database.main.cached_column_exists?(:application_settings, :help_page_support_url)
+ ApplicationSetting.database.cached_column_exists?(:help_page_support_url)
end
def disabled_oauth_sign_in_sources=(sources)
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index a1c6793607f..1a8bd05c42c 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -30,6 +30,8 @@ class AuditEvent < ApplicationRecord
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
+ scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) }
+ scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) }
after_initialize :initialize_details
@@ -106,6 +108,10 @@ class AuditEvent < ApplicationRecord
self[name] = self.details[name] = original
end
end
+
+ def self.find_user_id(username)
+ User.find_by_username(username)&.id
+ end
end
AuditEvent.prepend_mod_with('AuditEvent')
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index c8f6b9aaedb..b665f3d5d8c 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -14,7 +14,7 @@ class AwardEmoji < ApplicationRecord
validates :user, presence: true
validates :awardable, presence: true, unless: :importing?
- validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names }
+ validates :name, presence: true, 'gitlab/emoji_name': true
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }, unless: -> { ghost_user? || importing? }
participant :user
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index 7cae60a74d6..1d10cc82a85 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -7,11 +7,15 @@ module BlobViewer
self.file_types = %i(package_json)
def manager_name
- 'npm'
+ yarn? ? 'yarn' : 'npm'
+ end
+
+ def yarn?
+ json_data['engines'].present? && json_data['engines']['yarn'].present?
end
def manager_url
- 'https://www.npmjs.com/'
+ yarn? ? 'https://yarnpkg.com/' : 'https://www.npmjs.com/'
end
def package_name
@@ -38,7 +42,11 @@ module BlobViewer
end
def npm_url
- "https://www.npmjs.com/package/#{package_name}"
+ if yarn?
+ "https://yarnpkg.com/package/#{package_name}"
+ else
+ "https://www.npmjs.com/package/#{package_name}"
+ end
end
end
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index ecac4ab95f4..2368be6196c 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -112,6 +112,10 @@ class BulkImports::Entity < ApplicationRecord
@export_relations_url_path ||= EXPORT_RELATIONS_URL % { resource: pluralized_name, full_path: encoded_source_full_path }
end
+ def relation_download_url_path(relation)
+ "#{export_relations_url_path}/download?relation=#{relation}"
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index ff3f2663b73..da7312df18b 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ChatName < ApplicationRecord
+ include LooseForeignKey
+
LAST_USED_AT_INTERVAL = 1.hour
belongs_to :integration, foreign_key: :service_id
@@ -14,6 +16,8 @@ class ChatName < ApplicationRecord
validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+ loose_foreign_key :ci_pipeline_chat_data, :chat_name_id, on_delete: :async_delete
+
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
#
diff --git a/app/models/ci/application_record.rb b/app/models/ci/application_record.rb
index 913e7a62c66..ea7b1104e36 100644
--- a/app/models/ci/application_record.rb
+++ b/app/models/ci/application_record.rb
@@ -2,9 +2,12 @@
module Ci
class ApplicationRecord < ::ApplicationRecord
- self.gitlab_schema = :gitlab_ci
self.abstract_class = true
+ if Gitlab::Database.has_config?(:ci)
+ connects_to database: { writing: :ci, reading: :ci }
+ end
+
def self.table_name_prefix
'ci_'
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 990ef71a457..3fdc44bccf3 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -10,7 +10,6 @@ module Ci
include Presentable
include Importable
include Ci::HasRef
- include IgnorableColumns
BuildArchivedError = Class.new(StandardError)
@@ -70,9 +69,6 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
- ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
- ignore_columns :stage_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
-
##
# Since Gitlab 11.5, deployments records started being created right after
# `ci_builds` creation. We can look up a relevant `environment` through
@@ -175,6 +171,7 @@ module Ci
scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
+ scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
scope :with_secure_reports_from_config_options, -> (job_types) do
joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
@@ -313,12 +310,6 @@ module Ci
end
after_transition pending: :running do |build|
- unless build.update_deployment_after_transaction_commit?
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment&.run
- end
- end
-
build.run_after_commit do
build.pipeline.persistent_ref.create
@@ -339,12 +330,6 @@ module Ci
end
after_transition any => [:success] do |build|
- unless build.update_deployment_after_transaction_commit?
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment&.succeed
- end
- end
-
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
@@ -353,23 +338,6 @@ module Ci
after_transition any => [:failed] do |build|
next unless build.project
- next unless build.deployment
-
- unless build.update_deployment_after_transaction_commit?
- begin
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment.drop!
- end
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id)
- end
- end
-
- true
- end
-
- after_transition any => [:failed] do |build|
- next unless build.project
if build.auto_retry_allowed?
begin
@@ -380,25 +348,12 @@ module Ci
end
end
- after_transition any => [:skipped, :canceled] do |build, transition|
- unless build.update_deployment_after_transaction_commit?
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- if transition.to_name == :skipped
- build.deployment&.skip
- else
- build.deployment&.cancel
- end
- end
- end
- end
-
# Synchronize Deployment Status
# Please note that the data integirty is not assured because we can't use
# a database transaction due to DB decomposition.
after_transition do |build, transition|
next if transition.loopback?
next unless build.project
- next unless build.update_deployment_after_transaction_commit?
build.run_after_commit do
build.deployment&.sync_status_with(build)
@@ -585,7 +540,6 @@ module Ci
.concat(persisted_variables)
.concat(dependency_proxy_variables)
.concat(job_jwt_variables)
- .concat(kubernetes_variables)
.concat(scoped_variables)
.concat(job_variables)
.concat(persisted_environment_variables)
@@ -1120,12 +1074,6 @@ module Ci
runner&.instance_type?
end
- def update_deployment_after_transaction_commit?
- strong_memoize(:update_deployment_after_transaction_commit) do
- Feature.enabled?(:update_deployment_after_transaction_commit, project, default_enabled: :yaml)
- end
- end
-
protected
def run_status_commit_hooks!
@@ -1213,10 +1161,6 @@ module Ci
end
end
- def kubernetes_variables
- [] # Overridden in EE
- end
-
def conditionally_allow_failure!(exit_code)
return unless exit_code
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 0d6d6f7a6a5..ca68989002c 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -23,6 +23,7 @@ module Ci
serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :runtime_runner_features, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout
@@ -37,8 +38,7 @@ module Ci
job_timeout_source: 4
}
- ignore_column :build_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
- ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
+ ignore_columns :runner_features, remove_with: '14.7', remove_after: '2021-11-22'
def update_timeout_state
timeout = timeout_with_highest_precedence
@@ -48,6 +48,14 @@ module Ci
update(timeout: timeout.value, timeout_source: timeout.source)
end
+ def set_cancel_gracefully
+ runtime_runner_features.merge!( { cancel_gracefully: true } )
+ end
+
+ def cancel_gracefully?
+ runtime_runner_features[:cancel_gracefully] == true
+ end
+
private
def set_build_project
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index bf1470ca20f..d4cbbfac4ab 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -3,7 +3,6 @@
module Ci
class BuildNeed < Ci::ApplicationRecord
include BulkInsertSafe
- include IgnorableColumns
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
@@ -13,12 +12,5 @@ module Ci
scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
scope :artifacts, -> { where(artifacts: true) }
-
- # TODO: Remove once build_id_convert_to_bigint is not an "ignored" column anymore (see .ignore_columns above)
- # There is a database-side trigger to populate this column. This is unexpected in the context
- # of cloning an instance, e.g. when retrying the job. Hence we exclude the ignored column explicitly here.
- def attributes
- super.except('build_id_convert_to_bigint')
- end
end
end
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index e12c0f82c99..c6dbb5d0a43 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -4,8 +4,6 @@ module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < Ci::ApplicationRecord
- include IgnorableColumns
-
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
DEFAULT_PORT_NAME = 'default_port'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 6edb5ef4579..221a2284106 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -7,7 +7,6 @@ module Ci
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::OptimisticLocking
- include IgnorableColumns
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
diff --git a/app/models/ci/ci_database_record.rb b/app/models/ci/ci_database_record.rb
deleted file mode 100644
index e2b832a28e7..00000000000
--- a/app/models/ci/ci_database_record.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- # TODO: https://gitlab.com/groups/gitlab-org/-/epics/6168
- #
- # Do not use this yet outside of `ci_instance_variables`.
- # This class is part of a migration to move all CI classes to a new separate database.
- # Initially we are only going to be moving the `Ci::InstanceVariable` model and it will be duplicated in the main and CI tables
- # Do not extend this class in any other models.
- class CiDatabaseRecord < Ci::ApplicationRecord
- self.abstract_class = true
-
- if Gitlab::Database.has_config?(:ci)
- connects_to database: { writing: :ci, reading: :ci }
- end
- end
-end
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index f4aa935b983..da9d4dea537 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Ci
- class InstanceVariable < Ci::CiDatabaseRecord
+ class InstanceVariable < Ci::ApplicationRecord
extend Gitlab::ProcessMemoryCache::Helper
include Ci::NewHasVariable
include Ci::Maskable
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index ad3e867f9d5..ec1137920ef 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -10,9 +10,7 @@ module Ci
include Artifactable
include FileStoreMounter
include EachBatch
- include IgnorableColumns
-
- ignore_columns %i[id_convert_to_bigint job_id_convert_to_bigint], remove_with: '14.5', remove_after: '2021-11-22'
+ include Gitlab::Utils::StrongMemoize
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
@@ -124,6 +122,9 @@ module Ci
mount_file_store_uploader JobArtifactUploader
+ skip_callback :save, :after, :store_file!, if: :store_after_commit?
+ after_commit :store_file_after_commit!, on: [:create, :update], if: :store_after_commit?
+
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_file_format!, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
@@ -338,8 +339,23 @@ module Ci
}
end
+ def store_after_commit?
+ strong_memoize(:store_after_commit) do
+ trace? &&
+ JobArtifactUploader.direct_upload_enabled? &&
+ Feature.enabled?(:ci_store_trace_outside_transaction, project, default_enabled: :yaml)
+ end
+ end
+
private
+ def store_file_after_commit!
+ return unless previous_changes.key?(:file)
+
+ store_file!
+ update_file_store
+ end
+
def set_size
self.size = file.size
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0041ec5135c..a29aa756e38 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -71,7 +71,7 @@ module Ci
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
has_many :deployments, through: :builds
- has_many :environments, -> { distinct }, through: :deployments
+ has_many :environments, -> { distinct.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338658') }, through: :deployments
has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
has_many :downloadable_artifacts, -> do
not_expired.or(where_exists(::Ci::Pipeline.artifacts_locked.where('ci_pipelines.id = ci_builds.commit_id'))).downloadable.with_job
@@ -652,8 +652,15 @@ module Ci
end
def batch_lookup_report_artifact_for_file_type(file_type)
+ batch_lookup_report_artifact_for_file_types([file_type])
+ end
+
+ def batch_lookup_report_artifact_for_file_types(file_types)
+ file_types_to_search = []
+ file_types.each { |file_type| file_types_to_search.append(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s)) }
+
latest_report_artifacts
- .values_at(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s))
+ .values_at(*file_types_to_search.uniq)
.flatten
.compact
.last
@@ -684,7 +691,9 @@ module Ci
end
def freeze_period?
- Ci::FreezePeriodStatus.new(project: project).execute
+ strong_memoize(:freeze_period) do
+ Ci::FreezePeriodStatus.new(project: project).execute
+ end
end
def has_warnings?
@@ -780,6 +789,10 @@ module Ci
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
+ def variables_builder
+ @variables_builder ||= ::Gitlab::Ci::Variables::Builder.new(self)
+ end
+
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless persisted?
@@ -796,20 +809,7 @@ module Ci
variables.append(key: 'CI_PIPELINE_CREATED_AT', value: created_at&.iso8601)
variables.concat(predefined_commit_variables)
-
- if merge_request?
- variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
- variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
- variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
-
- diff = self.merge_request_diff
- if diff.present?
- variables.append(key: 'CI_MERGE_REQUEST_DIFF_ID', value: diff.id.to_s)
- variables.append(key: 'CI_MERGE_REQUEST_DIFF_BASE_SHA', value: diff.base_commit_sha)
- end
-
- variables.concat(merge_request.predefined_variables)
- end
+ variables.concat(predefined_merge_request_variables)
if open_merge_requests_refs.any?
variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(','))
@@ -825,27 +825,49 @@ module Ci
end
def predefined_commit_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_COMMIT_SHA', value: sha)
- variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
- variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
- variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
- variables.append(key: 'CI_COMMIT_BRANCH', value: ref) if branch?
- variables.append(key: 'CI_COMMIT_TAG', value: ref) if tag?
- variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
- variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
- variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
- variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
- variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)
- variables.append(key: 'CI_COMMIT_AUTHOR', value: git_author_full_text.to_s)
-
- # legacy variables
- variables.append(key: 'CI_BUILD_REF', value: sha)
- variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
- variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
- variables.append(key: 'CI_BUILD_TAG', value: ref) if tag?
+ strong_memoize(:predefined_commit_variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_COMMIT_SHA', value: sha)
+ variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
+ variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
+ variables.append(key: 'CI_COMMIT_BRANCH', value: ref) if branch?
+ variables.append(key: 'CI_COMMIT_TAG', value: ref) if tag?
+ variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+ variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
+ variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)
+ variables.append(key: 'CI_COMMIT_AUTHOR', value: git_author_full_text.to_s)
+
+ # legacy variables
+ variables.append(key: 'CI_BUILD_REF', value: sha)
+ variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
+ variables.append(key: 'CI_BUILD_TAG', value: ref) if tag?
+ end
+ end
+ end
+
+ def predefined_merge_request_variables
+ strong_memoize(:predefined_merge_request_variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ next variables unless merge_request?
+
+ variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
+
+ diff = self.merge_request_diff
+ if diff.present?
+ variables.append(key: 'CI_MERGE_REQUEST_DIFF_ID', value: diff.id.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_DIFF_BASE_SHA', value: diff.base_commit_sha)
+ end
+
+ variables.concat(merge_request.predefined_variables)
+ end
end
end
@@ -1254,6 +1276,18 @@ module Ci
self.builds.latest.build_matchers(project)
end
+ def predefined_vars_in_builder_enabled?
+ strong_memoize(:predefined_vars_in_builder_enabled) do
+ Feature.enabled?(:ci_predefined_vars_in_builder, project, default_enabled: :yaml)
+ end
+ end
+
+ def authorized_cluster_agents
+ strong_memoize(:authorized_cluster_agents) do
+ ::Clusters::AgentAuthorizationsFinder.new(project).execute.map(&:agent)
+ end
+ end
+
private
def add_message(severity, content)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 2f718ad7582..8a3025e5608 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -12,6 +12,7 @@ module Ci
include Gitlab::Utils::StrongMemoize
include TaggableQueries
include Presentable
+ include LooseForeignKey
add_authentication_token_field :token, encrypted: :optional
@@ -82,7 +83,9 @@ module Ci
groups = Gitlab::ObjectHierarchy.new(groups).base_and_ancestors
end
- joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
+ joins(:runner_namespaces)
+ .where(ci_runner_namespaces: { namespace_id: groups })
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
}
scope :belonging_to_group_or_project, -> (group_id, project_id) {
@@ -94,13 +97,16 @@ module Ci
union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql
from("(#{union_sql}) #{table_name}")
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
}
scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors
- joins(:groups).where(namespaces: { id: hierarchy_groups })
+ joins(:groups)
+ .where(namespaces: { id: hierarchy_groups })
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
}
scope :owned_or_instance_wide, -> (project_id) do
@@ -111,7 +117,7 @@ module Ci
instance_type
],
remove_duplicates: false
- )
+ ).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
end
scope :assignable_for, ->(project) do
@@ -162,6 +168,8 @@ module Ci
validates :config, json_schema: { filename: 'ci_runner_config' }
+ loose_foreign_key :clusters_applications_runners, :runner_id, on_delete: :async_nullify
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL for the description field and performs a full match on tokens.
@@ -266,6 +274,14 @@ module Ci
end
def status
+ return :not_connected unless contacted_at
+
+ online? ? :online : :offline
+ end
+
+ # DEPRECATED
+ # TODO Remove in %15.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
+ def deprecated_rest_status
if contacted_at.nil?
:not_connected
elsif active?
@@ -436,10 +452,8 @@ module Ci
end
def no_groups
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
- if groups.any?
- errors.add(:runner, 'cannot have groups assigned')
- end
+ if runner_namespaces.any?
+ errors.add(:runner, 'cannot have groups assigned')
end
end
@@ -450,10 +464,8 @@ module Ci
end
def exactly_one_group
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
- unless groups.one?
- errors.add(:runner, 'needs to be assigned to exactly one group')
- end
+ unless runner_namespaces.one?
+ errors.add(:runner, 'needs to be assigned to exactly one group')
end
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 95842d944f9..f78caf710a6 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -4,9 +4,6 @@ module Ci
module Sources
class Pipeline < Ci::ApplicationRecord
include Ci::NamespacedModelName
- include IgnorableColumns
-
- ignore_columns 'source_job_id_convert_to_bigint', remove_with: '14.5', remove_after: '2021-11-22'
self.table_name = "ci_sources_pipelines"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 131e18adf62..e2b15497638 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -6,7 +6,6 @@ module Ci
include Ci::HasStatus
include Gitlab::OptimisticLocking
include Presentable
- include IgnorableColumns
enum status: Ci::HasStatus::STATUSES_ENUM
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 595315f14ab..5bf5ae51ec8 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -3,6 +3,10 @@
module Ci
class Trigger < Ci::ApplicationRecord
include Presentable
+ include Limitable
+
+ self.limit_name = 'pipeline_triggers'
+ self.limit_scope = :project
belongs_to :project
belongs_to :owner, class_name: "User"
diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb
index 28a711aaf17..58ba874ab53 100644
--- a/app/models/clusters/agents/group_authorization.rb
+++ b/app/models/clusters/agents/group_authorization.rb
@@ -3,6 +3,8 @@
module Clusters
module Agents
class GroupAuthorization < ApplicationRecord
+ include ::Clusters::Agents::AuthorizationConfigScopes
+
self.table_name = 'agent_group_authorizations'
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb
index f6d19086751..b9b44741936 100644
--- a/app/models/clusters/agents/project_authorization.rb
+++ b/app/models/clusters/agents/project_authorization.rb
@@ -3,6 +3,8 @@
module Clusters
module Agents
class ProjectAuthorization < ApplicationRecord
+ include ::Clusters::Agents::AuthorizationConfigScopes
+
self.table_name = 'agent_project_authorizations'
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 7cef92ce81a..59a9251d6b7 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.31.0'
+ VERSION = '0.34.0'
self.table_name = 'clusters_applications_runners'
@@ -70,7 +70,7 @@ module Clusters
}
if cluster.group_type?
- attributes[:groups] = [group]
+ attributes[:runner_namespaces] = [::Ci::RunnerNamespace.new(namespace: group)]
elsif cluster.project_type?
attributes[:runner_projects] = [::Ci::RunnerProject.new(project: project)]
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index feac7bbc363..87afa9f9491 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -139,8 +139,6 @@ module Clusters
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
- scope :preload_elasticstack, -> { preload(:integration_elastic_stack) }
- scope :preload_environments, -> { preload(:environments) }
scope :managed, -> { where(managed: true) }
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
@@ -150,9 +148,7 @@ module Clusters
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
scope :with_name, -> (name) { where(name: name) }
- # with_application_prometheus scope is deprecated, and scheduled for removal
- # in %14.0. See https://gitlab.com/groups/gitlab-org/-/epics/4280
- scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) }
+ scope :with_integration_prometheus, -> { includes(:integration_prometheus).joins(:integration_prometheus) }
scope :with_project_http_integrations, -> (project_ids) do
conditions = { projects: :alert_management_http_integrations }
includes(conditions).joins(conditions).where(projects: { id: project_ids })
@@ -311,7 +307,7 @@ module Clusters
end
def kubeclient
- platform_kubernetes.kubeclient if kubernetes?
+ platform_kubernetes&.kubeclient if kubernetes?
end
def elastic_stack_adapter
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index d745a49afc1..8b21fa351a3 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -14,6 +14,13 @@ module Clusters
validates :cluster, presence: true
validates :enabled, inclusion: { in: [true, false] }
+ # Periodically checked and kept up to date for Monitor demo projects
+ enum health_status: {
+ unknown: 0,
+ healthy: 1,
+ unhealthy: 2
+ }
+
attr_encrypted :alert_manager_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 43427e2ebc7..d75f7984e2c 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -48,7 +48,7 @@ class CommitStatus < Ci::ApplicationRecord
scope :ordered, -> { order(:name) }
scope :ordered_by_stage, -> { order(stage_idx: :asc) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
- scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
+ scope :retried_ordered, -> { retried.order(name: :asc, id: :desc).includes(project: :namespace) }
scope :ordered_by_pipeline, -> { order(pipeline_id: :asc) }
scope :before_stage, -> (index) { where('stage_idx < ?', index) }
scope :for_stage, -> (index) { where(stage_idx: index) }
diff --git a/app/models/concerns/alert_event_lifecycle.rb b/app/models/concerns/alert_event_lifecycle.rb
index 4d2b717ead2..72fe7757b44 100644
--- a/app/models/concerns/alert_event_lifecycle.rb
+++ b/app/models/concerns/alert_event_lifecycle.rb
@@ -41,8 +41,6 @@ module AlertEventLifecycle
scope :firing, -> { where(status: status_value_for(:firing)) }
scope :resolved, -> { where(status: status_value_for(:resolved)) }
- scope :count_by_project_id, -> { group(:project_id).count }
-
def self.status_value_for(name)
state_machines[:status].states[name].value
end
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 7462e1e828b..324e0fb57cb 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -5,6 +5,23 @@ module Analytics
module StageEventModel
extend ActiveSupport::Concern
+ included do
+ scope :by_stage_event_hash_id, ->(id) { where(stage_event_hash_id: id) }
+ scope :by_project_id, ->(id) { where(project_id: id) }
+ scope :by_group_id, ->(id) { where(group_id: id) }
+ scope :end_event_timestamp_after, -> (date) { where(arel_table[:end_event_timestamp].gteq(date)) }
+ scope :end_event_timestamp_before, -> (date) { where(arel_table[:end_event_timestamp].lteq(date)) }
+ scope :start_event_timestamp_after, -> (date) { where(arel_table[:start_event_timestamp].gteq(date)) }
+ scope :start_event_timestamp_before, -> (date) { where(arel_table[:start_event_timestamp].lteq(date)) }
+ scope :authored, ->(user) { where(author_id: user) }
+ scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
+ scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
+ end
+
+ def issuable_id
+ attributes[self.class.issuable_id_column.to_s]
+ end
+
class_methods do
def upsert_data(data)
upsert_values = data.map do |row|
@@ -13,8 +30,9 @@ module Analytics
:issuable_id,
:group_id,
:project_id,
- :author_id,
:milestone_id,
+ :author_id,
+ :state_id,
:start_event_timestamp,
:end_event_timestamp
)
@@ -31,6 +49,7 @@ module Analytics
project_id,
milestone_id,
author_id,
+ state_id,
start_event_timestamp,
end_event_timestamp
)
@@ -39,10 +58,11 @@ module Analytics
DO UPDATE SET
group_id = excluded.group_id,
project_id = excluded.project_id,
- start_event_timestamp = excluded.start_event_timestamp,
- end_event_timestamp = excluded.end_event_timestamp,
milestone_id = excluded.milestone_id,
- author_id = excluded.author_id
+ author_id = excluded.author_id,
+ state_id = excluded.state_id,
+ start_event_timestamp = excluded.start_event_timestamp,
+ end_event_timestamp = excluded.end_event_timestamp
SQL
result = connection.execute(query)
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index e58e5ddc966..731729a1ed5 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -127,7 +127,7 @@ module CascadingNamespaceSettingAttribute
end
def alias_boolean(attribute)
- return unless Gitlab::Database.main.exists? && type_for_attribute(attribute).type == :boolean
+ return unless database.exists? && type_for_attribute(attribute).type == :boolean
alias_method :"#{attribute}?", attribute
end
@@ -176,10 +176,10 @@ module CascadingNamespaceSettingAttribute
private
def locked_value(attribute)
+ return application_setting_value(attribute) if locked_by_application_setting?(attribute)
+
ancestor = locked_ancestor(attribute)
return ancestor.read_attribute(attribute) if ancestor
-
- Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
end
def locked_ancestor(attribute)
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 27a704c1de0..a9589cea5e9 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -10,11 +10,14 @@ module Ci
# Variables in the environment name scope.
#
def scoped_variables(environment: expanded_environment_name, dependencies: true)
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.concat(predefined_variables)
+ track_duration do
+ variables = pipeline.variables_builder.scoped_variables(self, environment: environment, dependencies: dependencies)
+
+ variables.concat(predefined_variables) unless pipeline.predefined_vars_in_builder_enabled?
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runnable? && runner
+ variables.concat(kubernetes_variables)
variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
@@ -25,9 +28,23 @@ module Ci
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
+
+ variables
end
end
+ def track_duration
+ start_time = ::Gitlab::Metrics::System.monotonic_time
+ result = yield
+ duration = ::Gitlab::Metrics::System.monotonic_time - start_time
+
+ ::Gitlab::Ci::Pipeline::Metrics
+ .pipeline_builder_scoped_variables_histogram
+ .observe({}, duration.seconds)
+
+ result
+ end
+
##
# Variables that do not depend on the environment name.
#
@@ -72,6 +89,18 @@ module Ci
end
end
+ def kubernetes_variables
+ ::Gitlab::Ci::Variables::Collection.new.tap do |collection|
+ # Should get merged with the cluster kubeconfig in deployment_variables, see
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/335089
+ template = ::Ci::GenerateKubeconfigService.new(self).execute
+
+ if template.valid?
+ collection.append(key: 'KUBECONFIG', value: template.to_yaml, public: false, file: true)
+ end
+ end
+ end
+
def deployment_variables(environment:)
return [] unless environment
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 8d715279da8..ccaccec3b6b 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -14,21 +14,8 @@ module Ci
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.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
- STATUSES_DESCRIPTION = {
- created: 'Pipeline has been created',
- waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable',
- preparing: 'Pipeline is preparing to run',
- pending: 'Pipeline has not started running yet',
- running: 'Pipeline is running',
- failed: 'At least one stage of the pipeline failed',
- success: 'Pipeline completed successfully',
- canceled: 'Pipeline was canceled before completion',
- skipped: 'Pipeline was skipped',
- manual: 'Pipeline needs to be manually started',
- scheduled: 'Pipeline is scheduled to run'
- }.freeze
+ failed: 4, canceled: 5, skipped: 6, manual: 7,
+ scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
UnknownStatusError = Class.new(StandardError)
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 344f5aa4cd5..611b27c722b 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -20,7 +20,8 @@ module Ci
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true
- delegate :runner_features, to: :metadata, prefix: false, allow_nil: false
+ delegate :set_cancel_gracefully, to: :metadata, prefix: false, allow_nil: false
+ delegate :cancel_gracefully?, to: :metadata, prefix: false, allow_nil: false
before_create :ensure_metadata
end
diff --git a/app/models/concerns/clusters/agents/authorization_config_scopes.rb b/app/models/concerns/clusters/agents/authorization_config_scopes.rb
new file mode 100644
index 00000000000..0a0406c3389
--- /dev/null
+++ b/app/models/concerns/clusters/agents/authorization_config_scopes.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ module AuthorizationConfigScopes
+ extend ActiveSupport::Concern
+
+ included do
+ scope :with_available_ci_access_fields, ->(project) {
+ where("config->'access_as' IS NULL")
+ .or(where("config->'access_as' = '{}'"))
+ .or(where("config->'access_as' ?| array[:fields]", fields: available_ci_access_fields(project)))
+ }
+ end
+
+ class_methods do
+ def available_ci_access_fields(_project)
+ %w(agent)
+ end
+ end
+ end
+ end
+end
+
+Clusters::Agents::AuthorizationConfigScopes.prepend_mod
diff --git a/app/models/concerns/database_reflection.rb b/app/models/concerns/database_reflection.rb
new file mode 100644
index 00000000000..1842f5bf4ec
--- /dev/null
+++ b/app/models/concerns/database_reflection.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# A module that makes it easier/less verbose to reflect upon a database
+# connection.
+#
+# Using this module you can write this:
+#
+# User.database.database_name
+#
+# Instead of this:
+#
+# Gitlab::Database::Reflection.new(User).database_name
+module DatabaseReflection
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def database
+ @database_reflection ||= ::Gitlab::Database::Reflection.new(self)
+ end
+ end
+end
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
index 749d1ad65cd..4b325de61bc 100644
--- a/app/models/concerns/enums/vulnerability.rb
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -37,6 +37,15 @@ module Enums
security_audit: 4
}.with_indifferent_access.freeze
+ # keep the order of the values in the state enum, it is used in state_order method to properly order vulnerabilities based on state
+ # remember to recreate index_vulnerabilities_on_state_case_id index when you update or extend this enum
+ VULNERABILITY_STATES = {
+ detected: 1,
+ confirmed: 4,
+ resolved: 3,
+ dismissed: 2
+ }.with_indifferent_access.freeze
+
def self.confidence_levels
CONFIDENCE_LEVELS
end
@@ -52,6 +61,10 @@ module Enums
def self.detection_methods
DETECTION_METHODS
end
+
+ def self.vulnerability_states
+ VULNERABILITY_STATES
+ end
end
end
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
index 9d4463e5297..bfcf8a1e7b9 100644
--- a/app/models/concerns/file_store_mounter.rb
+++ b/app/models/concerns/file_store_mounter.rb
@@ -7,15 +7,13 @@ module FileStoreMounter
def mount_file_store_uploader(uploader)
mount_uploader(:file, uploader)
+ # This hook is a no-op when the file is uploaded after_commit
after_save :update_file_store, if: :saved_change_to_file?
end
end
- private
-
def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
+ # The file.object_store is set during `uploader.store!` and `uploader.migrate!`
+ update_column(:file_store, file.object_store)
end
end
diff --git a/app/models/concerns/has_integrations.rb b/app/models/concerns/has_integrations.rb
deleted file mode 100644
index 76e03d68600..00000000000
--- a/app/models/concerns/has_integrations.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module HasIntegrations
- extend ActiveSupport::Concern
-
- class_methods do
- def without_integration(integration)
- integrations = Integration
- .select('1')
- .where("#{Integration.table_name}.project_id = projects.id")
- .where(type: integration.type)
-
- Project
- .where('NOT EXISTS (?)', integrations)
- .where(pending_delete: false)
- .where(archived: false)
- end
- end
-end
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 4b4f9c0df84..28ee54afaa9 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -28,6 +28,7 @@ module HasUserType
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
+ scope :human_or_service_user, -> { humans.or(where(user_type: :service_user)) }
enum user_type: USER_TYPES
diff --git a/app/models/concerns/integrations/push_data_validations.rb b/app/models/concerns/integrations/push_data_validations.rb
new file mode 100644
index 00000000000..966fc94e289
--- /dev/null
+++ b/app/models/concerns/integrations/push_data_validations.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# This concern is used by registered integrations such as Integrations::TeamCity and
+# Integrations::DroneCi and adds methods to perform validations on the received
+# data.
+module Integrations
+ module PushDataValidations
+ extend ActiveSupport::Concern
+
+ def merge_request_valid?(data)
+ data.dig(:object_attributes, :state) == 'opened' && merge_request_unchecked?(data)
+ end
+
+ def push_valid?(data)
+ data[:total_commits_count] > 0 &&
+ !branch_removed?(data) &&
+ # prefer merge request trigger over push to avoid double builds
+ !opened_merge_requests?(data)
+ end
+
+ def tag_push_valid?(data)
+ data[:total_commits_count] > 0 && !branch_removed?(data)
+ end
+
+ private
+
+ def branch_removed?(data)
+ Gitlab::Git.blank_ref?(data[:after])
+ end
+
+ def opened_merge_requests?(data)
+ project.merge_requests
+ .opened
+ .from_project(project)
+ .from_source_branches(Gitlab::Git.ref_name(data[:ref]))
+ .exists?
+ end
+
+ def merge_request_unchecked?(data)
+ MergeRequest.state_machines[:merge_status]
+ .check_state?(data.dig(:object_attributes, :merge_status))
+ end
+ end
+end
diff --git a/app/models/concerns/integrations/reactively_cached.rb b/app/models/concerns/integrations/reactively_cached.rb
new file mode 100644
index 00000000000..62eff06c8e2
--- /dev/null
+++ b/app/models/concerns/integrations/reactively_cached.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ReactivelyCached
+ extend ActiveSupport::Concern
+
+ included do
+ include ::ReactiveCaching
+
+ # Default cache key: class name + project_id
+ self.reactive_cache_key = ->(integration) { [integration.class.model_name.singular, integration.project_id] }
+ self.reactive_cache_work_type = :external_dependency
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5c307158a9a..4273eb331a1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -92,7 +92,6 @@ module Issuable
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :opened, -> { with_state(:opened) }
- scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection
diff --git a/app/models/concerns/legacy_bulk_insert.rb b/app/models/concerns/legacy_bulk_insert.rb
new file mode 100644
index 00000000000..1249dfb70cd
--- /dev/null
+++ b/app/models/concerns/legacy_bulk_insert.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module LegacyBulkInsert
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Bulk inserts a number of rows into a table, optionally returning their
+ # IDs.
+ #
+ # This method is deprecated, and you should use the BulkInsertSafe module
+ # instead.
+ #
+ # table - The name of the table to insert the rows into.
+ # rows - An Array of Hash instances, each mapping the columns to their
+ # values.
+ # return_ids - When set to true the return value will be an Array of IDs of
+ # the inserted rows
+ # disable_quote - A key or an Array of keys to exclude from quoting (You
+ # become responsible for protection from SQL injection for
+ # these keys!)
+ # on_conflict - Defines an upsert. Values can be: :disabled (default) or
+ # :do_nothing
+ def legacy_bulk_insert(table, rows, return_ids: false, disable_quote: [], on_conflict: nil)
+ return if rows.empty?
+
+ keys = rows.first.keys
+ columns = keys.map { |key| connection.quote_column_name(key) }
+
+ disable_quote = Array(disable_quote).to_set
+ tuples = rows.map do |row|
+ keys.map do |k|
+ disable_quote.include?(k) ? row[k] : connection.quote(row[k])
+ end
+ end
+
+ sql = <<-EOF
+ INSERT INTO #{table} (#{columns.join(', ')})
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+
+ sql = "#{sql} ON CONFLICT DO NOTHING" if on_conflict == :do_nothing
+
+ sql = "#{sql} RETURNING id" if return_ids
+
+ result = connection.execute(sql)
+
+ if return_ids
+ result.values.map { |tuple| tuple[0].to_i }
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index 848ef63f1c2..98f6ad58434 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -41,9 +41,11 @@ module LoadedInGroupList
namespaces = Namespace.arel_table
children = namespaces.alias('children')
+ # TODO 6473: remove the filtering of the Namespaces::ProjectNamespace see https://gitlab.com/groups/gitlab-org/-/epics/6473
namespaces.project(Arel.star.count.as('preloaded_subgroup_count'))
.from(children)
.where(children[:parent_id].eq(namespaces[:id]))
+ .where(children[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name))
end
def member_count_sql
diff --git a/app/models/concerns/loose_foreign_key.rb b/app/models/concerns/loose_foreign_key.rb
index 4e822a04869..102292672b3 100644
--- a/app/models/concerns/loose_foreign_key.rb
+++ b/app/models/concerns/loose_foreign_key.rb
@@ -7,20 +7,18 @@ module LooseForeignKey
# Loose foreign keys allow delayed processing of associated database records
# with similar guarantees than a database foreign key.
#
- # TODO: finalize this later once the async job is in place
- #
# Prerequisites:
#
# To start using the concern, you'll need to install a database trigger to the parent
# table in a standard DB migration (not post-migration).
#
- # > add_loose_foreign_key_support(:projects, :gitlab_main)
+ # > track_record_deletions(:projects)
#
# Usage:
#
# > class Ci::Build < ApplicationRecord
# >
- # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
+ # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete
# >
# > # associations can be still defined, the dependent options is no longer necessary:
# > has_many :security_scans, class_name: 'Security::Scan'
@@ -32,14 +30,6 @@ module LooseForeignKey
# - :async_delete - deletes the children rows via an asynchronous process.
# - :async_nullify - sets the foreign key column to null via an asynchronous process.
#
- # Options for gitlab_schema:
- #
- # - :gitlab_ci
- # - :gitlab_main
- #
- # The value can be determined by calling `Model.gitlab_schema` where the Model represents
- # the model for the child table.
- #
# How it works:
#
# When adding loose foreign key support to the table, a DELETE trigger is installed
@@ -69,23 +59,17 @@ module LooseForeignKey
end
on_delete_options = %i[async_delete async_nullify]
- gitlab_schema_options = [ApplicationRecord.gitlab_schema, Ci::ApplicationRecord.gitlab_schema]
unless on_delete_options.include?(symbolized_options[:on_delete]&.to_sym)
raise "Invalid on_delete option given: #{symbolized_options[:on_delete]}. Valid options: #{on_delete_options.join(', ')}"
end
- unless gitlab_schema_options.include?(symbolized_options[:gitlab_schema]&.to_sym)
- raise "Invalid gitlab_schema option given: #{symbolized_options[:gitlab_schema]}. Valid options: #{gitlab_schema_options.join(', ')}"
- end
-
definition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
table_name.to_s,
to_table.to_s,
{
column: column.to_s,
- on_delete: symbolized_options[:on_delete].to_sym,
- gitlab_schema: symbolized_options[:gitlab_schema].to_sym
+ on_delete: symbolized_options[:on_delete].to_sym
}
)
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
new file mode 100644
index 00000000000..216a3a0bd64
--- /dev/null
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module MergeRequestReviewerState
+ extend ActiveSupport::Concern
+
+ included do
+ enum state: {
+ unreviewed: 0,
+ reviewed: 1,
+ attention_requested: 2
+ }
+
+ validates :state,
+ presence: true,
+ inclusion: { in: self.states.keys }
+
+ after_initialize :set_state, unless: :persisted?
+
+ def set_state
+ if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
+ self.state = :attention_requested
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index c4f810ab9b1..12041b103f6 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -14,13 +14,12 @@ module Milestoneable
validate :milestone_is_valid
- scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
- scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not( milestones: { releases: { tag: tag, project_id: project_id } } ) }
+ scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index f6d4e5bd27b..ea4fe5b27dc 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -98,6 +98,27 @@ module Noteable
.order('MIN(created_at), MIN(id)')
end
+ # This does not consider OutOfContextDiscussions in MRs
+ # where notes from commits are overriden so that they have
+ # the same discussion_id
+ def discussion_root_note_ids(notes_filter:)
+ relations = []
+
+ relations << discussion_notes.select(
+ "'notes' AS table_name",
+ 'discussion_id',
+ 'MIN(id) AS id',
+ 'MIN(created_at) AS created_at'
+ ).with_notes_filter(notes_filter)
+ .group(:discussion_id)
+
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ relations += synthetic_note_ids_relations
+ end
+
+ Note.from_union(relations, remove_duplicates: false).fresh
+ end
+
def capped_notes_count(max)
notes.limit(max).count
end
@@ -179,6 +200,18 @@ module Noteable
project_email.sub('@', "-#{iid}@")
end
+
+ private
+
+ # Synthetic system notes don't have discussion IDs because these are generated dynamically
+ # in Ruby. These are always root notes anyway so we don't need to group by discussion ID.
+ def synthetic_note_ids_relations
+ [
+ resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at),
+ resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at),
+ resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
+ ]
+ end
end
Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
deleted file mode 100644
index c444f238944..00000000000
--- a/app/models/concerns/reactive_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module ReactiveService
- extend ActiveSupport::Concern
-
- included do
- include ReactiveCaching
-
- # Default cache key: class name + project_id
- self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
- self.reactive_cache_work_type = :external_dependency
- end
-end
diff --git a/app/models/concerns/security/latest_pipeline_information.rb b/app/models/concerns/security/latest_pipeline_information.rb
new file mode 100644
index 00000000000..87eae3cac68
--- /dev/null
+++ b/app/models/concerns/security/latest_pipeline_information.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Security
+ module LatestPipelineInformation
+ private
+
+ def scanner_enabled?(scan_type)
+ latest_builds_reports.include?(scan_type)
+ end
+
+ def latest_builds_reports(only_successful_builds: false)
+ strong_memoize("latest_builds_reports_#{only_successful_builds}") do
+ builds = latest_security_builds
+ builds = builds.select { |build| build.status == 'success' } if only_successful_builds
+ builds.flat_map do |build|
+ build.options[:artifacts][:reports].keys
+ end
+ end
+ end
+
+ def latest_security_builds
+ return [] unless latest_default_branch_pipeline
+
+ ::Security::SecurityJobsFinder.new(pipeline: latest_default_branch_pipeline).execute +
+ ::Security::LicenseComplianceJobsFinder.new(pipeline: latest_default_branch_pipeline).execute
+ end
+
+ def latest_default_branch_pipeline
+ strong_memoize(:pipeline) { latest_pipeline }
+ end
+
+ def auto_devops_source?
+ latest_default_branch_pipeline&.auto_devops_source?
+ end
+ end
+end
diff --git a/app/models/concerns/service_push_data_validations.rb b/app/models/concerns/service_push_data_validations.rb
deleted file mode 100644
index 451804a2c56..00000000000
--- a/app/models/concerns/service_push_data_validations.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-# This concern is used by registered integrations such as Integrations::TeamCity and
-# Integrations::DroneCi and adds methods to perform validations on the received
-# data.
-
-module ServicePushDataValidations
- extend ActiveSupport::Concern
-
- def merge_request_valid?(data)
- data.dig(:object_attributes, :state) == 'opened' && merge_request_unchecked?(data)
- end
-
- def push_valid?(data)
- data[:total_commits_count] > 0 &&
- !branch_removed?(data) &&
- # prefer merge request trigger over push to avoid double builds
- !opened_merge_requests?(data)
- end
-
- def tag_push_valid?(data)
- data[:total_commits_count] > 0 && !branch_removed?(data)
- end
-
- private
-
- def branch_removed?(data)
- Gitlab::Git.blank_ref?(data[:after])
- end
-
- def opened_merge_requests?(data)
- project.merge_requests
- .opened
- .from_project(project)
- .from_source_branches(Gitlab::Git.ref_name(data[:ref]))
- .exists?
- end
-
- def merge_request_unchecked?(data)
- MergeRequest.state_machines[:merge_status]
- .check_state?(data.dig(:object_attributes, :merge_status))
- end
-end
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
index 17fda6c806c..3c906642b1a 100644
--- a/app/models/concerns/sha256_attribute.rb
+++ b/app/models/concerns/sha256_attribute.rb
@@ -39,7 +39,7 @@ module Sha256Attribute
end
def database_exists?
- Gitlab::Database.main.exists?
+ database.exists?
end
end
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index 27277bc5296..ba7c6c0cd8b 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -32,7 +32,7 @@ module ShaAttribute
end
def database_exists?
- Gitlab::Database.main.exists?
+ database.exists?
end
end
end
diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb
index 1c433a3275e..817a4465f91 100644
--- a/app/models/concerns/strip_attribute.rb
+++ b/app/models/concerns/strip_attribute.rb
@@ -2,7 +2,8 @@
# == Strip Attribute module
#
-# Contains functionality to clean attributes before validation
+# Contains functionality to remove leading and trailing
+# whitespace from the attribute before validation
#
# Usage:
#
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 79cbe225e5a..3fe9d7f4d71 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -11,9 +11,7 @@ module Timebox
include StripAttribute
include FromUnion
- TimeboxStruct = Struct.new(:title, :name, :id) do
- include GlobalID::Identification
-
+ TimeboxStruct = Struct.new(:title, :name, :id, :class_name) do
# Ensure these models match the interface required for exporting
def serializable_hash(_opts = {})
{ title: title, name: name, id: id }
@@ -22,6 +20,10 @@ module Timebox
def self.declarative_policy_class
"TimeboxPolicy"
end
+
+ def to_global_id
+ ::Gitlab::GlobalId.build(self, model_name: class_name, id: id)
+ end
end
# Represents a "No Timebox" state used for filtering Issues and Merge
@@ -33,10 +35,10 @@ module Timebox
included do
# Defines the same constants above, but inside the including class.
- const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0)
- const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
- const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
- const_set :Started, TimeboxStruct.new('Started', '#started', -3)
+ const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0, self.name)
+ const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1, self.name)
+ const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2, self.name)
+ const_set :Started, TimeboxStruct.new('Started', '#started', -3, self.name)
alias_method :timebox_id, :id
diff --git a/app/models/concerns/transactions.rb b/app/models/concerns/transactions.rb
new file mode 100644
index 00000000000..a186ebc8475
--- /dev/null
+++ b/app/models/concerns/transactions.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Transactions
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # inside_transaction? will return true if the caller is running within a
+ # transaction. Handles special cases when running inside a test environment,
+ # where tests may be wrapped in transactions
+ def inside_transaction?
+ base = Rails.env.test? ? @open_transactions_baseline.to_i : 0
+
+ connection.open_transactions > base
+ end
+
+ # These methods that access @open_transactions_baseline are not thread-safe.
+ # These are fine though because we only call these in RSpec's main thread.
+ # If we decide to run specs multi-threaded, we would need to use something
+ # like ThreadGroup to keep track of this value
+ def set_open_transactions_baseline
+ @open_transactions_baseline = connection.open_transactions
+ end
+
+ def reset_open_transactions_baseline
+ @open_transactions_baseline = 0
+ end
+ end
+end
diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb
index 00abe0a06e6..6d89521255c 100644
--- a/app/models/concerns/ttl_expirable.rb
+++ b/app/models/concerns/ttl_expirable.rb
@@ -5,10 +5,11 @@ module TtlExpirable
included do
validates :status, presence: true
+ default_value_for :read_at, Time.zone.now
enum status: { default: 0, expired: 1, processing: 2, error: 3 }
- scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) }
+ scope :read_before, ->(number_of_days) { where("read_at <= ?", Time.zone.now - number_of_days.days) }
scope :active, -> { where(status: :default) }
scope :lock_next_by, ->(sort) do
@@ -17,4 +18,8 @@ module TtlExpirable
.lock('FOR UPDATE SKIP LOCKED')
end
end
+
+ def read!
+ self.update(read_at: Time.zone.now)
+ end
end
diff --git a/app/models/concerns/update_highest_role.rb b/app/models/concerns/update_highest_role.rb
index 6432cc794a5..2b0ec5c7e21 100644
--- a/app/models/concerns/update_highest_role.rb
+++ b/app/models/concerns/update_highest_role.rb
@@ -15,7 +15,7 @@ module UpdateHighestRole
# Schedule a Sidekiq job to update the highest role for a User
#
# The job will be called outside of a transaction in order to ensure the changes
- # to be commited before attempting to update the highest role.
+ # to be committed before attempting to update the highest role.
# The exlusive lease will not be released after completion to prevent multiple jobs
# being executed during the defined timeout.
def update_highest_role
diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb
index dfb1e151b41..e51ed95bf70 100644
--- a/app/models/concerns/x509_serial_number_attribute.rb
+++ b/app/models/concerns/x509_serial_number_attribute.rb
@@ -39,7 +39,7 @@ module X509SerialNumberAttribute
end
def database_exists?
- Gitlab::Database.main.exists?
+ database.exists?
end
end
end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index ecdac64b31b..173b38b2c63 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -31,7 +31,7 @@ class CustomEmoji < ApplicationRecord
private
def valid_emoji_name
- if Gitlab::Emoji.emoji_exists?(name)
+ if TanukiEmoji.find_by_alpha_code(name)
errors.add(:name, _('%{name} is already being used for another emoji') % { name: self.name })
end
end
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index c632f8e2efa..5898bc3412f 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -7,7 +7,8 @@ class CustomerRelations::Contact < ApplicationRecord
belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'group_id'
belongs_to :organization, optional: true
- has_and_belongs_to_many :issues, join_table: :issue_customer_relations_contacts # rubocop: disable Rails/HasAndBelongsToMany
+ has_many :issue_contacts, inverse_of: :contact
+ has_many :issues, through: :issue_contacts, inverse_of: :customer_relations_contacts
strip_attributes! :phone, :first_name, :last_name
diff --git a/app/models/customer_relations/issue_contact.rb b/app/models/customer_relations/issue_contact.rb
new file mode 100644
index 00000000000..98faf8d6644
--- /dev/null
+++ b/app/models/customer_relations/issue_contact.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CustomerRelations::IssueContact < ApplicationRecord
+ self.table_name = "issue_customer_relations_contacts"
+
+ belongs_to :issue, optional: false, inverse_of: :customer_relations_contacts
+ belongs_to :contact, optional: false, inverse_of: :issue_contacts
+
+ validate :contact_belongs_to_issue_group
+
+ private
+
+ def contact_belongs_to_issue_group
+ return unless contact&.group_id
+ return unless issue&.project&.namespace_id
+ return if contact.group_id == issue.project.namespace_id
+
+ errors.add(:base, _('The contact does not belong to the same group as the issue'))
+ end
+end
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
index adad8e3013e..e99364b2709 100644
--- a/app/models/data_list.rb
+++ b/app/models/data_list.rb
@@ -1,22 +1,26 @@
# frozen_string_literal: true
class DataList
- def initialize(batch, data_fields_hash, klass)
+ def initialize(batch, data_fields_hash, data_fields_klass)
@batch = batch
@data_fields_hash = data_fields_hash
- @klass = klass
+ @data_fields_klass = data_fields_klass
end
def to_array
- [klass, columns, values]
+ [data_fields_klass, columns, values]
end
private
- attr_reader :batch, :data_fields_hash, :klass
+ attr_reader :batch, :data_fields_hash, :data_fields_klass
def columns
- data_fields_hash.keys << 'service_id'
+ data_fields_hash.keys << data_fields_foreign_key
+ end
+
+ def data_fields_foreign_key
+ data_fields_klass.reflections['integration'].foreign_key
end
def values
diff --git a/app/models/dependency_proxy/blob.rb b/app/models/dependency_proxy/blob.rb
index 7ca15652586..bd5c022e692 100644
--- a/app/models/dependency_proxy/blob.rb
+++ b/app/models/dependency_proxy/blob.rb
@@ -7,6 +7,8 @@ class DependencyProxy::Blob < ApplicationRecord
belongs_to :group
+ MAX_FILE_SIZE = 5.gigabytes.freeze
+
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb
index b83047efe54..64f484942ef 100644
--- a/app/models/dependency_proxy/manifest.rb
+++ b/app/models/dependency_proxy/manifest.rb
@@ -7,17 +7,19 @@ class DependencyProxy::Manifest < ApplicationRecord
belongs_to :group
+ MAX_FILE_SIZE = 10.megabytes.freeze
+ DIGEST_HEADER = 'Docker-Content-Digest'
+
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
validates :digest, presence: true
- mount_file_store_uploader DependencyProxy::FileUploader
+ scope :order_id_desc, -> { reorder(id: :desc) }
- def self.find_or_initialize_by_file_name_or_digest(file_name:, digest:)
- result = find_by(file_name: file_name) || find_by(digest: digest)
- return result if result
+ mount_file_store_uploader DependencyProxy::FileUploader
- new(file_name: file_name, digest: digest)
+ def self.find_by_file_name_or_digest(file_name:, digest:)
+ find_by(file_name: file_name) || find_by(digest: digest)
end
end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 8f5a713af3f..4ed38f578ee 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -7,12 +7,16 @@ class DeployKey < Key
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :deploy_keys_projects
+
+ has_many :deploy_keys_projects_with_write_access, -> { with_write_access }, class_name: "DeployKeysProject"
+ has_many :projects_with_write_access, -> { includes(:route) }, class_name: 'Project', through: :deploy_keys_projects_with_write_access, source: :project
has_many :protected_branch_push_access_levels, class_name: '::ProtectedBranch::PushAccessLevel'
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where(deploy_keys_projects: { project_id: projects }) }
scope :with_write_access, -> { joins(:deploy_keys_projects).merge(DeployKeysProject.with_write_access) }
scope :are_public, -> { where(public: true) }
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) }
+ scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) }
accepts_nested_attributes_for :deploy_keys_projects
@@ -52,10 +56,6 @@ class DeployKey < Key
end
end
- def projects_with_write_access
- Project.with_route.where(id: deploy_keys_projects.with_write_access.select(:project_id))
- end
-
def self.with_write_access_for_project(project, deploy_key: nil)
query = in_projects(project).with_write_access
query = query.where(id: deploy_key) if deploy_key
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index f91700f764b..ade19ce02a8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -8,11 +8,12 @@ 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
+
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
@@ -54,6 +55,8 @@ class Deployment < ApplicationRecord
scope :finished_after, ->(date) { where('finished_at >= ?', date) }
scope :finished_before, ->(date) { where('finished_at < ?', date) }
+ scope :ordered, -> { order(finished_at: :desc) }
+
FINISHED_STATUSES = %i[success failed canceled].freeze
state_machine :status, initial: :created do
@@ -99,6 +102,10 @@ class Deployment < ApplicationRecord
deployment.run_after_commit do
Deployments::UpdateEnvironmentWorker.perform_async(id)
Deployments::LinkMergeRequestWorker.perform_async(id)
+
+ if ::Feature.enabled?(:deployments_archive, deployment.project, default_enabled: :yaml)
+ Deployments::ArchiveInProjectWorker.perform_async(deployment.project_id)
+ end
end
end
@@ -132,6 +139,14 @@ class Deployment < ApplicationRecord
skipped: 5
}
+ def self.archivables_in(project, limit:)
+ start_iid = project.deployments.order(iid: :desc).limit(1)
+ .select("(iid - #{ARCHIVABLE_OFFSET}) AS start_iid")
+
+ project.deployments.preload(:environment).where('iid <= (?)', start_iid)
+ .where(archived: false).limit(limit)
+ end
+
def self.last_for_environment(environment)
ids = self
.for_environment(environment)
@@ -299,7 +314,7 @@ class Deployment < ApplicationRecord
"#{id} as deployment_id",
"#{environment_id} as environment_id").to_sql
- # We don't use `Gitlab::Database.main.bulk_insert` here so that we don't need to
+ # We don't use `ApplicationRecord.legacy_bulk_insert` here so that we don't need to
# first pluck lots of IDs into memory.
#
# We also ignore any duplicates so this method can be called multiple times
@@ -325,6 +340,7 @@ class Deployment < ApplicationRecord
def sync_status_with(build)
return false unless ::Deployment.statuses.include?(build.status)
+ return false if build.created? || build.status == self.status
update_status!(build.status)
rescue StandardError => e
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 6cda03557d1..5819404efb9 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -88,7 +88,7 @@ module DesignManagement
rows = design_actions.map { |action| action.row_attrs(version) }
- Gitlab::Database.main.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
diff --git a/app/models/email.rb b/app/models/email.rb
index 0140f784842..676e79406e9 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -29,7 +29,7 @@ class Email < ApplicationRecord
end
def unique_email
- self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
+ self.errors.add(:email, 'has already been taken') if primary_email_of_another_user?
end
def validate_email_format
@@ -40,4 +40,14 @@ class Email < ApplicationRecord
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
end
+
+ def user_primary_email?
+ email.casecmp?(user.email)
+ end
+
+ private
+
+ def primary_email_of_another_user?
+ User.where(email: email).where.not(id: user_id).exists?
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 31ab426728b..2618991c9e5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -425,6 +425,14 @@ class Environment < ApplicationRecord
clear_reactive_cache!
end
+ def should_link_to_merge_requests?
+ unfoldered? || production? || staging?
+ end
+
+ def unfoldered?
+ environment_type.nil?
+ end
+
private
def rollout_status_available?
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 2d6a4694def..efbb6adff70 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -18,9 +18,10 @@ class ErrorTracking::Error < ApplicationRecord
scope :for_status, -> (status) { where(status: status) }
validates :project, presence: true
- validates :name, presence: true
- validates :description, presence: true
- validates :actor, presence: true
+ validates :name, presence: true, length: { maximum: 255 }
+ validates :description, presence: true, length: { maximum: 1024 }
+ validates :actor, presence: true, length: { maximum: 255 }
+ validates :platform, length: { maximum: 255 }
validates :status, presence: true
enum status: {
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 686518a39fb..0b638f65768 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -6,7 +6,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
validates :payload, json_schema: { filename: 'error_tracking_event_payload' }
validates :error, presence: true
- validates :description, presence: true
+ validates :description, presence: true, length: { maximum: 1024 }
+ validates :level, length: { maximum: 255 }
+ validates :environment, length: { maximum: 255 }
validates :occurred_at, presence: true
def stacktrace
@@ -61,9 +63,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
pre_context = entry['pre_context']
post_context = entry['post_context']
- context += lines_with_position(pre_context, error_line_no - pre_context.size)
+ context += lines_with_position(pre_context, error_line_no - pre_context.size) if pre_context
context += lines_with_position([error_line], error_line_no)
- context += lines_with_position(post_context, error_line_no + 1)
+ context += lines_with_position(post_context, error_line_no + 1) if post_context
context.reject(&:blank?)
end
diff --git a/app/models/event.rb b/app/models/event.rb
index d6588699d27..f6174589a84 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -9,9 +9,6 @@ class Event < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include UsageStatistics
include ShaAttribute
- include IgnorableColumns
-
- ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 0c36e51120f..2775b520b2f 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -12,7 +12,8 @@ class GpgSignature < ApplicationRecord
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
- unknown_key: 5
+ unknown_key: 5,
+ multiple_signatures: 6
}
belongs_to :project
diff --git a/app/models/group.rb b/app/models/group.rb
index c5e119451e3..2dd20300ad2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -56,6 +56,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
+ has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
+ has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
+
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
@@ -194,13 +197,8 @@ class Group < Namespace
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
- inner_ancestors = if Feature.enabled?(:linear_group_ancestor_scopes, default_enabled: :yaml)
- inner_groups.self_and_ancestors
- else
- Gitlab::ObjectHierarchy.new(inner_groups).base_and_ancestors
- end
-
- inner_query = inner_ancestors
+ inner_query = inner_groups
+ .self_and_ancestors
.where(emails_disabled: true)
.select('1')
.limit(1)
@@ -317,13 +315,15 @@ class Group < Namespace
owners.include?(user)
end
- def add_users(users, access_level, current_user: nil, expires_at: nil)
+ def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
Members::Groups::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id
)
end
@@ -760,18 +760,6 @@ class Group < Namespace
Timelog.in_group(self)
end
- def cached_issues_state_count_enabled?
- Feature.enabled?(:cached_issues_state_count, self, default_enabled: :yaml)
- end
-
- def organizations
- ::CustomerRelations::Organization.where(group_id: self.id)
- end
-
- def contacts
- ::CustomerRelations::Contact.where(group_id: self.id)
- end
-
def dependency_proxy_image_ttl_policy
super || build_dependency_proxy_image_ttl_policy
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 158764bb783..d3059fa6d4a 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -14,7 +14,7 @@ class Integration < ApplicationRecord
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
- pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
+ pivotaltracker prometheus pushover redmine shimo slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
@@ -373,7 +373,7 @@ class Integration < ApplicationRecord
end
def to_data_fields_hash
- data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id')
+ data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id', 'integration_id')
end
def event_channel_names
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 1a7cbaa34c7..0774b84b69f 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -3,7 +3,7 @@
module Integrations
class Bamboo < BaseCi
include ActionView::Helpers::UrlHelper
- include ReactiveService
+ include ReactivelyCached
prop_accessor :bamboo_url, :build_key, :username, :password
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index c6335782b5e..ca72de47d30 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -73,7 +73,12 @@ module Integrations
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }.freeze,
{
type: 'text',
name: 'labels_to_be_notified',
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 94a37f0c4f2..9fad3a42647 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -5,7 +5,7 @@ require "addressable/uri"
module Integrations
class Buildkite < BaseCi
include HasWebHook
- include ReactiveService
+ include ReactivelyCached
extend Gitlab::Utils::Override
ENDPOINT = "https://buildkite.com"
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 76160a61bc3..21993dd3c43 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -41,7 +41,12 @@ module Integrations
[
{ type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
{ type: "checkbox", name: "notify_only_broken_pipelines" },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index c93ae432fe9..856d14c022d 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -3,8 +3,8 @@
module Integrations
class DroneCi < BaseCi
include HasWebHook
- include ReactiveService
- include ServicePushDataValidations
+ include PushDataValidations
+ include ReactivelyCached
extend Gitlab::Utils::Override
prop_accessor :drone_url, :token
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index e277633664f..a9cd67550dc 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -76,7 +76,12 @@ module Integrations
help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
{ type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ },
{
type: 'textarea',
name: 'recipients',
@@ -92,7 +97,7 @@ module Integrations
return if recipients.blank?
if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
- errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ errors.add(:recipients, s_("Integrations|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
end
end
end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index d02cfe4ec56..0d6b9fb1019 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -40,7 +40,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index ec6adc87bf4..42c291abf55 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -89,7 +89,6 @@ module Integrations
site: URI.join(url, '/').to_s.delete_suffix('/'), # Intended to find the root
context_path: (url.path.presence || '/').delete_suffix('/'),
auth_type: :basic,
- read_timeout: 120,
use_cookies: true,
additional_cookies: ['OBBasicAuth=fromDialog'],
use_ssl: url.scheme == 'https'
@@ -303,6 +302,14 @@ module Integrations
private
+ def branch_name(noteable)
+ if Feature.enabled?(:jira_use_first_ref_by_oid, project, default_enabled: :yaml)
+ noteable.first_ref_by_oid(project.repository)
+ else
+ noteable.ref_names(project.repository).first
+ end
+ end
+
def server_info
strong_memoize(:server_info) do
client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
@@ -496,7 +503,7 @@ module Integrations
{
id: noteable.short_id,
description: noteable.safe_message,
- branch: noteable.ref_names(project.repository).first
+ branch: branch_name(noteable)
}
elsif noteable.is_a?(MergeRequest)
{
@@ -521,7 +528,9 @@ module Integrations
yield
rescue StandardError => error
@error = error
- log_error("Error sending message", client_url: client_url, error: @error.message)
+ payload = { client_url: client_url }
+ Gitlab::ExceptionLogFormatter.format!(error, payload)
+ log_error("Error sending message", payload)
nil
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 5aad25e8ddc..71cd4ddaf82 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -37,7 +37,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index efba35cc2a8..6dc41958daa 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -4,9 +4,12 @@ module Integrations
class PipelinesEmail < Integration
include NotificationBranchSelection
+ RECIPIENTS_LIMIT = 30
+
prop_accessor :recipients, :branches_to_be_notified
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :recipients, presence: true, if: :validate_recipients?
+ validate :number_of_recipients_within_limit, if: :validate_recipients?
def initialize_properties
if properties.nil?
@@ -49,7 +52,7 @@ module Integrations
return unless supported_events.include?(data[:object_kind])
return unless force || should_pipeline_be_notified?(data)
- all_recipients = retrieve_recipients(data)
+ all_recipients = retrieve_recipients
return unless all_recipients.any?
@@ -71,6 +74,7 @@ module Integrations
name: 'notify_only_broken_pipelines' },
{ type: 'select',
name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
choices: branch_choices }
]
end
@@ -98,8 +102,18 @@ module Integrations
end
end
- def retrieve_recipients(data)
+ def retrieve_recipients
recipients.to_s.split(/[,\r\n ]+/).reject(&:empty?)
end
+
+ private
+
+ def number_of_recipients_within_limit
+ return if recipients.blank?
+
+ if retrieve_recipients.size > RECIPIENTS_LIMIT
+ errors.add(:recipients, s_("Integrations|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ end
+ end
end
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
new file mode 100644
index 00000000000..4f42fda2577
--- /dev/null
+++ b/app/models/integrations/shimo.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Shimo < Integration
+ prop_accessor :external_wiki_url
+ validates :external_wiki_url, presence: true, public_url: true, if: :activated?
+
+ def render?
+ valid? && activated?
+ end
+
+ def title
+ s_('Shimo|Shimo')
+ end
+
+ def description
+ s_('Shimo|Link to a Shimo Workspace from the sidebar.')
+ end
+
+ def self.to_param
+ 'shimo'
+ end
+
+ # support for `test` method
+ def execute(_data)
+ response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
+ response.body if response.code == 200
+ rescue StandardError
+ nil
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'external_wiki_url',
+ title: s_('Shimo|Shimo Workspace URL'),
+ required: true
+ }
+ ]
+ end
+ end
+end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 3f868b57597..008b591c304 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -2,8 +2,8 @@
module Integrations
class Teamcity < BaseCi
- include ReactiveService
- include ServicePushDataValidations
+ include PushDataValidations
+ include ReactivelyCached
prop_accessor :teamcity_url, :build_type, :username, :password
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index e3e180ae959..f085423d229 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -34,7 +34,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 6fd82a32035..7660eda6f83 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -36,7 +36,12 @@ module Integrations
[
{ type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ {
+ type: 'select',
+ name: 'branches_to_be_notified',
+ title: s_('Integrations|Branches for which notifications are to be sent'),
+ choices: branch_choices
+ }
]
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index 68c02f54c61..493d42cc40b 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -2,6 +2,8 @@
module Integrations
class Zentao < Integration
+ include Gitlab::Routing
+
data_field :url, :api_url, :api_token, :zentao_product_xid
validates :url, public_url: true, presence: true, if: :activated?
@@ -9,16 +11,29 @@ module Integrations
validates :api_token, presence: true, if: :activated?
validates :zentao_product_xid, presence: true, if: :activated?
+ # License Level: EEP_FEATURES
+ def self.issues_license_available?(project)
+ project&.licensed_feature_available?(:zentao_issues_integration)
+ end
+
def data_fields
zentao_tracker_data || self.build_zentao_tracker_data
end
def title
- self.class.name.demodulize
+ 'ZenTao'
end
def description
- s_("ZentaoIntegration|Use Zentao as this project's issue tracker.")
+ s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
+ end
+
+ def 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') },
+ link_end: '</a>'.html_safe
+ }
end
def self.to_param
@@ -42,28 +57,29 @@ module Integrations
{
type: 'text',
name: 'url',
- title: s_('ZentaoIntegration|Zentao Web URL'),
+ title: s_('ZentaoIntegration|ZenTao Web URL'),
placeholder: 'https://www.zentao.net',
- help: s_('ZentaoIntegration|Base URL of the Zentao instance.'),
+ help: s_('ZentaoIntegration|Base URL of the ZenTao instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
- title: s_('ZentaoIntegration|Zentao API URL (optional)'),
+ title: s_('ZentaoIntegration|ZenTao API URL (optional)'),
help: s_('ZentaoIntegration|If different from Web URL.')
},
{
type: 'password',
name: 'api_token',
- title: s_('ZentaoIntegration|Zentao API token'),
- non_empty_password_title: s_('ZentaoIntegration|Enter API token'),
+ title: s_('ZentaoIntegration|ZenTao API token'),
+ non_empty_password_title: s_('ZentaoIntegration|Enter new ZenTao API token'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
required: true
},
{
type: 'text',
name: 'zentao_product_xid',
- title: s_('ZentaoIntegration|Zentao Product ID'),
+ title: s_('ZentaoIntegration|ZenTao Product ID'),
required: true
}
]
@@ -76,3 +92,5 @@ module Integrations
end
end
end
+
+::Integrations::Zentao.prepend_mod
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 9c568414ec2..47dc084d69c 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -81,7 +81,8 @@ class Issue < ApplicationRecord
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts, through: :prometheus_alert_events
- has_and_belongs_to_many :customer_relations_contacts, join_table: :issue_customer_relations_contacts, class_name: 'CustomerRelations::Contact' # rubocop: disable Rails/HasAndBelongsToMany
+ has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
+ has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
accepts_nested_attributes_for :issuable_severity, update_only: true
accepts_nested_attributes_for :sentry_issue
@@ -203,6 +204,8 @@ class Issue < ApplicationRecord
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
+
+ issue.clear_closure_reason_references
end
end
@@ -378,6 +381,11 @@ class Issue < ApplicationRecord
!duplicated_to_id.nil?
end
+ def clear_closure_reason_references
+ self.moved_to_id = nil
+ self.duplicated_to_id = nil
+ end
+
def can_move?(user, to_project = nil)
if to_project
return false unless user.can?(:admin_issue, to_project)
diff --git a/app/models/key.rb b/app/models/key.rb
index 64385953865..a478434538c 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -46,7 +46,7 @@ class Key < ApplicationRecord
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
# Date is set specifically in this scope to improve query time.
- scope :expired_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') BETWEEN '2000-01-01' AND CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
+ scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
scope :expiring_soon_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') > CURRENT_DATE AND date(expires_at AT TIME ZONE 'UTC') < ? AND before_expiry_notification_delivered_at IS NULL", DAYS_TO_EXPIRE.days.from_now.to_date]) }
def self.regular_keys
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 4fb5fd8c58a..d326b07ad31 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -11,4 +11,16 @@ class LabelLink < ApplicationRecord
validates :label, presence: true, unless: :importing?
scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
+
+ # Example: Issues has at least one label within a project
+ # > Issue.where(project_id: 100) # or any scope on issues
+ # > .where(LabelLink.by_target_for_exists_query('Issue', Issue.arel_table[:id]).arel.exists)
+ scope :by_target_for_exists_query, -> (target_type, arel_join_column, label_ids = nil) do
+ relation = LabelLink
+ .where(target_type: target_type)
+ .where(arel_table['target_id'].eq(arel_join_column))
+
+ relation = relation.where(label_id: label_ids) if label_ids
+ relation
+ end
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index ca5a2800a03..c3b3e76f67b 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -1,5 +1,32 @@
# frozen_string_literal: true
class LooseForeignKeys::DeletedRecord < ApplicationRecord
- extend SuppressCompositePrimaryKeyWarning
+ self.primary_key = :id
+
+ scope :for_table, -> (table) { where(fully_qualified_table_name: table) }
+ scope :consume_order, -> { order(:partition, :consume_after, :id) }
+
+ enum status: { pending: 1, processed: 2 }, _prefix: :status
+
+ def self.load_batch_for_table(table, batch_size)
+ for_table(table)
+ .status_pending
+ .consume_order
+ .limit(batch_size)
+ .to_a
+ end
+
+ def self.mark_records_processed(all_records)
+ # Run a query for each partition to optimize the row lookup by primary key (partition, id)
+ update_count = 0
+
+ all_records.group_by(&:partition).each do |partition, records_within_partition|
+ update_count += status_pending
+ .where(partition: partition)
+ .where(id: records_within_partition.pluck(:id))
+ .update_all(status: :processed)
+ end
+
+ update_count
+ end
end
diff --git a/app/models/loose_foreign_keys/modification_tracker.rb b/app/models/loose_foreign_keys/modification_tracker.rb
new file mode 100644
index 00000000000..6eb04608cd9
--- /dev/null
+++ b/app/models/loose_foreign_keys/modification_tracker.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class ModificationTracker
+ MAX_DELETES = 100_000
+ MAX_UPDATES = 50_000
+ MAX_RUNTIME = 3.minutes
+
+ delegate :monotonic_time, to: :'Gitlab::Metrics::System'
+
+ def initialize
+ @delete_count_by_table = Hash.new { |h, k| h[k] = 0 }
+ @update_count_by_table = Hash.new { |h, k| h[k] = 0 }
+ @start_time = monotonic_time
+ @deletes_counter = Gitlab::Metrics.counter(
+ :loose_foreign_key_deletions,
+ 'The number of loose foreign key deletions'
+ )
+ @updates_counter = Gitlab::Metrics.counter(
+ :loose_foreign_key_updates,
+ 'The number of loose foreign key updates'
+ )
+ end
+
+ def add_deletions(table, count)
+ @delete_count_by_table[table] += count
+ @deletes_counter.increment({ table: table }, count)
+ end
+
+ def add_updates(table, count)
+ @update_count_by_table[table] += count
+ @updates_counter.increment({ table: table }, count)
+ end
+
+ def over_limit?
+ @delete_count_by_table.values.sum >= MAX_DELETES ||
+ @update_count_by_table.values.sum >= MAX_UPDATES ||
+ monotonic_time - @start_time >= MAX_RUNTIME
+ end
+
+ def stats
+ {
+ over_limit: over_limit?,
+ delete_count_by_table: @delete_count_by_table,
+ update_count_by_table: @update_count_by_table,
+ delete_count: @delete_count_by_table.values.sum,
+ update_count: @update_count_by_table.values.sum
+ }
+ end
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 21fd4aebd7b..11f67a77ee2 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -13,6 +13,7 @@ class Member < ApplicationRecord
include FromUnion
include UpdateHighestRole
include RestrictedSignup
+ include Gitlab::Experiment::Dsl
AVATAR_SIZE = 40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@@ -22,8 +23,10 @@ class Member < ApplicationRecord
belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ has_one :member_task
delegate :name, :username, :email, to: :user, prefix: true
+ delegate :tasks_to_be_done, to: :member_task, allow_nil: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite?
@@ -413,6 +416,14 @@ class Member < ApplicationRecord
def after_accept_invite
post_create_hook
+
+ if experiment(:invite_members_for_task).enabled?
+ run_after_commit_or_now do
+ if member_task
+ TasksToBeDone::CreateWorker.perform_async(member_task.id, created_by_id, [user_id.to_i])
+ end
+ end
+ end
end
def after_decline_invite
diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb
new file mode 100644
index 00000000000..f093619ff36
--- /dev/null
+++ b/app/models/members/member_task.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class MemberTask < ApplicationRecord
+ TASKS = {
+ code: 0,
+ ci: 1,
+ issues: 2
+ }.freeze
+
+ belongs_to :member
+ belongs_to :project
+
+ validates :member, :project, presence: true
+ validates :tasks, inclusion: { in: TASKS.values }
+ validate :tasks_uniqueness
+ validate :project_in_member_source
+
+ scope :for_members, -> (members) { joins(:member).where(member: members) }
+
+ def tasks_to_be_done
+ Array(self[:tasks]).map { |task| TASKS.key(task) }
+ end
+
+ def tasks_to_be_done=(tasks)
+ self[:tasks] = Array(tasks).map do |task|
+ TASKS[task.to_sym]
+ end.uniq
+ end
+
+ private
+
+ def tasks_uniqueness
+ errors.add(:tasks, 'are not unique') unless Array(tasks).length == Array(tasks).uniq.length
+ end
+
+ def project_in_member_source
+ if member.is_a?(GroupMember)
+ errors.add(:project, _('is not in the member group')) unless project.namespace == member.source
+ elsif member.is_a?(ProjectMember)
+ errors.add(:project, _('is not the member project')) unless project == member.source
+ end
+ end
+end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index eec46b3493e..89b72508e84 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -94,7 +94,6 @@ class ProjectMember < Member
override :refresh_member_authorized_projects
def refresh_member_authorized_projects(blocking:)
- return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh)
return unless user
# rubocop:disable CodeReuse/ServiceClass
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 15862fb2bfa..0cd8f12088c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -268,7 +268,6 @@ class MergeRequest < ApplicationRecord
from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
end
scope :merged, -> { with_state(:merged) }
- scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :open_and_closed, -> { with_states(:opened, :closed) }
scope :drafts, -> { where(draft: true) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
@@ -663,7 +662,7 @@ class MergeRequest < ApplicationRecord
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
- jid = MergeWorker.perform_async(id, user_id, params.to_h)
+ jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid)
# merge_ongoing? depends on merge_jid
@@ -682,7 +681,7 @@ class MergeRequest < ApplicationRecord
# attribute is set *and* that the sidekiq job is still running. So a JID
# for a completed RebaseWorker is equivalent to a nil JID.
jid = Sidekiq::Worker.skipping_transaction_check do
- RebaseWorker.perform_async(id, user_id, skip_ci)
+ RebaseWorker.with_status.perform_async(id, user_id, skip_ci)
end
update_column(:rebase_jid, jid)
@@ -1317,6 +1316,10 @@ class MergeRequest < ApplicationRecord
end
def default_merge_commit_message(include_description: false)
+ if self.target_project.merge_commit_template.present? && !include_description
+ return ::Gitlab::MergeRequests::MergeCommitMessage.new(merge_request: self).message
+ end
+
closes_issues_references = visible_closing_issues_for.map do |issue|
issue.to_reference(target_project)
end
@@ -1409,7 +1412,15 @@ class MergeRequest < ApplicationRecord
def environments
return Environment.none unless actual_head_pipeline&.merge_request?
- actual_head_pipeline.environments
+ build_for_actual_head_pipeline = Ci::Build.latest.where(pipeline: actual_head_pipeline)
+
+ environments = build_for_actual_head_pipeline.joins(:metadata)
+ .where.not('ci_builds_metadata.expanded_environment_name' => nil)
+ .distinct('ci_builds_metadata.expanded_environment_name')
+ .limit(100)
+ .pluck(:expanded_environment_name)
+
+ Environment.where(project: project, name: environments)
end
def fetch_ref!
@@ -1907,6 +1918,10 @@ class MergeRequest < ApplicationRecord
true
end
+ def find_assignee(user)
+ merge_request_assignees.find_by(user_id: user.id)
+ end
+
def find_reviewer(user)
merge_request_reviewers.find_by(user_id: user.id)
end
@@ -1930,6 +1945,10 @@ class MergeRequest < ApplicationRecord
end
end
+ def attention_requested_enabled?
+ Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml)
+ end
+
private
def set_draft_status
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index 86bf950ae19..fd8e5860040 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -1,10 +1,16 @@
# frozen_string_literal: true
class MergeRequestAssignee < ApplicationRecord
+ include MergeRequestReviewerState
+
belongs_to :merge_request, touch: true
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
validates :assignee, uniqueness: { scope: :merge_request_id }
scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
+
+ def cache_key
+ [model_name.cache_key, id, state, assignee.cache_key]
+ end
end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index 09824ed4468..ebbdecf8aa7 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -26,7 +26,7 @@ class MergeRequestContextCommit < ApplicationRecord
# create MergeRequestContextCommit by given commit sha and it's diff file record
def self.bulk_insert(rows, **args)
- Gitlab::Database.main.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index b9efebe3af2..fdf57068928 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -14,7 +14,7 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
- Gitlab::Database.main.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
def path
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bd94c0ad30e..2516ff05bda 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -515,7 +515,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
save!
end
@@ -535,7 +535,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
update!(stored_externally: false)
end
@@ -595,7 +595,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
# Faster inserts
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
end
def build_external_diff_tempfile(rows)
@@ -710,7 +710,7 @@ class MergeRequestDiff < ApplicationRecord
end
CommitCollection
- .new(merge_request.source_project, commits, merge_request.source_branch)
+ .new(merge_request.target_project, commits, merge_request.target_branch)
end
def save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index d9a1784cdda..66f1e45fd49 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -6,6 +6,12 @@ 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
@@ -51,9 +57,14 @@ class MergeRequestDiffCommit < ApplicationRecord
committer =
users[[commit_hash[:committer_name], commit_hash[:committer_email]]]
+ # These fields are only used to determine the author/committer IDs, we
+ # don't store them in the DB.
+ commit_hash = commit_hash
+ .except(:author_name, :author_email, :committer_name, :committer_email)
+
commit_hash.merge(
- commit_author_id: author&.id,
- committer_id: committer&.id,
+ commit_author_id: author.id,
+ committer_id: committer.id,
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
@@ -63,7 +74,7 @@ class MergeRequestDiffCommit < ApplicationRecord
)
end
- Gitlab::Database.main.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
def self.prepare_commits_for_bulk_insert(commits)
@@ -104,18 +115,18 @@ class MergeRequestDiffCommit < ApplicationRecord
end
def author_name
- commit_author_id ? commit_author.name : super
+ commit_author&.name
end
def author_email
- commit_author_id ? commit_author.email : super
+ commit_author&.email
end
def committer_name
- committer_id ? committer.name : super
+ committer&.name
end
def committer_email
- committer_id ? committer.email : super
+ committer&.email
end
end
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index 4a1f31a7f39..4abf0fa09f0 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -1,15 +1,12 @@
# frozen_string_literal: true
class MergeRequestReviewer < ApplicationRecord
- enum state: {
- unreviewed: 0,
- reviewed: 1
- }
-
- validates :state,
- presence: true,
- inclusion: { in: MergeRequestReviewer.states.keys }
+ include MergeRequestReviewerState
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
+
+ def cache_key
+ [model_name.cache_key, id, state, reviewer.cache_key]
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 07f9bb99952..353a896b3fe 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -16,9 +16,11 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Linear
include EachBatch
- ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
+ # Temporary column used for back-filling project namespaces.
+ # Remove it once the back-filling of all project namespaces is done.
+ ignore_column :tmp_project_id, remove_with: '14.7', remove_after: '2022-01-22'
- # Tells ActiveRecord not to store the full class name, in order to space some space
+ # Tells ActiveRecord not to store the full class name, in order to save some space
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
self.store_full_sti_class = false
self.store_full_class_name = false
@@ -54,7 +56,7 @@ class Namespace < ApplicationRecord
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
- has_many :children, class_name: "Namespace", foreign_key: :parent_id
+ has_many :children, -> { where(type: Group.sti_name) }, class_name: "Namespace", foreign_key: :parent_id
has_many :custom_emoji, inverse_of: :namespace
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
@@ -95,9 +97,11 @@ class Namespace < ApplicationRecord
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type, default_enabled: :yaml) }
- validate :nesting_level_allowed
- validate :changing_shared_runners_enabled_is_allowed
- validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed
+
+ # ProjectNamespaces excluded as they are not meant to appear in the group hierarchy at the moment.
+ validate :nesting_level_allowed, unless: -> { project_namespace? }
+ validate :changing_shared_runners_enabled_is_allowed, unless: -> { project_namespace? }
+ validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed, unless: -> { project_namespace? }
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :avatar_url, to: :owner, allow_nil: true
@@ -123,7 +127,7 @@ class Namespace < ApplicationRecord
scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) }
# TODO: this can be simplified with `type != 'Project'` when working on issue
# https://gitlab.com/gitlab-org/gitlab/-/issues/341070
- scope :without_project_namespaces, -> { where("type IS DISTINCT FROM ?", Namespaces::ProjectNamespace.sti_name) }
+ scope :without_project_namespaces, -> { where(Namespace.arel_table[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name)) }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
@@ -140,6 +144,7 @@ class Namespace < ApplicationRecord
'COALESCE(SUM(ps.snippets_size), 0) AS snippets_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
+ 'COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size',
'COALESCE(SUM(ps.uploads_size), 0) AS uploads_size'
)
@@ -189,9 +194,9 @@ class Namespace < ApplicationRecord
# Returns an ActiveRecord::Relation.
def search(query, include_parents: false)
if include_parents
- where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
+ without_project_namespaces.where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
else
- fuzzy_search(query, [:path, :name])
+ without_project_namespaces.fuzzy_search(query, [:path, :name])
end
end
@@ -494,6 +499,10 @@ class Namespace < ApplicationRecord
Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
end
+ def project_namespace_creation_enabled?
+ Feature.enabled?(:create_project_namespace_on_project_create, self, default_enabled: :yaml)
+ end
+
private
def expire_child_caches
@@ -535,21 +544,23 @@ class Namespace < ApplicationRecord
# Until we compare the inconsistency rates of the new specialized worker and
# the old approach, we still run AuthorizedProjectsWorker
# but with some delay and lower urgency as a safety net.
- Group
- .joins(project_group_links: :project)
- .where(projects: { namespace_id: id })
- .distinct
- .find_each do |group|
- group.refresh_members_authorized_projects(
- blocking: false,
- priority: UserProjectAccessChangedService::LOW_PRIORITY
- )
- end
+ enqueue_jobs_for_groups_requiring_authorizations_refresh(priority: UserProjectAccessChangedService::LOW_PRIORITY)
else
- Group
- .joins(project_group_links: :project)
- .where(projects: { namespace_id: id })
- .find_each(&:refresh_members_authorized_projects)
+ enqueue_jobs_for_groups_requiring_authorizations_refresh(priority: UserProjectAccessChangedService::HIGH_PRIORITY)
+ end
+ end
+
+ def enqueue_jobs_for_groups_requiring_authorizations_refresh(priority:)
+ groups_requiring_authorizations_refresh = Group
+ .joins(project_group_links: :project)
+ .where(projects: { namespace_id: id })
+ .distinct
+
+ groups_requiring_authorizations_refresh.find_each do |group|
+ group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: priority
+ )
end
end
@@ -573,7 +584,7 @@ class Namespace < ApplicationRecord
end
if user_namespace?
- errors.add(:parent_id, _('cannot not be used for user namespace'))
+ errors.add(:parent_id, _('cannot be used for user namespace'))
elsif group_namespace?
errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user_namespace?
end
diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb
index d1806c1c088..22ec550dee2 100644
--- a/app/models/namespaces/project_namespace.rb
+++ b/app/models/namespaces/project_namespace.rb
@@ -4,8 +4,6 @@ module Namespaces
class ProjectNamespace < Namespace
has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace
- validates :project, presence: true
-
def self.sti_name
'Project'
end
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index d7130322ed1..1736fe82ca5 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -161,7 +161,7 @@ module Namespaces
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
- skope = self.class.without_sti_condition
+ skope = self.class
if top
skope = skope.where("traversal_ids @> ('{?}')", top.id)
@@ -181,7 +181,6 @@ module Namespaces
# standard SELECT to avoid mismatched attribute errors when trying to
# chain future ActiveRelation commands, and retain the ordering.
skope = self.class
- .without_sti_condition
.from(skope, self.class.table_name)
.select(skope.arel_table[Arel.star])
.order(depth: hierarchy_order)
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 2da0e48c2da..f5c44171c42 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -15,12 +15,18 @@ module Namespaces
select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
end
+ def roots
+ return super unless use_traversal_ids_roots?
+
+ root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct
+ unscoped.where(id: root_ids)
+ end
+
def self_and_ancestors(include_self: true, hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestor_scopes?
records = unscoped
- .without_sti_condition
- .where(id: without_sti_condition.select('unnest(traversal_ids)'))
+ .where(id: select('unnest(traversal_ids)'))
.order_by_depth(hierarchy_order)
.normal_select
@@ -40,24 +46,24 @@ module Namespaces
def self_and_descendants(include_self: true)
return super unless use_traversal_ids?
- records = self_and_descendants_with_duplicates(include_self: include_self)
-
- distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
-
- distinct.normal_select
+ if Feature.enabled?(:traversal_ids_btree, default_enabled: :yaml)
+ self_and_descendants_with_comparison_operators(include_self: include_self)
+ else
+ records = self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
+ distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
+ distinct.normal_select
+ end
end
def self_and_descendant_ids(include_self: true)
return super unless use_traversal_ids?
- self_and_descendants_with_duplicates(include_self: include_self)
- .select('DISTINCT namespaces.id')
- end
-
- # Make sure we drop the STI `type = 'Group'` condition for better performance.
- # Logically equivalent so long as hierarchies remain homogeneous.
- def without_sti_condition
- unscope(where: :type)
+ if Feature.enabled?(:traversal_ids_btree, default_enabled: :yaml)
+ self_and_descendants_with_comparison_operators(include_self: include_self).as_ids
+ else
+ self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
+ .select('DISTINCT namespaces.id')
+ end
end
def order_by_depth(hierarchy_order)
@@ -75,7 +81,7 @@ module Namespaces
# When we have queries that break this SELECT * format we can run in to errors.
# For example `SELECT DISTINCT on(...)` will fail when we chain a `.count` c
def normal_select
- unscoped.without_sti_condition.from(all, :namespaces)
+ unscoped.from(all, :namespaces)
end
private
@@ -84,16 +90,52 @@ module Namespaces
Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
end
+ def use_traversal_ids_roots?
+ Feature.enabled?(:use_traversal_ids_roots, default_enabled: :yaml) &&
+ use_traversal_ids?
+ end
+
def use_traversal_ids_for_ancestor_scopes?
Feature.enabled?(:use_traversal_ids_for_ancestor_scopes, default_enabled: :yaml) &&
use_traversal_ids?
end
- def self_and_descendants_with_duplicates(include_self: true)
+ def self_and_descendants_with_comparison_operators(include_self: true)
+ base = all.select(
+ :traversal_ids,
+ 'LEAD (namespaces.traversal_ids, 1) OVER (ORDER BY namespaces.traversal_ids ASC) next_traversal_ids'
+ )
+ cte = Gitlab::SQL::CTE.new(:base_cte, base)
+
+ namespaces = Arel::Table.new(:namespaces)
+ records = unscoped
+ .with(cte.to_arel)
+ .from([cte.table, namespaces])
+
+ # Bound the search space to ourselves (optional) and descendants.
+ #
+ # WHERE (base_cte.next_traversal_ids IS NULL OR base_cte.next_traversal_ids > namespaces.traversal_ids)
+ # AND next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
+ records = records
+ .where(cte.table[:next_traversal_ids].eq(nil).or(cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
+ .where(next_sibling_func(cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
+
+ # AND base_cte.traversal_ids <= namespaces.traversal_ids
+ if include_self
+ records.where(cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
+ else
+ records.where(cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
+ end
+ end
+
+ def next_sibling_func(*args)
+ Arel::Nodes::NamedFunction.new('next_traversal_ids_sibling', args)
+ end
+
+ def self_and_descendants_with_duplicates_with_array_operator(include_self: true)
base_ids = select(:id)
records = unscoped
- .without_sti_condition
.from("namespaces, (#{base_ids.to_sql}) base")
.where('namespaces.traversal_ids @> ARRAY[base.id]')
diff --git a/app/models/namespaces/traversal/recursive_scopes.rb b/app/models/namespaces/traversal/recursive_scopes.rb
index 6659cefe095..925d9b8bb0c 100644
--- a/app/models/namespaces/traversal/recursive_scopes.rb
+++ b/app/models/namespaces/traversal/recursive_scopes.rb
@@ -10,6 +10,13 @@ module Namespaces
select('id')
end
+ def roots
+ Gitlab::ObjectHierarchy
+ .new(all)
+ .base_and_ancestors
+ .where(namespaces: { parent_id: nil })
+ end
+
def self_and_ancestors(include_self: true, hierarchy_order: nil)
records = Gitlab::ObjectHierarchy.new(all).base_and_ancestors(hierarchy_order: hierarchy_order)
diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb
index 22b7a0a3b2b..d4d7d352e71 100644
--- a/app/models/namespaces/user_namespace.rb
+++ b/app/models/namespaces/user_namespace.rb
@@ -3,6 +3,26 @@
# TODO: currently not created/mapped in the database, will be done in another issue
# https://gitlab.com/gitlab-org/gitlab/-/issues/341070
module Namespaces
+ ####################################################################
+ # PLEASE DO NOT OVERRIDE METHODS IN THIS CLASS!
+ #
+ # This class is a placeholder for STI. But we also want to ensure
+ # tests using `:namespace` factory are still testing the same functionality.
+ #
+ # Many legacy tests use `:namespace` which has a slight semantic
+ # mismatch as it always has been a User (personal) namespace.
+ #
+ # If you need to make a change here, please ping the
+ # Manage/Workspaces group so we can ensure that the
+ # changes do not break existing functionality.
+ #
+ # As Namespaces evolve we may be able to relax this restriction
+ # but for now, please check in with us <3
+ #
+ # For details, see the discussion in
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74152
+ ####################################################################
+
class UserNamespace < Namespace
def self.sti_name
'User'
diff --git a/app/models/note.rb b/app/models/note.rb
index 37473518892..cb285028203 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -114,6 +114,7 @@ class Note < ApplicationRecord
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
+ scope :with_discussion_ids, ->(discussion_ids) { where(discussion_id: discussion_ids) }
scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb
index e49199d911c..9221187d92a 100644
--- a/app/models/packages/npm.rb
+++ b/app/models/packages/npm.rb
@@ -9,5 +9,9 @@ module Packages
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
+
+ def self.table_name_prefix
+ 'packages_npm_'
+ end
end
end
diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb
new file mode 100644
index 00000000000..7388c4bdbd2
--- /dev/null
+++ b/app/models/packages/npm/metadatum.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Packages::Npm::Metadatum < ApplicationRecord
+ belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum
+
+ validates :package, presence: true
+ # From https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
+ validates :package_json, json_schema: { filename: "npm_package_json" }
+ validate :ensure_npm_package_type
+ validate :ensure_package_json_size
+
+ private
+
+ def ensure_npm_package_type
+ return if package&.npm?
+
+ errors.add(:base, _('Package type must be NPM'))
+ end
+
+ def ensure_package_json_size
+ return if package_json.to_s.size < 20000
+
+ errors.add(:package_json, _('structure is too large'))
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 34eae6ab5dc..962a1057a22 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -39,8 +39,9 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
+ has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
has_many :build_infos, inverse_of: :package
- has_many :pipelines, through: :build_infos
+ has_many :pipelines, through: :build_infos, disable_joins: true
has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
has_one :debian_distribution, through: :debian_publication, source: :distribution, inverse_of: :packages, class_name: 'Packages::Debian::ProjectDistribution'
@@ -102,7 +103,6 @@ class Packages::Package < ApplicationRecord
scope :with_status, ->(status) { where(status: status) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
- scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
@@ -126,11 +126,13 @@ class Packages::Package < ApplicationRecord
.where(Packages::Composer::Metadatum.table_name => { target_sha: target })
end
scope :preload_composer, -> { preload(:composer_metadatum) }
+ scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) }
scope :preload_files, -> { preload(:package_files) }
+ scope :preload_pipelines, -> { preload(pipelines: :user) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
scope :select_distinct_name, -> { select(:name).distinct }
@@ -245,7 +247,7 @@ class Packages::Package < ApplicationRecord
def versions
project.packages
- .including_build_info
+ .preload_pipelines
.including_tags
.with_name(name)
.where.not(version: version)
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 14701b8a800..87c9f56cc41 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -15,7 +15,7 @@ class Packages::PackageFile < ApplicationRecord
has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum'
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
- has_many :pipelines, through: :package_file_build_infos
+ has_many :pipelines, through: :package_file_build_infos, disable_joins: true
has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum'
has_one :helm_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Helm::FileMetadatum'
@@ -38,6 +38,7 @@ class Packages::PackageFile < ApplicationRecord
scope :with_format, ->(format) { where(::Packages::PackageFile.arel_table[:file_name].matches("%.#{format}")) }
scope :preload_package, -> { preload(:package) }
+ scope :preload_pipelines, -> { preload(pipelines: :user) }
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) }
diff --git a/app/models/preloaders/group_policy_preloader.rb b/app/models/preloaders/group_policy_preloader.rb
new file mode 100644
index 00000000000..95d6e0b5c1f
--- /dev/null
+++ b/app/models/preloaders/group_policy_preloader.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class GroupPolicyPreloader
+ def initialize(groups, current_user)
+ @groups = groups
+ @current_user = current_user
+ end
+
+ def execute
+ Preloaders::UserMaxAccessLevelInGroupsPreloader.new(@groups, @current_user).execute
+ Preloaders::GroupRootAncestorPreloader.new(@groups, root_ancestor_preloads).execute
+ end
+
+ private
+
+ def root_ancestor_preloads
+ []
+ end
+ end
+end
+
+Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader')
diff --git a/app/models/preloaders/group_root_ancestor_preloader.rb b/app/models/preloaders/group_root_ancestor_preloader.rb
new file mode 100644
index 00000000000..3ca713d9635
--- /dev/null
+++ b/app/models/preloaders/group_root_ancestor_preloader.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class GroupRootAncestorPreloader
+ def initialize(groups, root_ancestor_preloads = [])
+ @groups = groups
+ @root_ancestor_preloads = root_ancestor_preloads
+ end
+
+ def execute
+ return unless ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+
+ # type == 'Group' condition located on subquery to prevent a filter in the query
+ root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
+ .select('namespaces.*, root_query.id as source_id')
+
+ root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
+
+ root_ancestors_by_id = root_query.group_by(&:source_id)
+
+ @groups.each do |group|
+ group.root_ancestor = root_ancestors_by_id[group.id].first
+ end
+ end
+
+ private
+
+ def join_sql
+ Group.select('id, traversal_ids[1] as root_id').where(id: @groups.map(&:id)).to_sql
+ end
+ end
+end
diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
index 14f1d271572..bdd76d39ec1 100644
--- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
@@ -3,7 +3,6 @@
module Preloaders
# This class preloads the max access level (role) for the user within the given groups and
# stores the values in requests store.
- # Will only be able to preload max access level for groups where the user is a direct member
class UserMaxAccessLevelInGroupsPreloader
include BulkMemberAccessLoad
@@ -13,8 +12,17 @@ module Preloaders
end
def execute
+ if ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ preload_with_traversal_ids
+ else
+ preload_direct_memberships
+ end
+ end
+
+ private
+
+ def preload_direct_memberships
group_memberships = GroupMember.active_without_invites_and_requests
- .non_minimal_access
.where(user: @user, source_id: @groups)
.group(:source_id)
.maximum(:access_level)
@@ -23,5 +31,22 @@ module Preloaders
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
end
end
+
+ def preload_with_traversal_ids
+ max_access_levels = GroupMember.active_without_invites_and_requests
+ .where(user: @user)
+ .joins("INNER JOIN (#{traversal_join_sql}) as hierarchy ON members.source_id = hierarchy.traversal_id")
+ .group('hierarchy.id')
+ .maximum(:access_level)
+
+ @groups.each do |group|
+ max_access_level = max_access_levels[group.id] || Gitlab::Access::NO_ACCESS
+ merge_value_to_request_store(User, @user.id, group.id, max_access_level)
+ end
+ end
+
+ def traversal_join_sql
+ Namespace.select('id, unnest(traversal_ids) as traversal_id').where(id: @groups.map(&:id)).to_sql
+ end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2ceba10e86e..2288850553c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,7 +19,6 @@ class Project < ApplicationRecord
include Presentable
include HasRepository
include HasWiki
- include HasIntegrations
include CanMoveRepositoryStorage
include Routable
include GroupDescendant
@@ -98,7 +97,7 @@ class Project < ApplicationRecord
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
- before_save :ensure_project_namespace_in_sync
+ before_validation :ensure_project_namespace_in_sync
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@@ -147,7 +146,7 @@ class Project < ApplicationRecord
belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have
# a project without a project_namespace (or vice-versa)
- belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
+ belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id'
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
@@ -189,6 +188,7 @@ class Project < ApplicationRecord
has_one :prometheus_integration, class_name: 'Integrations::Prometheus', inverse_of: :project
has_one :pushover_integration, class_name: 'Integrations::Pushover'
has_one :redmine_integration, class_name: 'Integrations::Redmine'
+ has_one :shimo_integration, class_name: 'Integrations::Shimo'
has_one :slack_integration, class_name: 'Integrations::Slack'
has_one :slack_slash_commands_integration, class_name: 'Integrations::SlackSlashCommands'
has_one :teamcity_integration, class_name: 'Integrations::Teamcity'
@@ -451,6 +451,7 @@ class Project < ApplicationRecord
:allow_merge_on_skipped_pipeline=, :has_confluence?,
to: :project_setting
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
+ delegate :merge_commit_template, :merge_commit_template=, to: :project_setting, allow_nil: true
delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
@@ -475,6 +476,7 @@ class Project < ApplicationRecord
validates :project_feature, presence: true
validates :namespace, presence: true
+ validates :project_namespace, presence: true, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? }
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
@@ -492,6 +494,7 @@ class Project < ApplicationRecord
validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
+ validates :suggestion_commit_message, length: { maximum: 255 }
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
@@ -857,6 +860,18 @@ class Project < ApplicationRecord
rescue ActionController::RoutingError, URI::InvalidURIError
nil
end
+
+ def without_integration(integration)
+ integrations = Integration
+ .select('1')
+ .where("#{Integration.table_name}.project_id = projects.id")
+ .where(type: integration.type)
+
+ Project
+ .where('NOT EXISTS (?)', integrations)
+ .where(pending_delete: false)
+ .where(archived: false)
+ end
end
def initialize(attributes = nil)
@@ -1453,7 +1468,7 @@ class Project < ApplicationRecord
end
def disabled_integrations
- [:zentao]
+ [:shimo]
end
def find_or_initialize_integration(name)
@@ -1777,10 +1792,12 @@ class Project < ApplicationRecord
def all_runners
Ci::Runner.from_union([runners, group_runners, shared_runners])
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937')
end
def all_available_runners
Ci::Runner.from_union([runners, group_runners, available_shared_runners])
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937')
end
# Once issue 339937 is fixed, please search for all mentioned of
@@ -2051,14 +2068,16 @@ class Project < ApplicationRecord
end
def predefined_variables
- Gitlab::Ci::Variables::Collection.new
- .concat(predefined_ci_server_variables)
- .concat(predefined_project_variables)
- .concat(pages_variables)
- .concat(container_registry_variables)
- .concat(dependency_proxy_variables)
- .concat(auto_devops_variables)
- .concat(api_variables)
+ strong_memoize(:predefined_variables) do
+ Gitlab::Ci::Variables::Collection.new
+ .concat(predefined_ci_server_variables)
+ .concat(predefined_project_variables)
+ .concat(pages_variables)
+ .concat(container_registry_variables)
+ .concat(dependency_proxy_variables)
+ .concat(auto_devops_variables)
+ .concat(api_variables)
+ end
end
def predefined_project_variables
@@ -2579,18 +2598,21 @@ class Project < ApplicationRecord
config = Gitlab.config.incoming_email
wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
- config.address&.gsub(wildcard, "#{full_path_slug}-#{id}-issue-")
+ config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
end
def service_desk_custom_address
return unless Gitlab::ServiceDeskEmail.enabled?
- key = service_desk_setting&.project_key
- return unless key.present?
+ key = service_desk_setting&.project_key || default_service_desk_suffix
Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
+ def default_service_desk_suffix
+ "#{id}-issue-"
+ end
+
def root_namespace
if namespace.has_parent?
namespace.root_ancestor
@@ -2911,12 +2933,28 @@ class Project < ApplicationRecord
end
def ensure_project_namespace_in_sync
- if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present?
- project_namespace.name = name
- project_namespace.path = path
- project_namespace.parent = namespace
- project_namespace.visibility_level = visibility_level
- end
+ # create project_namespace when project is created if create_project_namespace_on_project_create FF is enabled
+ build_project_namespace if project_namespace_creation_enabled?
+
+ # regardless of create_project_namespace_on_project_create FF we need
+ # to keep project and project namespace in sync if there is one
+ sync_attributes(project_namespace) if sync_project_namespace?
+ end
+
+ def project_namespace_creation_enabled?
+ new_record? && !project_namespace && self.namespace && self.root_namespace.project_namespace_creation_enabled?
+ end
+
+ def sync_project_namespace?
+ (changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present?
+ end
+
+ def sync_attributes(project_namespace)
+ project_namespace.name = name
+ project_namespace.path = path
+ project_namespace.parent = namespace
+ project_namespace.shared_runners_enabled = shared_runners_enabled
+ project_namespace.visibility_level = visibility_level
end
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 64e768007ee..fed19a37a16 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -24,8 +24,9 @@ class ProjectAuthorization < ApplicationRecord
end
connection.execute <<-EOF.strip_heredoc
- INSERT INTO project_authorizations (user_id, project_id, access_level)
- VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ ON CONFLICT DO NOTHING
EOF
end
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 24d892290a6..6c8d2226bc9 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -12,6 +12,8 @@ class ProjectSetting < ApplicationRecord
self.primary_key = :project_id
+ validates :merge_commit_template, length: { maximum: 500 }
+
def squash_enabled_by_default?
%w[always default_on].include?(squash_option)
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 774d81156b7..94904e9792f 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -41,13 +41,15 @@ class ProjectTeam
member
end
- def add_users(users, access_level, current_user: nil, expires_at: nil)
+ def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id
)
end
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index f3352ecc5ee..8d6f8c3a9ca 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -28,3 +28,5 @@ module Projects
end
end
end
+
+::Projects::Topic.prepend_mod_with('Projects::Topic')
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
index 441b94e1855..8358be35470 100644
--- a/app/models/push_event_payload.rb
+++ b/app/models/push_event_payload.rb
@@ -2,9 +2,6 @@
class PushEventPayload < ApplicationRecord
extend SuppressCompositePrimaryKeyWarning
- include IgnorableColumns
-
- ignore_columns :event_id_convert_to_bigint, remove_with: '14.4', remove_after: '2021-10-22'
include ShaAttribute
diff --git a/app/models/release.rb b/app/models/release.rb
index eac6346cc60..0fda6940249 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -34,6 +34,7 @@ class Release < ApplicationRecord
project: [:project_feature, :route, { namespace: :route }])
}
scope :with_milestones, -> { joins(:milestone_releases) }
+ scope :with_group_milestones, -> { joins(:milestones).where.not(milestones: { group_id: nil }) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) }
scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 119d874a6e1..47482f04bca 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -731,10 +731,8 @@ class Repository
raw_repository.local_branches(sort_by: sort_by, pagination_params: pagination_params)
end
- def tags_sorted_by(value)
- return raw_repository.tags(sort_by: value) if Feature.enabled?(:tags_finder_gitaly, project, default_enabled: :yaml)
-
- tags_ruby_sort(value)
+ def tags_sorted_by(value, pagination_params = nil)
+ raw_repository.tags(sort_by: value, pagination_params: pagination_params)
end
# Params:
@@ -1091,6 +1089,13 @@ class Repository
after_create
true
+ rescue Gitlab::Git::Repository::RepositoryExists
+ # We do not want to call `#after_create` given that we didn't create the
+ # repo, but we obviously have a mismatch between what's in our exists cache
+ # and actual on-disk state as seen by Gitaly. Let's thus expire our caches.
+ expire_status_cache
+
+ nil
end
def create_from_bundle(bundle_path)
@@ -1163,34 +1168,6 @@ class Repository
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
- # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/339741
- def tags_ruby_sort(value)
- case value
- when 'name_asc'
- VersionSorter.sort(tags) { |tag| tag.name }
- when 'name_desc'
- VersionSorter.rsort(tags) { |tag| tag.name }
- when 'updated_desc'
- tags_sorted_by_committed_date.reverse
- when 'updated_asc'
- tags_sorted_by_committed_date
- else
- tags
- end
- end
-
- # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/339741
- def tags_sorted_by_committed_date
- # Annotated tags can point to any object (e.g. a blob), but generally
- # tags point to a commit. If we don't have a commit, then just default
- # to putting the tag at the end of the list.
- default = Time.current
-
- tags.sort_by do |tag|
- tag.dereferenced_target&.committed_date || default
- end
- end
-
def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, tags)
end
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index ff564d87449..f1ca5c23997 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -50,6 +50,7 @@ class Suggestion < ApplicationRecord
next _("Can't apply as the source branch was deleted.") unless noteable.source_branch_exists?
next outdated_reason if outdated?(cached: cached) || !note.active?
next _("This suggestion already matches its content.") unless different_content?
+ next _("This file was modified for readability, and can't accept suggestions. Edit it directly.") if file_path.end_with? "ipynb"
end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 94a99603848..742b8fd2a9d 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -18,6 +18,7 @@ class Todo < ApplicationRecord
DIRECTLY_ADDRESSED = 7
MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature
REVIEW_REQUESTED = 9
+ ATTENTION_REQUESTED = 10
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -28,7 +29,8 @@ class Todo < ApplicationRecord
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable,
DIRECTLY_ADDRESSED => :directly_addressed,
- MERGE_TRAIN_REMOVED => :merge_train_removed
+ MERGE_TRAIN_REMOVED => :merge_train_removed,
+ ATTENTION_REQUESTED => :attention_requested
}.freeze
belongs_to :author, class_name: "User"
@@ -189,6 +191,10 @@ class Todo < ApplicationRecord
action == REVIEW_REQUESTED
end
+ def attention_requested?
+ action == ATTENTION_REQUESTED
+ end
+
def merge_train_removed?
action == MERGE_TRAIN_REMOVED
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index c1a3df82457..ac7ebb31abc 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -2,6 +2,7 @@
class Upload < ApplicationRecord
include Checksummable
+
# Upper limit for foreground checksum processing
CHECKSUM_THRESHOLD = 100.megabytes
@@ -51,9 +52,9 @@ class Upload < ApplicationRecord
##
# FastDestroyAll concerns
- def finalize_fast_destroy(keys)
- keys.each do |store_class, paths|
- store_class.new.delete_keys_async(paths)
+ def finalize_fast_destroy(items_to_remove)
+ items_to_remove.each do |store_class, keys|
+ store_class.new.delete_keys_async(keys)
end
end
end
@@ -65,6 +66,10 @@ class Upload < ApplicationRecord
uploader_class.absolute_path(self)
end
+ def relative_path
+ uploader_class.relative_path(self)
+ end
+
def calculate_checksum!
self.checksum = nil
return unless needs_checksum?
diff --git a/app/models/uploads/fog.rb b/app/models/uploads/fog.rb
index b44e273e9ab..5d57b644dbe 100644
--- a/app/models/uploads/fog.rb
+++ b/app/models/uploads/fog.rb
@@ -15,13 +15,21 @@ module Uploads
end
def delete_keys(keys)
- keys.each do |key|
- connection.delete_object(bucket_name, key)
- end
+ keys.each { |key| delete_object(key) }
end
private
+ def delete_object(key)
+ connection.delete_object(bucket_name, key)
+
+ # So far, only GoogleCloudStorage raises an exception when the file is not found.
+ # Other providers support idempotent requests and does not raise an error
+ # when the file is missing.
+ rescue ::Google::Apis::ClientError => e
+ Gitlab::ErrorTracking.log_exception(e)
+ end
+
def object_store
Gitlab.config.uploads.object_store
end
diff --git a/app/models/uploads/local.rb b/app/models/uploads/local.rb
index bd295a66838..9df69998991 100644
--- a/app/models/uploads/local.rb
+++ b/app/models/uploads/local.rb
@@ -55,3 +55,5 @@ module Uploads
end
end
end
+
+Uploads::Local.prepend_mod
diff --git a/app/models/user.rb b/app/models/user.rb
index 0e19e6e4a79..3ab5b7ee364 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -123,7 +123,7 @@ class User < ApplicationRecord
# Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :expired_and_unnotified_keys, -> { expired_and_not_notified }, class_name: 'Key'
+ has_many :expired_today_and_unnotified_keys, -> { expired_today_and_not_notified }, class_name: 'Key'
has_many :expiring_soon_and_unnotified_keys, -> { expiring_soon_and_not_notified }, class_name: 'Key'
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :group_deploy_keys
@@ -274,14 +274,21 @@ class User < ApplicationRecord
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
+ after_create :add_primary_email_to_emails!, if: :confirmed?
after_commit(on: :update) do
if previous_changes.key?('email')
- # Grab previous_email here since previous_changes changes after
- # #update_emails_with_primary_email and #update_notification_email are called
+ # Add the old primary email to Emails if not added already - this should be removed
+ # after the background migration for MR https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70872/ has completed,
+ # as the primary email is now added to Emails upon confirmation
+ # Issue to remove that: https://gitlab.com/gitlab-org/gitlab/-/issues/344134
previous_confirmed_at = previous_changes.key?('confirmed_at') ? previous_changes['confirmed_at'][0] : confirmed_at
previous_email = previous_changes[:email][0]
+ if previous_confirmed_at && !emails.exists?(email: previous_email)
+ # rubocop: disable CodeReuse/ServiceClass
+ Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)
+ # rubocop: enable CodeReuse/ServiceClass
+ end
- update_emails_with_primary_email(previous_confirmed_at, previous_email)
update_invalid_gpg_signatures
end
end
@@ -454,8 +461,8 @@ class User < ApplicationRecord
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) }
- scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
- scope :with_no_activity, -> { active.where(last_activity_on: nil) }
+ scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
+ scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :get_ids_by_username, -> (username) { where(username: username).pluck(:id) }
@@ -466,7 +473,11 @@ class User < ApplicationRecord
end
def active_for_authentication?
- super && can?(:log_in)
+ return false unless super
+
+ check_ldap_if_ldap_blocked!
+
+ can?(:log_in)
end
# The messages for these keys are defined in `devise.en.yml`
@@ -935,6 +946,8 @@ class User < ApplicationRecord
end
def unique_email
+ return if errors.added?(:email, _('has already been taken'))
+
if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, _('has already been taken'))
end
@@ -963,24 +976,6 @@ class User < ApplicationRecord
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
end
- # Note: the use of the Emails services will cause `saves` on the user object, running
- # through the callbacks again and can have side effects, such as the `previous_changes`
- # hash and `_was` variables getting munged.
- # By using an `after_commit` instead of `after_update`, we avoid the recursive callback
- # scenario, though it then requires us to use the `previous_changes` hash
- # rubocop: disable CodeReuse/ServiceClass
- def update_emails_with_primary_email(previous_confirmed_at, previous_email)
- primary_email_record = emails.find_by(email: email)
- Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
-
- # the original primary email was confirmed, and we want that to carry over. We don't
- # have access to the original confirmation values at this point, so just set confirmed_at
- Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)
-
- update_columns(confirmed_at: primary_email_record.confirmed_at) if primary_email_record&.confirmed_at
- end
- # rubocop: enable CodeReuse/ServiceClass
-
def update_invalid_gpg_signatures
gpg_keys.each(&:update_invalid_gpg_signatures)
end
@@ -1025,8 +1020,10 @@ class User < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def remove_project_authorizations(project_ids)
- project_authorizations.where(project_id: project_ids).delete_all
+ def remove_project_authorizations(project_ids, per_batch = 1000)
+ project_ids.each_slice(per_batch) do |project_ids_batch|
+ project_authorizations.where(project_id: project_ids_batch).delete_all
+ end
end
def authorized_projects(min_access_level = nil)
@@ -1389,7 +1386,7 @@ class User < ApplicationRecord
all_emails << email unless temp_oauth_email?
all_emails << private_commit_email if include_private_email
all_emails.concat(emails.map(&:email))
- all_emails
+ all_emails.uniq
end
def verified_emails(include_private_email: true)
@@ -1397,7 +1394,7 @@ class User < ApplicationRecord
verified_emails << email if primary_email_verified?
verified_emails << private_commit_email if include_private_email
verified_emails.concat(emails.confirmed.pluck(:email))
- verified_emails
+ verified_emails.uniq
end
def public_verified_emails
@@ -1610,8 +1607,6 @@ class User < ApplicationRecord
true
end
- # TODO Please check all callers and remove allow_cross_joins_across_databases,
- # when https://gitlab.com/gitlab-org/gitlab/-/issues/336436 is done.
def ci_owned_runners
@ci_owned_runners ||= begin
project_runners = Ci::RunnerProject
@@ -1624,7 +1619,7 @@ class User < ApplicationRecord
.joins(:runner)
.select('ci_runners.*')
- Ci::Runner.from_union([project_runners, group_runners])
+ Ci::Runner.from_union([project_runners, group_runners]).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436')
end
end
@@ -1980,6 +1975,37 @@ class User < ApplicationRecord
ci_job_token_scope.present?
end
+ # override from Devise::Confirmable
+ #
+ # Add the primary email to user.emails (or confirm it if it was already
+ # present) when the primary email is confirmed.
+ def confirm(*args)
+ saved = super(*args)
+ return false unless saved
+
+ email_to_confirm = self.emails.find_by(email: self.email)
+
+ if email_to_confirm.present?
+ email_to_confirm.confirm(*args)
+ else
+ add_primary_email_to_emails!
+ end
+
+ saved
+ end
+
+ def user_project
+ strong_memoize(:user_project) do
+ personal_projects.find_by(path: username, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ def user_readme
+ strong_memoize(:user_readme) do
+ user_project&.repository&.readme
+ end
+ end
+
protected
# override, from Devise::Validatable
@@ -2020,6 +2046,12 @@ class User < ApplicationRecord
'en'
end
+ # rubocop: disable CodeReuse/ServiceClass
+ def add_primary_email_to_emails!
+ Emails::CreateService.new(self, user: self, email: self.email).execute(confirmed_at: self.confirmed_at)
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
def notification_email_verified
return if notification_email.blank? || temp_oauth_email?
@@ -2153,6 +2185,13 @@ class User < ApplicationRecord
def ci_job_token_scope_cache_key
"users:#{id}:ci:job_token_scope"
end
+
+ # An `ldap_blocked` user will be unblocked if LDAP indicates they are allowed.
+ def check_ldap_if_ldap_blocked!
+ return unless ::Gitlab::Auth::Ldap::Config.enabled? && ldap_blocked?
+
+ ::Gitlab::Auth::Ldap::Access.allowed?(self)
+ end
end
User.prepend_mod_with('User')
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
index 1c8634e47c3..7a803e8f1f6 100644
--- a/app/models/user_status.rb
+++ b/app/models/user_status.rb
@@ -22,7 +22,7 @@ class UserStatus < ApplicationRecord
enum availability: { not_set: 0, busy: 1 }
validates :user, presence: true
- validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names }
+ validates :emoji, 'gitlab/emoji_name': true
validates :message, length: { maximum: 100 }, allow_blank: true
scope :scheduled_for_cleanup, -> { where(arel_table[:clear_status_at].lteq(Time.current)) }
@@ -33,3 +33,5 @@ class UserStatus < ApplicationRecord
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
end
end
+
+UserStatus.prepend_mod_with('UserStatus')
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index a4cc43d1f13..556ee03605d 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -9,6 +9,7 @@ module Users
belongs_to :user
validates :holder_name, length: { maximum: 26 }
+ validates :network, length: { maximum: 32 }
validates :last_digits, allow_nil: true, numericality: {
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
}
@@ -17,7 +18,7 @@ module Users
self.class.where(
expiration_date: expiration_date,
last_digits: last_digits,
- holder_name: holder_name
+ network: network
).order(credit_card_validated_at: :desc).includes(:user)
end
end
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index 8fe52ac7ecc..1f1eaacfe5c 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -22,7 +22,8 @@ module Users
experience: 4,
team_short: 5,
trial_short: 6,
- admin_verify: 7
+ admin_verify: 7,
+ invite_team: 8
}, _suffix: true
scope :without_track_and_series, -> (track, series) do
diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb
index a903541f69a..a314ae8920b 100644
--- a/app/models/users_statistics.rb
+++ b/app/models/users_statistics.rb
@@ -3,12 +3,6 @@
class UsersStatistics < ApplicationRecord
scope :order_created_at_desc, -> { order(created_at: :desc) }
- class << self
- def latest
- order_created_at_desc.first
- end
- end
-
def active
[
without_groups_and_projects,
@@ -26,30 +20,26 @@ class UsersStatistics < ApplicationRecord
end
class << self
- def create_current_stats!
- stats_by_role = highest_role_stats
+ def latest
+ order_created_at_desc.first
+ end
- create!(
- without_groups_and_projects: without_groups_and_projects_stats,
- with_highest_role_guest: stats_by_role[:guest],
- with_highest_role_reporter: stats_by_role[:reporter],
- with_highest_role_developer: stats_by_role[:developer],
- with_highest_role_maintainer: stats_by_role[:maintainer],
- with_highest_role_owner: stats_by_role[:owner],
- bots: bot_stats,
- blocked: blocked_stats
- )
+ def create_current_stats!
+ create!(highest_role_stats)
end
private
def highest_role_stats
{
- owner: batch_count_for_access_level(Gitlab::Access::OWNER),
- maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
- developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
- reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
- guest: batch_count_for_access_level(Gitlab::Access::GUEST)
+ without_groups_and_projects: without_groups_and_projects_stats,
+ with_highest_role_guest: batch_count_for_access_level(Gitlab::Access::GUEST),
+ with_highest_role_reporter: batch_count_for_access_level(Gitlab::Access::REPORTER),
+ with_highest_role_developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER),
+ with_highest_role_maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER),
+ with_highest_role_owner: batch_count_for_access_level(Gitlab::Access::OWNER),
+ bots: bot_stats,
+ blocked: blocked_stats
}
end
diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb
index 76f8faa11c7..71b50192e29 100644
--- a/app/models/webauthn_registration.rb
+++ b/app/models/webauthn_registration.rb
@@ -5,7 +5,8 @@
class WebauthnRegistration < ApplicationRecord
belongs_to :user
- validates :credential_xid, :public_key, :name, :counter, presence: true
+ validates :credential_xid, :public_key, :counter, presence: true
+ validates :name, length: { minimum: 0, allow_nil: false }
validates :counter,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
end