Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/analytics/cycle_analytics/issue_stage_event.rb11
-rw-r--r--app/models/analytics/cycle_analytics/merge_request_stage_event.rb11
-rw-r--r--app/models/application_record.rb36
-rw-r--r--app/models/application_setting.rb102
-rw-r--r--app/models/application_setting_implementation.rb17
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/bulk_imports/entity.rb24
-rw-r--r--app/models/bulk_imports/tracker.rb4
-rw-r--r--app/models/ci/application_record.rb1
-rw-r--r--app/models/ci/bridge.rb18
-rw-r--r--app/models/ci/build.rb43
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb10
-rw-r--r--app/models/ci/build_trace_metadata.rb45
-rw-r--r--app/models/ci/job_artifact.rb15
-rw-r--r--app/models/ci/pending_build.rb82
-rw-r--r--app/models/ci/pipeline.rb29
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb40
-rw-r--r--app/models/ci/sources/pipeline.rb3
-rw-r--r--app/models/clusters/agent.rb6
-rw-r--r--app/models/clusters/agents/group_authorization.rb16
-rw-r--r--app/models/clusters/agents/implicit_authorization.rb20
-rw-r--r--app/models/clusters/agents/project_authorization.rb14
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/clusters_hierarchy.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb16
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/compare.rb10
-rw-r--r--app/models/concerns/approvable_base.rb4
-rw-r--r--app/models/concerns/cache_markdown_field.rb33
-rw-r--r--app/models/concerns/calloutable.rb15
-rw-r--r--app/models/concerns/ci/contextable.rb4
-rw-r--r--app/models/concerns/cron_schedulable.rb8
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb1
-rw-r--r--app/models/concerns/featurable.rb21
-rw-r--r--app/models/concerns/has_repository.rb4
-rw-r--r--app/models/concerns/integrations/has_data_fields.rb1
-rw-r--r--app/models/concerns/issuable.rb17
-rw-r--r--app/models/concerns/loose_foreign_key.rb95
-rw-r--r--app/models/concerns/mentionable.rb11
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb121
-rw-r--r--app/models/concerns/partitioned_table.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb12
-rw-r--r--app/models/concerns/sanitizable.rb52
-rw-r--r--app/models/concerns/sortable_title.rb21
-rw-r--r--app/models/concerns/taggable_queries.rb10
-rw-r--r--app/models/customer_relations/contact.rb33
-rw-r--r--app/models/customer_relations/organization.rb10
-rw-r--r--app/models/dependency_proxy/blob.rb3
-rw-r--r--app/models/dependency_proxy/image_ttl_group_policy.rb11
-rw-r--r--app/models/dependency_proxy/manifest.rb3
-rw-r--r--app/models/deploy_keys_project.rb1
-rw-r--r--app/models/design_management/action.rb2
-rw-r--r--app/models/diff_note.rb28
-rw-r--r--app/models/environment.rb73
-rw-r--r--app/models/environment_status.rb10
-rw-r--r--app/models/error_tracking/client_key.rb6
-rw-r--r--app/models/error_tracking/error.rb25
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb2
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/group.rb50
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/models/instance_configuration.rb17
-rw-r--r--app/models/integration.rb6
-rw-r--r--app/models/integrations/base_chat_notification.rb2
-rw-r--r--app/models/integrations/datadog.rb2
-rw-r--r--app/models/integrations/prometheus.rb2
-rw-r--r--app/models/integrations/slack_slash_commands.rb2
-rw-r--r--app/models/integrations/zentao.rb78
-rw-r--r--app/models/integrations/zentao_tracker_data.rb23
-rw-r--r--app/models/internal_id.rb209
-rw-r--r--app/models/issue.rb23
-rw-r--r--app/models/issue/metrics.rb39
-rw-r--r--app/models/loose_foreign_keys.rb7
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb49
-rw-r--r--app/models/member.rb7
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb79
-rw-r--r--app/models/merge_request/metrics.rb19
-rw-r--r--app/models/merge_request_diff.rb17
-rw-r--r--app/models/milestone.rb30
-rw-r--r--app/models/namespace.rb60
-rw-r--r--app/models/namespace_setting.rb14
-rw-r--r--app/models/namespaces/project_namespace.rb13
-rw-r--r--app/models/namespaces/traversal/linear.rb5
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb51
-rw-r--r--app/models/namespaces/traversal/recursive.rb8
-rw-r--r--app/models/namespaces/traversal/recursive_scopes.rb16
-rw-r--r--app/models/namespaces/user_namespace.rb11
-rw-r--r--app/models/note.rb17
-rw-r--r--app/models/onboarding_progress.rb2
-rw-r--r--app/models/operations/feature_flag.rb6
-rw-r--r--app/models/operations/feature_flag_scope.rb4
-rw-r--r--app/models/packages/package.rb9
-rw-r--r--app/models/packages/package_file.rb22
-rw-r--r--app/models/pages_deployment.rb7
-rw-r--r--app/models/postgresql/detached_partition.rb2
-rw-r--r--app/models/preloaders/commit_status_preloader.rb29
-rw-r--r--app/models/preloaders/merge_requests_preloader.rb19
-rw-r--r--app/models/preloaders/user_max_access_level_in_groups_preloader.rb27
-rw-r--r--app/models/project.rb137
-rw-r--r--app/models/project_feature.rb16
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/projects/project_topic.rb8
-rw-r--r--app/models/projects/topic.rb10
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/push_event_payload.rb3
-rw-r--r--app/models/release.rb1
-rw-r--r--app/models/repository.rb62
-rw-r--r--app/models/service_desk_setting.rb12
-rw-r--r--app/models/shard.rb6
-rw-r--r--app/models/user.rb181
-rw-r--r--app/models/user_callout.rb9
-rw-r--r--app/models/user_detail.rb17
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/users/group_callout.rb25
-rw-r--r--app/models/work_item/type.rb24
-rw-r--r--app/models/zoom_meeting.rb2
118 files changed, 1763 insertions, 977 deletions
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
new file mode 100644
index 00000000000..1da8973ff21
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class IssueStageEvent < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
+ validates(*%i[stage_event_hash_id issue_id group_id project_id start_event_timestamp], presence: true)
+ 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
new file mode 100644
index 00000000000..d2f899ae933
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class MergeRequestStageEvent < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
+ validates(*%i[stage_event_hash_id merge_request_id group_id project_id start_event_timestamp], presence: true)
+ end
+ end
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index d9375b55e89..d2757d8c17d 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
+ self.gitlab_schema = :gitlab_main
self.abstract_class = true
alias_method :reset, :reload
@@ -30,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_ensure_unique(retries: 0)
- transaction(requires_new: true) do
+ transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield
end
rescue ActiveRecord::RecordNotUnique
@@ -54,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base
# currently one third of the default 15-second timeout
def self.with_fast_read_statement_timeout(timeout_ms = 5000)
::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
- transaction(requires_new: true) do
+ transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
yield
@@ -63,14 +64,6 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_find_or_create_by(*args, &block)
- return optimized_safe_find_or_create_by(*args, &block) if Feature.enabled?(:optimize_safe_find_or_create_by, default_enabled: :yaml)
-
- safe_ensure_unique(retries: 1) do
- find_or_create_by(*args, &block)
- end
- end
-
- def self.optimized_safe_find_or_create_by(*args, &block)
record = find_by(*args)
return record if record.present?
@@ -79,7 +72,7 @@ class ApplicationRecord < ActiveRecord::Base
#
# When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
# and that skips some code that adds the newly created record to the association.
- transaction(requires_new: true) { all.create(*args, &block) }
+ transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions
rescue ActiveRecord::RecordNotUnique
find_by(*args)
end
@@ -103,23 +96,18 @@ class ApplicationRecord < ActiveRecord::Base
enum(enum_mod.key => values)
end
- def self.transaction(**options, &block)
- if options[:requires_new] && track_subtransactions?
- ::Gitlab::Database::Metrics.subtransactions_increment(self.name)
- end
-
- super(**options, &block)
- end
-
- def self.track_subtransactions?
- ::Feature.enabled?(:active_record_subtransactions_counter, type: :ops, default_enabled: :yaml) &&
- connection.transaction_open?
- end
-
def self.cached_column_list
self.column_names.map { |column_name| self.arel_table[column_name] }
end
+ def self.default_select_columns
+ if ignored_columns.any?
+ cached_column_list
+ else
+ arel_table[Arel.star]
+ end
+ end
+
def readable_by?(user)
Ability.allowed?(user, "read_#{to_ability_name}".to_sym, self)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index c4b6bcb9395..5f16b990d01 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -6,6 +6,7 @@ class ApplicationSetting < ApplicationRecord
include TokenAuthenticatable
include ChronicDurationAttribute
include IgnorableColumns
+ include Sanitizable
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
ignore_column :seat_link_enabled, remove_with: '14.4', remove_after: '2021-09-22'
@@ -32,6 +33,8 @@ class ApplicationSetting < ApplicationRecord
alias_attribute :instance_group_id, :instance_administrators_group_id
alias_attribute :instance_administrators_group, :instance_group
+ sanitizes! :default_branch_name
+
def self.kroki_formats_attributes
{
blockdiag: {
@@ -204,6 +207,10 @@ class ApplicationSetting < ApplicationRecord
numericality: { only_integer: true, greater_than_or_equal_to: 0,
less_than: ::Gitlab::Pages::MAX_SIZE / 1.megabyte }
+ validates :jobs_per_stage_page_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
@@ -343,6 +350,8 @@ class ApplicationSetting < ApplicationRecord
validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes }
+ validates :max_yaml_size_bytes, numericality: { only_integer: true, greater_than: 0 }, presence: true
+ validates :max_yaml_depth, numericality: { only_integer: true, greater_than: 0 }, presence: true
validates :email_restrictions, untrusted_regexp: true
@@ -463,53 +472,28 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
allow_blank: true
- validates :throttle_unauthenticated_requests_per_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_unauthenticated_period_in_seconds,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_unauthenticated_packages_api_requests_per_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_unauthenticated_packages_api_period_in_seconds,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_authenticated_api_requests_per_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_authenticated_api_period_in_seconds,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_authenticated_web_requests_per_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_authenticated_web_period_in_seconds,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_authenticated_packages_api_requests_per_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_authenticated_packages_api_period_in_seconds,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_protected_paths_requests_per_period,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
-
- validates :throttle_protected_paths_period_in_seconds,
- presence: true,
- numericality: { only_integer: true, greater_than: 0 }
+ with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do
+ validates :throttle_unauthenticated_api_requests_per_period
+ validates :throttle_unauthenticated_api_period_in_seconds
+ validates :throttle_unauthenticated_requests_per_period
+ validates :throttle_unauthenticated_period_in_seconds
+ validates :throttle_unauthenticated_packages_api_requests_per_period
+ validates :throttle_unauthenticated_packages_api_period_in_seconds
+ validates :throttle_unauthenticated_files_api_requests_per_period
+ validates :throttle_unauthenticated_files_api_period_in_seconds
+ validates :throttle_authenticated_api_requests_per_period
+ validates :throttle_authenticated_api_period_in_seconds
+ validates :throttle_authenticated_git_lfs_requests_per_period
+ validates :throttle_authenticated_git_lfs_period_in_seconds
+ validates :throttle_authenticated_web_requests_per_period
+ validates :throttle_authenticated_web_period_in_seconds
+ validates :throttle_authenticated_packages_api_requests_per_period
+ validates :throttle_authenticated_packages_api_period_in_seconds
+ validates :throttle_authenticated_files_api_requests_per_period
+ validates :throttle_authenticated_files_api_period_in_seconds
+ validates :throttle_protected_paths_requests_per_period
+ validates :throttle_protected_paths_period_in_seconds
+ end
validates :notes_create_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -534,6 +518,18 @@ class ApplicationSetting < ApplicationRecord
validates :floc_enabled,
inclusion: { in: [true, false], message: _('must be a boolean value') }
+ enum sidekiq_job_limiter_mode: {
+ Gitlab::SidekiqMiddleware::SizeLimiter::Validator::TRACK_MODE => 0,
+ Gitlab::SidekiqMiddleware::SizeLimiter::Validator::COMPRESS_MODE => 1 # The default
+ }
+
+ validates :sidekiq_job_limiter_mode,
+ inclusion: { in: self.sidekiq_job_limiter_modes }
+ validates :sidekiq_job_limiter_compression_threshold_bytes,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :sidekiq_job_limiter_limit_bytes,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -573,7 +569,7 @@ class ApplicationSetting < ApplicationRecord
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
- before_validation :sanitize_default_branch_name
+ before_validation :normalize_default_branch_name
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -603,12 +599,8 @@ class ApplicationSetting < ApplicationRecord
!!(sourcegraph_url =~ %r{\Ahttps://(www\.)?sourcegraph\.com})
end
- def sanitize_default_branch_name
- self.default_branch_name = if default_branch_name.blank?
- nil
- else
- Sanitize.fragment(self.default_branch_name)
- end
+ def normalize_default_branch_name
+ self.default_branch_name = default_branch_name.presence
end
def instance_review_permitted?
@@ -622,7 +614,7 @@ class ApplicationSetting < ApplicationRecord
def self.create_from_defaults
check_schema!
- transaction(requires_new: true) do
+ transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
super
end
rescue ActiveRecord::RecordNotUnique
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 060c831a11b..612fda158d3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -109,6 +109,8 @@ module ApplicationSettingImplementation
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
max_import_size: 0,
+ max_yaml_size_bytes: 1.megabyte,
+ max_yaml_depth: 100,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
notes_create_limit: 300,
@@ -161,24 +163,36 @@ module ApplicationSettingImplementation
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
throttle_authenticated_api_requests_per_period: 7200,
+ throttle_authenticated_git_lfs_enabled: false,
+ throttle_authenticated_git_lfs_period_in_seconds: 60,
+ throttle_authenticated_git_lfs_requests_per_period: 1000,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_web_requests_per_period: 7200,
throttle_authenticated_packages_api_enabled: false,
throttle_authenticated_packages_api_period_in_seconds: 15,
throttle_authenticated_packages_api_requests_per_period: 1000,
+ throttle_authenticated_files_api_enabled: false,
+ throttle_authenticated_files_api_period_in_seconds: 15,
+ throttle_authenticated_files_api_requests_per_period: 500,
throttle_incident_management_notification_enabled: false,
throttle_incident_management_notification_per_period: 3600,
throttle_incident_management_notification_period_in_seconds: 3600,
throttle_protected_paths_enabled: false,
throttle_protected_paths_in_seconds: 10,
throttle_protected_paths_per_period: 60,
+ throttle_unauthenticated_api_enabled: false,
+ throttle_unauthenticated_api_period_in_seconds: 3600,
+ throttle_unauthenticated_api_requests_per_period: 3600,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_unauthenticated_requests_per_period: 3600,
throttle_unauthenticated_packages_api_enabled: false,
throttle_unauthenticated_packages_api_period_in_seconds: 15,
throttle_unauthenticated_packages_api_requests_per_period: 800,
+ throttle_unauthenticated_files_api_enabled: false,
+ throttle_unauthenticated_files_api_period_in_seconds: 15,
+ throttle_unauthenticated_files_api_requests_per_period: 125,
time_tracking_limit_to_hours: false,
two_factor_grace_period: 48,
unique_ips_limit_enabled: false,
@@ -197,7 +211,8 @@ module ApplicationSettingImplementation
kroki_url: nil,
kroki_formats: { blockdiag: false, bpmn: false, excalidraw: false },
rate_limiting_response_text: nil,
- whats_new_variant: 0
+ whats_new_variant: 0,
+ user_deactivation_emails_enabled: true
}
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index c8f6b9aaedb..d251b0adbd3 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -66,3 +66,5 @@ class AwardEmoji < ApplicationRecord
awardable.try(:update_upvotes_count) if upvote?
end
end
+
+AwardEmoji.prepend_mod_with('AwardEmoji')
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 24f86b44841..ab5d248ff8c 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -78,6 +78,30 @@ class BulkImports::Entity < ApplicationRecord
ERB::Util.url_encode(source_full_path)
end
+ def pipelines
+ @pipelines ||= case source_type
+ when 'group_entity'
+ BulkImports::Groups::Stage.pipelines
+ when 'project_entity'
+ BulkImports::Projects::Stage.pipelines
+ end
+ end
+
+ def pipeline_exists?(name)
+ pipelines.any? { |_, pipeline| pipeline.to_s == name.to_s }
+ end
+
+ def create_pipeline_trackers!
+ self.class.transaction do
+ pipelines.each do |stage, pipeline|
+ trackers.create!(
+ stage: stage,
+ pipeline_name: pipeline
+ )
+ end
+ end
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 1b108d5c042..c185470b1c2 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -34,8 +34,8 @@ class BulkImports::Tracker < ApplicationRecord
end
def pipeline_class
- unless BulkImports::Stage.pipeline_exists?(pipeline_name)
- raise NameError, "'#{pipeline_name}' is not a valid BulkImport Pipeline"
+ unless entity.pipeline_exists?(pipeline_name)
+ raise BulkImports::Error, "'#{pipeline_name}' is not a valid BulkImport Pipeline"
end
pipeline_name.constantize
diff --git a/app/models/ci/application_record.rb b/app/models/ci/application_record.rb
index 9d4a8f0648e..913e7a62c66 100644
--- a/app/models/ci/application_record.rb
+++ b/app/models/ci/application_record.rb
@@ -2,6 +2,7 @@
module Ci
class ApplicationRecord < ::ApplicationRecord
+ self.gitlab_schema = :gitlab_ci
self.abstract_class = true
def self.table_name_prefix
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 577bca282ef..97fb8233d34 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -28,10 +28,10 @@ module Ci
state_machine :status do
after_transition [:created, :manual, :waiting_for_resource] => :pending do |bridge|
- next unless bridge.downstream_project
+ next unless bridge.triggers_downstream_pipeline?
bridge.run_after_commit do
- bridge.schedule_downstream_pipeline!
+ ::Ci::CreateCrossProjectPipelineWorker.perform_async(bridge.id)
end
end
@@ -64,12 +64,6 @@ module Ci
)
end
- def schedule_downstream_pipeline!
- raise InvalidBridgeTypeError unless downstream_project
-
- ::Ci::CreateCrossProjectPipelineWorker.perform_async(self.id)
- end
-
def inherit_status_from_downstream!(pipeline)
case pipeline.status
when 'success'
@@ -112,10 +106,18 @@ module Ci
pipeline if triggers_child_pipeline?
end
+ def triggers_downstream_pipeline?
+ triggers_child_pipeline? || triggers_cross_project_pipeline?
+ end
+
def triggers_child_pipeline?
yaml_for_downstream.present?
end
+ def triggers_cross_project_pipeline?
+ downstream_project_path.present?
+ end
+
def tags
[:bridge]
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1ca291a659b..e2e24247679 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -90,6 +90,10 @@ module Ci
end
end
+ def persisted_environment=(environment)
+ strong_memoize(:persisted_environment) { environment }
+ end
+
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
@@ -166,8 +170,6 @@ module Ci
scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
- scope :with_secure_reports_from_options, -> (job_type) { where('options like :job_type', job_type: "%:artifacts:%:reports:%:#{job_type}:%") }
-
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)
end
@@ -306,7 +308,9 @@ module Ci
end
after_transition pending: :running do |build|
- build.deployment&.run
+ Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
+ build.deployment&.run
+ end
build.run_after_commit do
build.pipeline.persistent_ref.create
@@ -328,7 +332,9 @@ module Ci
end
after_transition any => [:success] do |build|
- build.deployment&.succeed
+ Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
+ build.deployment&.succeed
+ end
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
@@ -341,7 +347,9 @@ module Ci
next unless build.deployment
begin
- build.deployment.drop!
+ 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
@@ -362,10 +370,12 @@ module Ci
end
after_transition any => [:skipped, :canceled] do |build, transition|
- if transition.to_name == :skipped
- build.deployment&.skip
- else
- build.deployment&.cancel
+ 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
@@ -712,6 +722,10 @@ module Ci
update_column(:trace, nil)
end
+ def ensure_trace_metadata!
+ Ci::BuildTraceMetadata.find_or_upsert_for!(id)
+ end
+
def artifacts_expose_as
options.dig(:artifacts, :expose_as)
end
@@ -748,7 +762,9 @@ module Ci
def any_runners_available?
cache_for_available_runners do
- project.active_runners.exists?
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do
+ project.active_runners.exists?
+ end
end
end
@@ -1013,9 +1029,10 @@ module Ci
# Consider this object to have a structural integrity problems
def doom!
- update_columns(
- status: :failed,
- failure_reason: :data_integrity_failure)
+ transaction do
+ update_columns(status: :failed, failure_reason: :data_integrity_failure)
+ all_queuing_entries.delete_all
+ end
end
def degradation_threshold
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
index 3bfac2b33c0..1cae2279434 100644
--- a/app/models/ci/build_trace_chunks/fog.rb
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -80,12 +80,10 @@ module Ci
private
def append_strings(old_data, new_data)
- if Feature.enabled?(:ci_job_trace_force_encode, default_enabled: :yaml)
- # When object storage is in use, old_data may be retrieved in UTF-8.
- old_data = old_data.force_encoding(Encoding::ASCII_8BIT)
- # new_data should already be in ASCII-8BIT, but just in case it isn't, do this.
- new_data = new_data.force_encoding(Encoding::ASCII_8BIT)
- end
+ # When object storage is in use, old_data may be retrieved in UTF-8.
+ old_data = old_data.force_encoding(Encoding::ASCII_8BIT)
+ # new_data should already be in ASCII-8BIT, but just in case it isn't, do this.
+ new_data = new_data.force_encoding(Encoding::ASCII_8BIT)
old_data + new_data
end
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index 05bdb3d8b7b..901b84ceec6 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -2,6 +2,7 @@
module Ci
class BuildTraceMetadata < Ci::ApplicationRecord
+ MAX_ATTEMPTS = 5
self.table_name = 'ci_build_trace_metadata'
self.primary_key = :build_id
@@ -9,5 +10,49 @@ module Ci
belongs_to :trace_artifact, class_name: 'Ci::JobArtifact'
validates :build, presence: true
+ validates :archival_attempts, presence: true
+
+ def self.find_or_upsert_for!(build_id)
+ record = find_by(build_id: build_id)
+ return record if record
+
+ upsert({ build_id: build_id }, unique_by: :build_id)
+ find_by!(build_id: build_id)
+ end
+
+ # The job is retried around 5 times during the 7 days retention period for
+ # trace chunks as defined in `Ci::BuildTraceChunks::RedisBase::CHUNK_REDIS_TTL`
+ def can_attempt_archival_now?
+ return false unless archival_attempts_available?
+ return true unless last_archival_attempt_at
+
+ last_archival_attempt_at + backoff < Time.current
+ end
+
+ def archival_attempts_available?
+ archival_attempts <= MAX_ATTEMPTS
+ end
+
+ def increment_archival_attempts!
+ increment!(:archival_attempts, touch: :last_archival_attempt_at)
+ end
+
+ def track_archival!(trace_artifact_id)
+ update!(trace_artifact_id: trace_artifact_id, archived_at: Time.current)
+ end
+
+ def archival_attempts_message
+ if archival_attempts_available?
+ 'The job can not be archived right now.'
+ else
+ 'The job is out of archival attempts.'
+ end
+ end
+
+ private
+
+ def backoff
+ ::Gitlab::Ci::Trace::Backoff.new(archival_attempts).value_with_jitter
+ end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 1f0da4345f2..ad3e867f9d5 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -10,6 +10,9 @@ 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'
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
@@ -182,7 +185,6 @@ module Ci
scope :order_expired_desc, -> { order(expire_at: :desc) }
scope :with_destroy_preloads, -> { includes(project: [:route, :statistics]) }
- scope :scoped_project, -> { where('ci_job_artifacts.project_id = projects.id') }
scope :for_project, ->(project) { where(project_id: project) }
scope :created_in_time_range, ->(from: nil, to: nil) { where(created_at: from..to) }
@@ -232,6 +234,17 @@ module Ci
hashed_path: 2
}
+ # `locked` will be populated from the source of truth on Ci::Pipeline
+ # in order to clean up expired job artifacts in a performant way.
+ # The values should be the same as `Ci::Pipeline.lockeds` with the
+ # additional value of `unknown` to indicate rows that have not
+ # yet been populated from the parent Ci::Pipeline
+ enum locked: {
+ unlocked: 0,
+ artifacts_locked: 1,
+ unknown: 2
+ }, _prefix: :artifact
+
def validate_file_format!
unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym
errors.add(:base, _('Invalid file format with specified file type'))
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 7cf3a387516..ccad6290fac 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -2,6 +2,8 @@
module Ci
class PendingBuild < Ci::ApplicationRecord
+ include EachBatch
+
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :namespace, inverse_of: :pending_builds, class_name: 'Namespace'
@@ -11,52 +13,62 @@ module Ci
scope :ref_protected, -> { where(protected: true) }
scope :queued_before, ->(time) { where(arel_table[:created_at].lt(time)) }
scope :with_instance_runners, -> { where(instance_runners_enabled: true) }
+ scope :for_tags, ->(tag_ids) do
+ if tag_ids.present?
+ where('ci_pending_builds.tag_ids <@ ARRAY[?]::int[]', Array.wrap(tag_ids))
+ else
+ where("ci_pending_builds.tag_ids = '{}'")
+ end
+ end
- def self.upsert_from_build!(build)
- entry = self.new(args_from_build(build))
+ class << self
+ def upsert_from_build!(build)
+ entry = self.new(args_from_build(build))
- entry.validate!
+ entry.validate!
- self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
- end
+ self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
+ end
- def self.args_from_build(build)
- args = {
- build: build,
- project: build.project,
- protected: build.protected?,
- namespace: build.project.namespace
- }
+ private
+
+ def args_from_build(build)
+ project = build.project
+
+ args = {
+ build: build,
+ project: project,
+ protected: build.protected?,
+ namespace: project.namespace
+ }
+
+ if Feature.enabled?(:ci_pending_builds_maintain_tags_data, type: :development, default_enabled: :yaml)
+ args.store(:tag_ids, build.tags_ids)
+ end
+
+ if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
+ args.store(:instance_runners_enabled, shared_runners_enabled?(project))
+ end
+
+ if Feature.enabled?(:ci_pending_builds_maintain_namespace_traversal_ids, type: :development, default_enabled: :yaml)
+ args.store(:namespace_traversal_ids, project.namespace.traversal_ids) if group_runners_enabled?(project)
+ end
- if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
- args.merge(instance_runners_enabled: shareable?(build))
- else
args
end
- end
- private_class_method :args_from_build
-
- def self.shareable?(build)
- shared_runner_enabled?(build) &&
- builds_access_level?(build) &&
- project_not_removed?(build)
- end
- private_class_method :shareable?
- def self.shared_runner_enabled?(build)
- build.project.shared_runners.exists?
- end
- private_class_method :shared_runner_enabled?
+ def shared_runners_enabled?(project)
+ builds_enabled?(project) && project.shared_runners_enabled?
+ end
- def self.project_not_removed?(build)
- !build.project.pending_delete?
- end
- private_class_method :project_not_removed?
+ def group_runners_enabled?(project)
+ builds_enabled?(project) && project.group_runners_enabled?
+ end
- def self.builds_access_level?(build)
- build.project.project_feature.builds_access_level.nil? || build.project.project_feature.builds_access_level > 0
+ def builds_enabled?(project)
+ project.builds_enabled? && !project.pending_delete?
+ end
end
- private_class_method :builds_access_level?
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 70e67953e31..1a0cec3c935 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -66,6 +66,7 @@ module Ci
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :generic_commit_statuses, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'GenericCommitStatus'
has_many :job_artifacts, through: :builds
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
@@ -307,6 +308,7 @@ module Ci
scope :ci_and_parent_sources, -> { where(source: Enums::Ci::Pipeline.ci_and_parent_sources.values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
+ scope :where_not_sha, -> (sha) { where.not(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :for_ref, -> (ref) { where(ref: ref) }
@@ -317,7 +319,6 @@ module Ci
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
- scope :eager_load_project, -> { eager_load(project: [:route, { namespace: :route }]) }
scope :with_pipeline_source, -> (source) { where(source: source)}
scope :outside_pipeline_family, ->(pipeline) do
@@ -588,13 +589,11 @@ module Ci
end
def cancel_running(retries: 1)
- commit_status_relations = [:project, :pipeline]
- ci_build_relations = [:deployment, :taggings]
+ preloaded_relations = [:project, :pipeline, :deployment, :taggings]
retry_lock(cancelable_statuses, retries, name: 'ci_pipeline_cancel_running') do |cancelables|
cancelables.find_in_batches do |batch|
- ActiveRecord::Associations::Preloader.new.preload(batch, commit_status_relations)
- ActiveRecord::Associations::Preloader.new.preload(batch.select { |job| job.is_a?(Ci::Build) }, ci_build_relations)
+ Preloaders::CommitStatusPreloader.new(batch).execute(preloaded_relations)
batch.each do |job|
yield(job) if block_given?
@@ -1108,7 +1107,7 @@ module Ci
merge_request.modified_paths
elsif branch_updated?
push_details.modified_paths
- elsif external_pull_request? && ::Feature.enabled?(:ci_modified_paths_of_external_prs, project, default_enabled: :yaml)
+ elsif external_pull_request?
external_pull_request.modified_paths
end
end
@@ -1220,24 +1219,12 @@ module Ci
self.ci_ref = Ci::Ref.ensure_for(self)
end
- # We need `base_and_ancestors` in a specific order to "break" when needed.
- # If we use `find_each`, then the order is broken.
- # rubocop:disable Rails/FindEach
def reset_source_bridge!(current_user)
- if ::Feature.enabled?(:ci_reset_bridge_with_subsequent_jobs, project, default_enabled: :yaml)
- return unless bridge_waiting?
+ return unless bridge_waiting?
- source_bridge.pending!
- Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass
- else
- self_and_upstreams.includes(:source_bridge).each do |pipeline|
- break unless pipeline.bridge_waiting?
-
- pipeline.source_bridge.pending!
- end
- end
+ source_bridge.pending!
+ Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass
end
- # rubocop:enable Rails/FindEach
# EE-only
def merge_train_pipeline?
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index a0e8886414b..3dca77af051 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -8,7 +8,7 @@ module Ci
alias_attribute :secret_value, :value
- validates :key, uniqueness: { scope: :pipeline_id }
+ validates :key, presence: true
def hook_attrs
{ key: key, value: value }
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 432c3a408a9..4aa232ad26b 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -208,16 +208,18 @@ module Ci
Arel.sql("(#{arel_tag_names_array.to_sql})")
]
- group(*unique_params).pluck('array_agg(ci_runners.id)', *unique_params).map do |values|
- Gitlab::Ci::Matching::RunnerMatcher.new({
- runner_ids: values[0],
- runner_type: values[1],
- public_projects_minutes_cost_factor: values[2],
- private_projects_minutes_cost_factor: values[3],
- run_untagged: values[4],
- access_level: values[5],
- tag_list: values[6]
- })
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339621') do
+ group(*unique_params).pluck('array_agg(ci_runners.id)', *unique_params).map do |values|
+ Gitlab::Ci::Matching::RunnerMatcher.new({
+ runner_ids: values[0],
+ runner_type: values[1],
+ public_projects_minutes_cost_factor: values[2],
+ private_projects_minutes_cost_factor: values[3],
+ run_untagged: values[4],
+ access_level: values[5],
+ tag_list: values[6]
+ })
+ end
end
end
@@ -385,6 +387,12 @@ module Ci
read_attribute(:contacted_at)
end
+ def namespace_ids
+ strong_memoize(:namespace_ids) do
+ runner_namespaces.pluck(:namespace_id).compact
+ end
+ end
+
private
def cleanup_runner_queue
@@ -420,14 +428,18 @@ module Ci
end
def no_projects
- if projects.any?
- errors.add(:runner, 'cannot have projects assigned')
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
+ if projects.any?
+ errors.add(:runner, 'cannot have projects assigned')
+ end
end
end
def no_groups
- if groups.any?
- errors.add(:runner, 'cannot have groups assigned')
+ ::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
end
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index f78caf710a6..95842d944f9 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -4,6 +4,9 @@ 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/clusters/agent.rb b/app/models/clusters/agent.rb
index 9fb8cd024c5..cf6d95fc6df 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -10,6 +10,12 @@ module Clusters
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
+ has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization'
+ has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group
+
+ has_many :project_authorizations, class_name: 'Clusters::Agents::ProjectAuthorization'
+ has_many :authorized_projects, class_name: '::Project', through: :project_authorizations, source: :project
+
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb
new file mode 100644
index 00000000000..74c0cec3b7e
--- /dev/null
+++ b/app/models/clusters/agents/group_authorization.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class GroupAuthorization < ApplicationRecord
+ self.table_name = 'agent_group_authorizations'
+
+ belongs_to :agent, class_name: 'Clusters::Agent', optional: false
+ belongs_to :group, class_name: '::Group', optional: false
+
+ validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
+
+ delegate :project, to: :agent
+ end
+ end
+end
diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb
new file mode 100644
index 00000000000..967cc686045
--- /dev/null
+++ b/app/models/clusters/agents/implicit_authorization.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class ImplicitAuthorization
+ attr_reader :agent
+
+ delegate :id, to: :agent, prefix: true
+ delegate :project, to: :agent
+
+ def initialize(agent:)
+ @agent = agent
+ end
+
+ def config
+ nil
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb
new file mode 100644
index 00000000000..1c71a0a432a
--- /dev/null
+++ b/app/models/clusters/agents/project_authorization.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class ProjectAuthorization < ApplicationRecord
+ self.table_name = 'agent_project_authorizations'
+
+ belongs_to :agent, class_name: 'Clusters::Agent', optional: false
+ belongs_to :project, class_name: '::Project', optional: false
+
+ validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 2fff0a69a26..feac7bbc363 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -148,6 +148,7 @@ module Clusters
scope :with_management_project, -> { where.not(management_project: nil) }
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
diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb
index 162a1a3290d..9435d258d67 100644
--- a/app/models/clusters/clusters_hierarchy.rb
+++ b/app/models/clusters/clusters_hierarchy.rb
@@ -83,7 +83,7 @@ module Clusters
project_id: clusterable.id
}
- model.sanitize_sql_array([Arel.sql(order), values])
+ Arel.sql(model.sanitize_sql_array([Arel.sql(order), values]))
end
def group_clusters_base_query
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 7f5f87e3e36..7ec614b048c 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -137,6 +137,14 @@ module Clusters
kubeclient.patch_ingress(ingress.name, data, namespace)
end
+ def kubeconfig(namespace)
+ to_kubeconfig(
+ url: api_url,
+ namespace: namespace,
+ token: token,
+ ca_pem: ca_pem)
+ end
+
private
def default_namespace(project, environment_name:)
@@ -154,14 +162,6 @@ module Clusters
).execute
end
- def kubeconfig(namespace)
- to_kubeconfig(
- url: api_url,
- namespace: namespace,
- token: token,
- ca_pem: ca_pem)
- end
-
def read_pods(namespace)
kubeclient.get_pods(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index b34d64de101..8cba3d04502 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -56,15 +56,19 @@ class CommitStatus < Ci::ApplicationRecord
scope :for_ref, -> (ref) { where(ref: ref) }
scope :by_name, -> (name) { where(name: name) }
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
- scope :eager_load_pipeline, -> { eager_load(:pipeline, project: { namespace: :route }) }
scope :with_pipeline, -> { joins(:pipeline) }
- scope :updated_at_before, ->(date) { where('updated_at < ?', date) }
+ scope :updated_at_before, ->(date) { where('ci_builds.updated_at < ?', date) }
+ scope :created_at_before, ->(date) { where('ci_builds.created_at < ?', date) }
scope :updated_before, ->(lookback:, timeout:) {
where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
}
+ # The scope applies `pluck` to split the queries. Use with care.
scope :for_project_paths, -> (paths) do
- where(project: Project.where_full_path_in(Array(paths)))
+ # Pluck is used to split this query. Splitting the query is required for database decomposition for `ci_*` tables.
+ # https://docs.gitlab.com/ee/development/database/transaction_guidelines.html#database-decomposition-and-sharding
+ project_ids = Project.where_full_path_in(Array(paths)).pluck(:id)
+ where(project: project_ids)
end
scope :with_preloads, -> do
diff --git a/app/models/compare.rb b/app/models/compare.rb
index 2eaaf98c260..f1b0bf19c11 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -25,6 +25,16 @@ class Compare
@straight = straight
end
+ # Return a Hash of parameters for passing to a URL helper
+ #
+ # See `namespace_project_compare_url`
+ def to_param
+ {
+ from: @straight ? start_commit_sha : base_commit_sha,
+ to: head_commit_sha
+ }
+ end
+
def cache_key
[@project, :compare, diff_refs.hash]
end
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index ef7ba7b1089..8240f9bd6ea 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -54,4 +54,8 @@ module ApprovableBase
def can_be_approved_by?(user)
user && !approved_by?(user) && user.can?(:approve_merge_request, self)
end
+
+ def can_be_unapproved_by?(user)
+ user && approved_by?(user) && user.can?(:approve_merge_request, self)
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 44d9beff27e..9414d16beef 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -160,39 +160,6 @@ module CacheMarkdownField
# We can only store mentions if the mentionable is a database object
return unless self.is_a?(ApplicationRecord)
- return store_mentions_without_subtransaction! if Feature.enabled?(:store_mentions_without_subtransaction, default_enabled: :yaml)
-
- refs = all_references(self.author)
-
- references = {}
- references[:mentioned_users_ids] = refs.mentioned_user_ids.presence
- references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
- references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence
-
- # One retry is enough as next time `model_user_mention` should return the existing mention record,
- # that threw the `ActiveRecord::RecordNotUnique` exception in first place.
- self.class.safe_ensure_unique(retries: 1) do
- user_mention = model_user_mention
-
- # this may happen due to notes polymorphism, so noteable_id may point to a record
- # that no longer exists as we cannot have FK on noteable_id
- break if user_mention.blank?
-
- user_mention.mentioned_users_ids = references[:mentioned_users_ids]
- user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
- user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]
-
- if user_mention.has_mentions?
- user_mention.save!
- else
- user_mention.destroy!
- end
- end
-
- true
- end
-
- def store_mentions_without_subtransaction!
identifier = user_mention_identifier
# this may happen due to notes polymorphism, so noteable_id may point to a record
diff --git a/app/models/concerns/calloutable.rb b/app/models/concerns/calloutable.rb
new file mode 100644
index 00000000000..8b9cfae6a32
--- /dev/null
+++ b/app/models/concerns/calloutable.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Calloutable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :user
+
+ validates :user, presence: true
+ end
+
+ def dismissed_after?(dismissed_after)
+ dismissed_at > dismissed_after
+ end
+end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index bdba2d3e251..27a704c1de0 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -33,13 +33,13 @@ module Ci
#
def simple_variables
strong_memoize(:simple_variables) do
- scoped_variables(environment: nil).to_runner_variables
+ scoped_variables(environment: nil)
end
end
def simple_variables_without_dependencies
strong_memoize(:variables_without_dependencies) do
- scoped_variables(environment: nil, dependencies: false).to_runner_variables
+ scoped_variables(environment: nil, dependencies: false)
end
end
diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb
index 48605ecc3d7..d5b86db2640 100644
--- a/app/models/concerns/cron_schedulable.rb
+++ b/app/models/concerns/cron_schedulable.rb
@@ -14,12 +14,10 @@ module CronSchedulable
# The `next_run_at` column is set to the actual execution date of worker that
# triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
# in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
- def calculate_next_run_at
- now = Time.zone.now
+ def calculate_next_run_at(start_time = Time.zone.now)
+ ideal_next_run = ideal_next_run_from(start_time)
- ideal_next_run = ideal_next_run_from(now)
-
- if ideal_next_run == cron_worker_next_run_from(now)
+ if ideal_next_run == cron_worker_next_run_from(start_time)
ideal_next_run
else
cron_worker_next_run_from(ideal_next_run)
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 16dec5fb081..7f46e44697e 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -26,6 +26,7 @@ module Enums
pipeline_loop_detected: 17,
no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data
trace_size_exceeded: 19,
+ builds_disabled: 20,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb
index ed9bce87da1..70d67fc7559 100644
--- a/app/models/concerns/featurable.rb
+++ b/app/models/concerns/featurable.rb
@@ -83,6 +83,10 @@ module Featurable
end
end
+ included do
+ validate :allowed_access_levels
+ end
+
def access_level(feature)
public_send(self.class.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -94,4 +98,21 @@ module Featurable
def string_access_level(feature)
self.class.str_from_access_level(access_level(feature))
end
+
+ private
+
+ def allowed_access_levels
+ validator = lambda do |field|
+ level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
+ not_allowed = level > ENABLED
+ self.errors.add(field, "cannot have public visibility level") if not_allowed
+ end
+
+ (self.class.available_features - feature_validation_exclusion).each {|f| validator.call("#{f}_access_level")}
+ end
+
+ # Features that we should exclude from the validation
+ def feature_validation_exclusion
+ []
+ end
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 1b4c590694a..9218ba47d20 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -122,4 +122,8 @@ module HasRepository
def after_repository_change_head
reload_default_branch
end
+
+ def after_change_head_branch_does_not_exist(branch)
+ # No-op (by default)
+ end
end
diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb
index e9aaaac8226..1709b56080e 100644
--- a/app/models/concerns/integrations/has_data_fields.rb
+++ b/app/models/concerns/integrations/has_data_fields.rb
@@ -46,6 +46,7 @@ module Integrations
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData'
has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData'
+ has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'
def data_fields
raise NotImplementedError
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 8d0f8b01d64..5c307158a9a 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -26,6 +26,7 @@ module Issuable
include UpdatedAtFilterable
include ClosedAtFilterable
include VersionedDescription
+ include SortableTitle
TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800
@@ -116,20 +117,6 @@ module Issuable
end
# rubocop:enable GitlabSecurity/SqlInjection
- scope :without_particular_labels, ->(label_names) do
- labels_table = Label.arel_table
- label_links_table = LabelLink.arel_table
- issuables_table = klass.arel_table
- inner_query = label_links_table.project('true')
- .join(labels_table, Arel::Nodes::InnerJoin).on(labels_table[:id].eq(label_links_table[:label_id]))
- .where(label_links_table[:target_type].eq(name)
- .and(label_links_table[:target_id].eq(issuables_table[:id]))
- .and(labels_table[:title].in(label_names)))
- .exists.not
-
- where(inner_query)
- end
-
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) }
scope :join_project, -> { joins(:project) }
@@ -293,6 +280,8 @@ module Issuable
when 'popularity', 'popularity_desc', 'upvotes_desc' then order_upvotes_desc
when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels)
+ when 'title_asc' then order_title_asc.with_order_id_desc
+ when 'title_desc' then order_title_desc.with_order_id_desc
else order_by(method)
end
diff --git a/app/models/concerns/loose_foreign_key.rb b/app/models/concerns/loose_foreign_key.rb
new file mode 100644
index 00000000000..4e822a04869
--- /dev/null
+++ b/app/models/concerns/loose_foreign_key.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module LooseForeignKey
+ extend ActiveSupport::Concern
+
+ # This concern adds loose foreign key support to ActiveRecord models.
+ # 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)
+ #
+ # Usage:
+ #
+ # > class Ci::Build < ApplicationRecord
+ # >
+ # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
+ # >
+ # > # associations can be still defined, the dependent options is no longer necessary:
+ # > has_many :security_scans, class_name: 'Security::Scan'
+ # >
+ # > end
+ #
+ # Options for on_delete:
+ #
+ # - :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
+ # which tracks the record deletions (stores primary key value of the deleted row) in
+ # a database table.
+ #
+ # These deletion records are processed asynchronously and records are cleaned up
+ # according to the loose foreign key definitions described in the model.
+ #
+ # The cleanup happens in batches, which reduces the likelyhood of statement timeouts.
+ #
+ # When all associations related to the deleted record are cleaned up, the record itself
+ # is deleted.
+ included do
+ class_attribute :loose_foreign_key_definitions, default: []
+ end
+
+ class_methods do
+ def loose_foreign_key(to_table, column, options)
+ symbolized_options = options.symbolize_keys
+
+ unless base_class?
+ raise <<~MSG
+ loose_foreign_key can be only used on base classes, inherited classes are not supported.
+ Please define the loose_foreign_key on the #{base_class.name} class.
+ MSG
+ 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
+ }
+ )
+
+ self.loose_foreign_key_definitions += [definition]
+ end
+ end
+end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 4df9e32d8ec..a0ea5ac8012 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -217,17 +217,6 @@ module Mentionable
def user_mention_association
association(:user_mentions).reflection
end
-
- # User mention that is parsed from model description rather then its related notes.
- # Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
- # Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
- # a description attribute.
- #
- # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
- # in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block.
- def model_user_mention
- user_mentions.where(note_id: nil).first_or_initialize
- end
end
Mentionable.prepend_mod_with('Mentionable')
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
deleted file mode 100644
index 19d2ac620f3..00000000000
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-module OptimizedIssuableLabelFilter
- extend ActiveSupport::Concern
-
- prepended do
- extend Gitlab::Cache::RequestCache
-
- # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
- request_cache(:find_label_ids) { [root_namespace.id, params.label_names] }
- end
-
- def by_label(items)
- return items unless params.labels?
-
- return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
-
- target_model = items.model
-
- if params.filter_by_no_label?
- items.where('NOT EXISTS (?)', optimized_any_label_query(target_model))
- elsif params.filter_by_any_label?
- items.where('EXISTS (?)', optimized_any_label_query(target_model))
- else
- issuables_with_selected_labels(items, target_model)
- end
- end
-
- # Taken from IssuableFinder
- def count_by_state
- return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
-
- count_params = params.merge(state: nil, sort: nil, force_cte: true)
- finder = self.class.new(current_user, count_params)
-
- state_counts = finder
- .execute
- .reorder(nil)
- .group(:state_id)
- .count
-
- counts = Hash.new(0)
-
- state_counts.each do |key, value|
- counts[count_key(key)] += value
- end
-
- counts[:all] = counts.values.sum
- counts.with_indifferent_access
- end
-
- private
-
- def issuables_with_selected_labels(items, target_model)
- if root_namespace
- all_label_ids = find_label_ids
- # Found less labels in the DB than we were searching for. Return nothing.
- return items.none if all_label_ids.size != params.label_names.size
-
- all_label_ids.each do |label_ids|
- items = items.where('EXISTS (?)', optimized_label_query_by_label_ids(target_model, label_ids))
- end
- else
- params.label_names.each do |label_name|
- items = items.where('EXISTS (?)', optimized_label_query_by_label_name(target_model, label_name))
- end
- end
-
- items
- end
-
- def find_label_ids
- group_labels = Label
- .where(project_id: nil)
- .where(title: params.label_names)
- .where(group_id: root_namespace.self_and_descendants.select(:id))
-
- project_labels = Label
- .where(group_id: nil)
- .where(title: params.label_names)
- .where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendants.select(:id)))
-
- Label
- .from_union([group_labels, project_labels], remove_duplicates: false)
- .reorder(nil)
- .pluck(:title, :id)
- .group_by(&:first)
- .values
- .map { |labels| labels.map(&:last) }
- end
-
- def root_namespace
- strong_memoize(:root_namespace) do
- (params.project || params.group)&.root_ancestor
- end
- end
-
- def optimized_any_label_query(target_model)
- LabelLink
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
- .limit(1)
- end
-
- def optimized_label_query_by_label_ids(target_model, label_ids)
- LabelLink
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
- .where(label_id: label_ids)
- .limit(1)
- end
-
- def optimized_label_query_by_label_name(target_model, label_name)
- LabelLink
- .joins(:label)
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
- .where(labels: { name: label_name })
- .limit(1)
- end
-end
diff --git a/app/models/concerns/partitioned_table.rb b/app/models/concerns/partitioned_table.rb
index eab5d4c35bb..23d2d00b346 100644
--- a/app/models/concerns/partitioned_table.rb
+++ b/app/models/concerns/partitioned_table.rb
@@ -14,8 +14,6 @@ module PartitionedTable
strategy_class = PARTITIONING_STRATEGIES[strategy.to_sym] || raise(ArgumentError, "Unknown partitioning strategy: #{strategy}")
@partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs)
-
- Gitlab::Database::Partitioning::PartitionManager.register(self)
end
end
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 75dfed6d58f..c32e499c329 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -135,21 +135,21 @@ module RelativePositioning
before, after = [before, after].sort_by(&:relative_position) if before && after
RelativePositioning.mover.move(self, before, after)
- rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ rescue NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_after(before = self)
RelativePositioning.mover.move(self, before, nil)
- rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ rescue NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_before(after = self)
RelativePositioning.mover.move(self, nil, after)
- rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ rescue NoSpaceLeft => e
could_not_move(e)
raise e
end
@@ -159,9 +159,6 @@ module RelativePositioning
rescue NoSpaceLeft => e
could_not_move(e)
self.relative_position = MAX_POSITION
- rescue ActiveRecord::QueryCanceled => e
- could_not_move(e)
- raise e
end
def move_to_start
@@ -169,9 +166,6 @@ module RelativePositioning
rescue NoSpaceLeft => e
could_not_move(e)
self.relative_position = MIN_POSITION
- rescue ActiveRecord::QueryCanceled => e
- could_not_move(e)
- raise e
end
# This method is used during rebalancing - override it to customise the update
diff --git a/app/models/concerns/sanitizable.rb b/app/models/concerns/sanitizable.rb
new file mode 100644
index 00000000000..05756beb404
--- /dev/null
+++ b/app/models/concerns/sanitizable.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+# == Sanitizable concern
+#
+# This concern adds HTML sanitization and validation to models. The intention is
+# to help prevent XSS attacks in the event of a by-pass in the frontend
+# sanitizer due to a configuration issue or a vulnerability in the sanitizer.
+# This approach is commonly referred to as defense-in-depth.
+#
+# Example:
+#
+# module Dast
+# class Profile < ApplicationRecord
+# include Sanitizable
+#
+# sanitizes! :name, :description
+
+module Sanitizable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def sanitize(input)
+ return unless input
+
+ # We return the input unchanged to avoid escaping pre-escaped HTML fragments.
+ # Please see gitlab-org/gitlab#293634 for an example.
+ return input unless input == CGI.unescapeHTML(input.to_s)
+
+ CGI.unescapeHTML(Sanitize.fragment(input))
+ end
+
+ def sanitizes!(*attrs)
+ instance_eval do
+ before_validation do
+ attrs.each do |attr|
+ input = public_send(attr) # rubocop: disable GitlabSecurity/PublicSend
+
+ public_send("#{attr}=", self.class.sanitize(input)) # rubocop: disable GitlabSecurity/PublicSend
+ end
+ end
+
+ validates_each(*attrs) do |record, attr, input|
+ # We reject pre-escaped HTML fragments as invalid to avoid saving them
+ # to the database.
+ unless input.to_s == CGI.unescapeHTML(input.to_s)
+ record.errors.add(attr, 'cannot contain escaped HTML entities')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/sortable_title.rb b/app/models/concerns/sortable_title.rb
new file mode 100644
index 00000000000..7c5cad17f4c
--- /dev/null
+++ b/app/models/concerns/sortable_title.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module SortableTitle
+ extend ActiveSupport::Concern
+
+ included do
+ scope :order_title_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
+ scope :order_title_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:title].lower)) }
+ end
+
+ class_methods do
+ def simple_sorts
+ super.merge(
+ {
+ 'title_asc' => -> { order_title_asc },
+ 'title_desc' => -> { order_title_desc }
+ }
+ )
+ end
+ end
+end
diff --git a/app/models/concerns/taggable_queries.rb b/app/models/concerns/taggable_queries.rb
index cba2e93a86d..06799f0a9f4 100644
--- a/app/models/concerns/taggable_queries.rb
+++ b/app/models/concerns/taggable_queries.rb
@@ -3,6 +3,10 @@
module TaggableQueries
extend ActiveSupport::Concern
+ MAX_TAGS_IDS = 50
+
+ TooManyTagsError = Class.new(StandardError)
+
class_methods do
# context is a name `acts_as_taggable context`
def arel_tag_names_array(context = :tags)
@@ -34,4 +38,10 @@ module TaggableQueries
where("EXISTS (?)", matcher)
end
end
+
+ def tags_ids
+ tags.limit(MAX_TAGS_IDS).order('id ASC').pluck(:id).tap do |ids|
+ raise TooManyTagsError if ids.size >= MAX_TAGS_IDS
+ end
+ end
end
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
new file mode 100644
index 00000000000..aaa7e2ae175
--- /dev/null
+++ b/app/models/customer_relations/contact.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class CustomerRelations::Contact < ApplicationRecord
+ include StripAttribute
+
+ self.table_name = "customer_relations_contacts"
+
+ belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id'
+ belongs_to :organization, optional: true
+
+ strip_attributes! :phone, :first_name, :last_name
+
+ enum state: {
+ inactive: 0,
+ active: 1
+ }
+
+ validates :group, presence: true
+ validates :phone, length: { maximum: 32 }
+ validates :first_name, presence: true, length: { maximum: 255 }
+ validates :last_name, presence: true, length: { maximum: 255 }
+ validates :email, length: { maximum: 255 }
+ validates :description, length: { maximum: 1024 }
+ validate :validate_email_format
+
+ private
+
+ def validate_email_format
+ return unless email
+
+ self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
+ end
+end
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index caf1cd68cc5..a18d3ab8148 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
class CustomerRelations::Organization < ApplicationRecord
+ include StripAttribute
+
self.table_name = "customer_relations_organizations"
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id'
- before_validation :strip_whitespace!
+ strip_attributes! :name
enum state: {
inactive: 0,
@@ -22,10 +24,4 @@ class CustomerRelations::Organization < ApplicationRecord
where(group: group_id)
.where('LOWER(name) = LOWER(?)', name)
end
-
- private
-
- def strip_whitespace!
- name&.strip!
- end
end
diff --git a/app/models/dependency_proxy/blob.rb b/app/models/dependency_proxy/blob.rb
index 3a81112340a..5de6b1cf28f 100644
--- a/app/models/dependency_proxy/blob.rb
+++ b/app/models/dependency_proxy/blob.rb
@@ -8,6 +8,9 @@ class DependencyProxy::Blob < ApplicationRecord
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
+ validates :status, presence: true
+
+ enum status: { default: 0, expired: 1 }
mount_file_store_uploader DependencyProxy::FileUploader
diff --git a/app/models/dependency_proxy/image_ttl_group_policy.rb b/app/models/dependency_proxy/image_ttl_group_policy.rb
new file mode 100644
index 00000000000..5a1b8cb8f1f
--- /dev/null
+++ b/app/models/dependency_proxy/image_ttl_group_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class DependencyProxy::ImageTtlGroupPolicy < ApplicationRecord
+ self.primary_key = :group_id
+
+ belongs_to :group
+
+ validates :group, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+ validates :ttl, numericality: { greater_than: 0 }, allow_nil: true
+end
diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb
index d613d5708f0..15e5137b50a 100644
--- a/app/models/dependency_proxy/manifest.rb
+++ b/app/models/dependency_proxy/manifest.rb
@@ -9,6 +9,9 @@ class DependencyProxy::Manifest < ApplicationRecord
validates :file, presence: true
validates :file_name, presence: true
validates :digest, presence: true
+ validates :status, presence: true
+
+ enum status: { default: 0, expired: 1 }
mount_file_store_uploader DependencyProxy::FileUploader
diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb
index 40c66d5bc4c..363ef0b1c9a 100644
--- a/app/models/deploy_keys_project.rb
+++ b/app/models/deploy_keys_project.rb
@@ -3,7 +3,6 @@
class DeployKeysProject < ApplicationRecord
belongs_to :project, inverse_of: :deploy_keys_projects
belongs_to :deploy_key, inverse_of: :deploy_keys_projects
- scope :without_project_deleted, -> { joins(:project).where(projects: { pending_delete: false }) }
scope :in_project, ->(project) { where(project: project) }
scope :with_write_access, -> { where(can_push: true) }
diff --git a/app/models/design_management/action.rb b/app/models/design_management/action.rb
index ecd7973a523..b9df2873a73 100644
--- a/app/models/design_management/action.rb
+++ b/app/models/design_management/action.rb
@@ -17,6 +17,8 @@ module DesignManagement
# we assume sequential ordering.
scope :ordered, -> { order(version_id: :asc) }
+ scope :by_design, -> (design) { where(design: design) }
+ scope :by_event, -> (event) { where(event: event) }
# For each design, only select the most recent action
scope :most_recent, -> do
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c8a0773cc5b..6ebac6384bc 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -22,7 +22,7 @@ class DiffNote < Note
validate :verify_supported, unless: :importing?
before_validation :set_line_code, if: :on_text?, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ after_save :keep_around_commits, unless: -> { importing? || skip_keep_around_commits }
NoteDiffFileCreationError = Class.new(StandardError)
@@ -115,6 +115,20 @@ class DiffNote < Note
position&.multiline?
end
+ def shas
+ [
+ self.original_position.base_sha,
+ self.original_position.start_sha,
+ self.original_position.head_sha
+ ].tap do |a|
+ if self.position != self.original_position
+ a << self.position.base_sha
+ a << self.position.start_sha
+ a << self.position.head_sha
+ end
+ end
+ end
+
private
def enqueue_diff_file_creation_job
@@ -173,18 +187,6 @@ class DiffNote < Note
end
def keep_around_commits
- shas = [
- self.original_position.base_sha,
- self.original_position.start_sha,
- self.original_position.head_sha
- ]
-
- if self.position != self.original_position
- shas << self.position.base_sha
- shas << self.position.start_sha
- shas << self.position.head_sha
- end
-
repository.keep_around(*shas)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 963249c018a..48522a23068 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -27,11 +27,10 @@ class Environment < ApplicationRecord
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
- has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
- has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
- has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
- has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
+ has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) }
+ has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) }
+
has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
@@ -77,6 +76,7 @@ class Environment < ApplicationRecord
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
scope :preload_cluster, -> { preload(last_deployment: :cluster) }
+ scope :preload_project, -> { preload(:project) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
@@ -132,6 +132,10 @@ class Environment < ApplicationRecord
state :available
state :stopped
+ before_transition any => :stopped do |environment|
+ environment.auto_stop_at = nil
+ end
+
after_transition do |environment|
environment.expire_etag_cache
end
@@ -168,33 +172,6 @@ class Environment < ApplicationRecord
end
class << self
- ##
- # This method returns stop actions (jobs) for multiple environments within one
- # query. It's useful to avoid N+1 problem.
- #
- # NOTE: The count of environments should be small~medium (e.g. < 5000)
- def stop_actions
- cte = cte_for_deployments_with_stop_action
- ci_builds = Ci::Build.arel_table
-
- inner_join_stop_actions = ci_builds.join(cte.table).on(
- ci_builds[:project_id].eq(cte.table[:project_id])
- .and(ci_builds[:ref].eq(cte.table[:ref]))
- .and(ci_builds[:name].eq(cte.table[:on_stop]))
- ).join_sources
-
- pipeline_ids = ci_builds.join(cte.table).on(
- ci_builds[:id].eq(cte.table[:deployable_id])
- ).project(:commit_id)
-
- Ci::Build.joins(inner_join_stop_actions)
- .with(cte.to_arel)
- .where(ci_builds[:commit_id].in(pipeline_ids))
- .where(status: Ci::HasStatus::BLOCKED_STATUS)
- .preload_project_and_pipeline_project
- .preload(:user, :metadata, :deployment)
- end
-
def count_by_state
environments_count_by_state = group(:state).count
@@ -202,15 +179,35 @@ class Environment < ApplicationRecord
count_hash[state] = environments_count_by_state[state.to_s] || 0
end
end
+ end
+
+ def last_deployable
+ last_deployment&.deployable
+ end
- private
+ # NOTE: Below assocation overrides is a workaround for issue https://gitlab.com/gitlab-org/gitlab/-/issues/339908
+ # It helps to avoid cross joins with the CI database.
+ # Caveat: It also overrides and losses the default AR caching mechanism.
+ # Read - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68870#note_677227727
- def cte_for_deployments_with_stop_action
- Gitlab::SQL::CTE.new(:deployments_with_stop_action,
- Deployment.where(environment_id: select(:id))
- .distinct_on_environment
- .stoppable)
- end
+ # NOTE: Association Preloads does not use the overriden definitions below.
+ # Association Preloads when preloading uses the original definitions from the relationships above.
+ # https://github.com/rails/rails/blob/75ac626c4e21129d8296d4206a1960563cc3d4aa/activerecord/lib/active_record/associations/preloader.rb#L158
+ # But after preloading, when they are called it is using the overriden methods below.
+ # So we are checking for `association_cached?(:association_name)` in the overridden methods and calling `super` which inturn fetches the preloaded values.
+
+ # Overriding association
+ def last_visible_deployable
+ return super if association_cached?(:last_visible_deployable) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml)
+
+ last_visible_deployment&.deployable
+ end
+
+ # Overriding association
+ def last_visible_pipeline
+ return super if association_cached?(:last_visible_pipeline) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml)
+
+ last_visible_deployable&.pipeline
end
def clear_prometheus_reactive_cache!(query_name)
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 07c0983f239..3be7af2e4bf 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -100,11 +100,13 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
- next unless Ability.allowed?(user, :read_environment, environment)
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/340781') do
+ pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
+ next unless Ability.allowed?(user, :read_environment, environment)
- EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
- end.compact
+ EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
+ end.compact
+ end
end
private_class_method :build_environments_status
end
diff --git a/app/models/error_tracking/client_key.rb b/app/models/error_tracking/client_key.rb
index 9d12c0ed6f1..8e59f6f9ecb 100644
--- a/app/models/error_tracking/client_key.rb
+++ b/app/models/error_tracking/client_key.rb
@@ -14,9 +14,13 @@ class ErrorTracking::ClientKey < ApplicationRecord
find_by(public_key: key)
end
+ def sentry_dsn
+ @sentry_dsn ||= ErrorTracking::Collector::Dsn.build_url(public_key, project_id)
+ end
+
private
def generate_key
- self.public_key = "glet_#{SecureRandom.hex}"
+ self.public_key ||= "glet_#{SecureRandom.hex}"
end
end
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 32932c4d045..39ecc487806 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ErrorTracking::Error < ApplicationRecord
+ include Sortable
+
belongs_to :project
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
@@ -22,11 +24,28 @@ class ErrorTracking::Error < ApplicationRecord
def self.report_error(name:, description:, actor:, platform:, timestamp:)
safe_find_or_create_by(
name: name,
- description: description,
actor: actor,
platform: platform
- ) do |error|
- error.update!(last_seen_at: timestamp)
+ ).tap do |error|
+ error.update!(
+ # Description can contain object id, so it can't be
+ # used as a group criteria for similar errors.
+ description: description,
+ last_seen_at: timestamp
+ )
+ end
+ end
+
+ def self.sort_by_attribute(method)
+ case method.to_s
+ when 'last_seen'
+ order(last_seen_at: :desc)
+ when 'first_seen'
+ order(first_seen_at: :desc)
+ when 'frequency'
+ order(events_count: :desc)
+ else
+ order_id_desc
end
end
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index c5a77427588..dd5ce9f7387 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -51,7 +51,7 @@ module ErrorTracking
end
def integrated_client?
- integrated && ::Feature.enabled?(:integrated_error_tracking, project)
+ integrated
end
def api_url=(value)
diff --git a/app/models/event.rb b/app/models/event.rb
index f6174589a84..d6588699d27 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -9,6 +9,9 @@ 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/group.rb b/app/models/group.rb
index f6b45a755e4..437c750afa6 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -18,6 +18,10 @@ class Group < Namespace
include EachBatch
include BulkMemberAccessLoad
+ def self.sti_name
+ 'Group'
+ end
+
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -74,13 +78,16 @@ class Group < Namespace
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :dependency_proxy_setting, class_name: 'DependencyProxy::GroupSetting'
+ has_one :dependency_proxy_image_ttl_policy, class_name: 'DependencyProxy::ImageTtlGroupPolicy'
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, to: :namespace_settings
+ has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id
+
+ delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, :setup_for_company, :jobs_to_be_done, to: :namespace_settings
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -260,6 +267,15 @@ class Group < Namespace
Gitlab::UrlBuilder.build(self, only_path: only_path)
end
+ def dependency_proxy_image_prefix
+ # The namespace path can include uppercase letters, which
+ # Docker doesn't allow. The proxy expects it to be downcased.
+ url = "#{web_url.downcase}#{DependencyProxy::URL_SUFFIX}"
+
+ # Docker images do not include the protocol
+ url.partition('//').last
+ end
+
def human_name
full_name
end
@@ -296,7 +312,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Groups::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
@@ -642,6 +658,10 @@ class Group < Namespace
members.owners.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
+ def membership_locked?
+ false # to support project and group calling this as 'source'
+ end
+
def supports_events?
false
end
@@ -734,6 +754,22 @@ 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
+
private
def max_member_access(user_ids)
@@ -822,9 +858,15 @@ class Group < Namespace
end
def self.groups_including_descendants_by(group_ids)
- Gitlab::ObjectHierarchy
- .new(Group.where(id: group_ids))
+ groups = Group.where(id: group_ids)
+
+ if Feature.enabled?(:linear_group_including_descendants_by, default_enabled: :yaml)
+ groups.self_and_descendants
+ else
+ Gitlab::ObjectHierarchy
+ .new(groups)
.base_and_descendants
+ end
end
def disable_shared_runners!
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 9a78fe3971c..cb5c1ac48cd 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -80,6 +80,8 @@ class WebHook < ApplicationRecord
end
def backoff!
+ return if backoff_count >= MAX_FAILURES && disabled_until && disabled_until > Time.current
+
assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
save(validate: false)
end
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 09a60e9dd10..9565dae08b5 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -13,7 +13,7 @@ class InstanceConfiguration
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
- gitlab_ci: gitlab_ci,
+ size_limits: size_limits,
package_file_size_limits: package_file_size_limits,
rate_limits: rate_limits }.deep_symbolize_keys
end
@@ -38,11 +38,16 @@ class InstanceConfiguration
rescue Resolv::ResolvError
end
- def gitlab_ci
- Settings.gitlab_ci
- .to_h
- .merge(artifacts_max_size: { value: Gitlab::CurrentSettings.max_artifacts_size.megabytes,
- default: 100.megabytes })
+ def size_limits
+ {
+ max_attachment_size: application_settings[:max_attachment_size].megabytes,
+ receive_max_input_size: application_settings[:receive_max_input_size]&.megabytes,
+ max_import_size: application_settings[:max_import_size] > 0 ? application_settings[:max_import_size].megabytes : nil,
+ diff_max_patch_bytes: application_settings[:diff_max_patch_bytes].bytes,
+ max_artifacts_size: application_settings[:max_artifacts_size].megabytes,
+ max_pages_size: application_settings[:max_pages_size] > 0 ? application_settings[:max_pages_size].megabytes : nil,
+ snippet_size_limit: application_settings[:snippet_size_limit]&.bytes
+ }
end
def package_file_size_limits
diff --git a/app/models/integration.rb b/app/models/integration.rb
index a9c865569d0..158764bb783 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -274,7 +274,7 @@ class Integration < ApplicationRecord
end
def self.closest_group_integration(type, scope)
- group_ids = scope.ancestors(hierarchy_order: :asc).select(:id)
+ group_ids = scope.ancestors(hierarchy_order: :asc).reselect(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
@@ -357,6 +357,10 @@ class Integration < ApplicationRecord
[]
end
+ def password_fields
+ fields.select { |f| f[:type] == 'password' }.pluck(:name)
+ end
+
# Expose a list of fields in the JSON endpoint.
#
# This list is used in `Integration#as_json(only: json_fields)`.
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 5eae8bce92a..c6335782b5e 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -253,3 +253,5 @@ module Integrations
end
end
end
+
+Integrations::BaseChatNotification.prepend_mod_with('Integrations::BaseChatNotification')
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 6422f6bddab..72e0ca22ac2 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -7,7 +7,7 @@ module Integrations
extend Gitlab::Utils::Override
DEFAULT_DOMAIN = 'datadoghq.com'
- URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_domain}/api/v2/webhook'
+ URL_TEMPLATE = 'https://webhook-intake.%{datadog_domain}/api/v2/webhook'
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 54cb823d606..5746343c31c 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -76,7 +76,7 @@ module Integrations
name: 'google_iap_audience_client_id',
title: 'Google IAP Audience Client ID',
placeholder: s_('PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com'),
- help: s_('PrometheusService|PrometheusService|The ID of the IAP-secured resource.'),
+ help: s_('PrometheusService|The ID of the IAP-secured resource.'),
autocomplete: 'off',
required: false
},
diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb
index ff1f806df45..72e3c4a8cbc 100644
--- a/app/models/integrations/slack_slash_commands.rb
+++ b/app/models/integrations/slack_slash_commands.rb
@@ -9,7 +9,7 @@ module Integrations
end
def description
- "Perform common operations in Slack"
+ "Perform common operations in Slack."
end
def self.to_param
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
new file mode 100644
index 00000000000..68c02f54c61
--- /dev/null
+++ b/app/models/integrations/zentao.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Zentao < Integration
+ data_field :url, :api_url, :api_token, :zentao_product_xid
+
+ validates :url, public_url: true, presence: true, if: :activated?
+ validates :api_url, public_url: true, allow_blank: true
+ validates :api_token, presence: true, if: :activated?
+ validates :zentao_product_xid, presence: true, if: :activated?
+
+ def data_fields
+ zentao_tracker_data || self.build_zentao_tracker_data
+ end
+
+ def title
+ self.class.name.demodulize
+ end
+
+ def description
+ s_("ZentaoIntegration|Use Zentao as this project's issue tracker.")
+ end
+
+ def self.to_param
+ name.demodulize.downcase
+ end
+
+ def test(*_args)
+ client.ping
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def self.supported_event_actions
+ %w()
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'url',
+ title: s_('ZentaoIntegration|Zentao Web URL'),
+ placeholder: 'https://www.zentao.net',
+ help: s_('ZentaoIntegration|Base URL of the Zentao instance.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'api_url',
+ 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'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'zentao_product_xid',
+ title: s_('ZentaoIntegration|Zentao Product ID'),
+ required: true
+ }
+ ]
+ end
+
+ private
+
+ def client
+ @client ||= ::Gitlab::Zentao::Client.new(self)
+ end
+ end
+end
diff --git a/app/models/integrations/zentao_tracker_data.rb b/app/models/integrations/zentao_tracker_data.rb
new file mode 100644
index 00000000000..468e4e5d7d7
--- /dev/null
+++ b/app/models/integrations/zentao_tracker_data.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ZentaoTrackerData < ApplicationRecord
+ belongs_to :integration, inverse_of: :zentao_tracker_data, foreign_key: :integration_id
+ delegate :activated?, to: :integration
+ validates :integration, presence: true
+
+ scope :encryption_options, -> do
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :zentao_product_xid, encryption_options
+ attr_encrypted :api_token, encryption_options
+ end
+end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index a54de3c82d1..10d24ab50b2 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -4,7 +4,7 @@
# generated for a given scope and usage.
#
# The monotone sequence may be broken if an ID is explicitly provided
-# to `.track_greatest_and_save!` or `#track_greatest`.
+# to `#track_greatest`.
#
# For example, issues use their project to scope internal ids:
# In that sense, scope is "project" and usage is "issues".
@@ -29,32 +29,6 @@ class InternalId < ApplicationRecord
where(**scope, usage: usage)
end
- # Increments #last_value and saves the record
- #
- # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
- # As such, the increment is atomic and safe to be called concurrently.
- def increment_and_save!
- update_and_save { self.last_value = (last_value || 0) + 1 }
- end
-
- # Increments #last_value with new_value if it is greater than the current,
- # and saves the record
- #
- # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
- # As such, the increment is atomic and safe to be called concurrently.
- def track_greatest_and_save!(new_value)
- update_and_save { self.last_value = [last_value || 0, new_value].max }
- end
-
- private
-
- def update_and_save(&block)
- lock!
- yield
- save!
- last_value
- end
-
class << self
def track_greatest(subject, scope, usage, new_value, init)
build_generator(subject, scope, usage, init).track_greatest(new_value)
@@ -99,132 +73,7 @@ class InternalId < ApplicationRecord
private
def build_generator(subject, scope, usage, init = nil)
- if Feature.enabled?(:generate_iids_without_explicit_locking)
- ImplicitlyLockingInternalIdGenerator.new(subject, scope, usage, init)
- else
- InternalIdGenerator.new(subject, scope, usage, init)
- end
- end
- end
-
- class InternalIdGenerator
- # Generate next internal id for a given scope and usage.
- #
- # For currently supported usages, see #usage enum.
- #
- # The method implements a locking scheme that has the following properties:
- # 1) Generated sequence of internal ids is unique per (scope and usage)
- # 2) The method is thread-safe and may be used in concurrent threads/processes.
- # 3) The generated sequence is gapless.
- # 4) In the absence of a record in the internal_ids table, one will be created
- # and last_value will be calculated on the fly.
- #
- # subject: The instance or class we're generating an internal id for.
- # scope: Attributes that define the scope for id generation.
- # Valid keys are `project/project_id` and `namespace/namespace_id`.
- # usage: Symbol to define the usage of the internal id, see InternalId.usages
- # init: Proc that accepts the subject and the scope and returns Integer|NilClass
- attr_reader :subject, :scope, :scope_attrs, :usage, :init
-
- def initialize(subject, scope, usage, init = nil)
- @subject = subject
- @scope = scope
- @usage = usage
- @init = init
-
- raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty?
-
- unless InternalId.usages.has_key?(usage.to_s)
- raise ArgumentError, "Usage '#{usage}' is unknown. Supported values are #{InternalId.usages.keys} from InternalId.usages"
- end
- end
-
- # Generates next internal id and returns it
- # init: Block that gets called to initialize InternalId record if not present
- # Make sure to not throw exceptions in the absence of records (if this is expected).
- def generate
- InternalId.internal_id_transactions_increment(operation: :generate, usage: usage)
-
- subject.transaction do
- # Create a record in internal_ids if one does not yet exist
- # and increment its last value
- #
- # Note this will acquire a ROW SHARE lock on the InternalId record
- record.increment_and_save!
- end
- end
-
- # Reset tries to rewind to `value-1`. This will only succeed,
- # if `value` stored in database is equal to `last_value`.
- # value: The expected last_value to decrement
- def reset(value)
- return false unless value
-
- InternalId.internal_id_transactions_increment(operation: :reset, usage: usage)
-
- updated =
- InternalId
- .where(**scope, usage: usage_value)
- .where(last_value: value)
- .update_all('last_value = last_value - 1')
-
- updated > 0
- end
-
- # Create a record in internal_ids if one does not yet exist
- # and set its new_value if it is higher than the current last_value
- #
- # Note this will acquire a ROW SHARE lock on the InternalId record
-
- def track_greatest(new_value)
- InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
-
- subject.transaction do
- record.track_greatest_and_save!(new_value)
- end
- end
-
- def record
- @record ||= (lookup || create_record)
- end
-
- def with_lock(&block)
- InternalId.internal_id_transactions_increment(operation: :with_lock, usage: usage)
-
- record.with_lock(&block)
- end
-
- private
-
- # Retrieve InternalId record for (project, usage) combination, if it exists
- def lookup
- InternalId.find_by(**scope, usage: usage_value)
- end
-
- def usage_value
- @usage_value ||= InternalId.usages[usage.to_s]
- end
-
- # Create InternalId record for (scope, usage) combination, if it doesn't exist
- #
- # We blindly insert without synchronization. If another process
- # was faster in doing this, we'll realize once we hit the unique key constraint
- # violation. We can safely roll-back the nested transaction and perform
- # a lookup instead to retrieve the record.
- def create_record
- raise ArgumentError, 'Cannot initialize without init!' unless init
-
- instance = subject.is_a?(::Class) ? nil : subject
-
- subject.transaction(requires_new: true) do
- InternalId.create!(
- **scope,
- usage: usage_value,
- last_value: init.call(instance, scope) || 0
- )
- end
- rescue ActiveRecord::RecordNotUnique
- lookup
+ ImplicitlyLockingInternalIdGenerator.new(subject, scope, usage, init)
end
end
@@ -247,6 +96,8 @@ class InternalId < ApplicationRecord
# init: Proc that accepts the subject and the scope and returns Integer|NilClass
attr_reader :subject, :scope, :scope_attrs, :usage, :init
+ RecordAlreadyExists = Class.new(StandardError)
+
def initialize(subject, scope, usage, init = nil)
@subject = subject
@scope = scope
@@ -270,10 +121,8 @@ class InternalId < ApplicationRecord
return next_iid if next_iid
- create_record!(subject, scope, usage, init) do |iid|
- iid.last_value += 1
- end
- rescue ActiveRecord::RecordNotUnique
+ create_record!(subject, scope, usage, initial_value(subject, scope) + 1)
+ rescue RecordAlreadyExists
retry
end
@@ -302,10 +151,8 @@ class InternalId < ApplicationRecord
next_iid = update_record!(subject, scope, usage, function)
return next_iid if next_iid
- create_record!(subject, scope, usage, init) do |object|
- object.last_value = [object.last_value, new_value].max
- end
- rescue ActiveRecord::RecordNotUnique
+ create_record!(subject, scope, usage, [initial_value(subject, scope), new_value].max)
+ rescue RecordAlreadyExists
retry
end
@@ -317,27 +164,45 @@ class InternalId < ApplicationRecord
stmt.set(arel_table[:last_value] => new_value)
stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints
- ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value') # rubocop: disable Database/MultipleDatabases
+ InternalId.connection.insert(stmt, 'Update InternalId', 'last_value')
end
- def create_record!(subject, scope, usage, init)
- raise ArgumentError, 'Cannot initialize without init!' unless init
+ def create_record!(subject, scope, usage, value)
+ scope[:project].save! if scope[:project] && !scope[:project].persisted?
+ scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted?
- instance = subject.is_a?(::Class) ? nil : subject
+ attributes = {
+ project_id: scope[:project]&.id || scope[:project_id],
+ namespace_id: scope[:namespace]&.id || scope[:namespace_id],
+ usage: usage_value,
+ last_value: value
+ }
- subject.transaction(requires_new: true) do
- last_value = init.call(instance, scope) || 0
+ result = InternalId.insert(attributes)
- internal_id = InternalId.create!(**scope, usage: usage, last_value: last_value) do |subject|
- yield subject if block_given?
- end
+ raise RecordAlreadyExists if result.empty?
- internal_id.last_value
- end
+ value
end
def arel_table
InternalId.arel_table
end
+
+ def initial_value(subject, scope)
+ raise ArgumentError, 'Cannot initialize without init!' unless init
+
+ # `init` computes the maximum based on actual records. We use the
+ # primary to make sure we have up to date results
+ Gitlab::Database::LoadBalancing::Session.current.use_primary do
+ instance = subject.is_a?(::Class) ? nil : subject
+
+ init.call(instance, scope) || 0
+ end
+ end
+
+ def usage_value
+ @usage_value ||= InternalId.usages[usage.to_s]
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 48e3fdd51e9..e0b0c352c22 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -128,13 +128,15 @@ class Issue < ApplicationRecord
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
- scope :public_only, -> { where(confidential: false) }
+ scope :public_only, -> {
+ without_hidden.where(confidential: false)
+ }
+
scope :confidential_only, -> { where(confidential: true) }
scope :without_hidden, -> {
if Feature.enabled?(:ban_user_feature_flag)
- where(id: joins('LEFT JOIN banned_users ON banned_users.user_id = issues.author_id WHERE banned_users.user_id IS NULL')
- .select('issues.id'))
+ where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
else
all
end
@@ -323,6 +325,13 @@ class Issue < ApplicationRecord
)
end
+ def self.column_order_id_asc
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table[:id].asc
+ )
+ end
+
def self.to_branch_name(*args)
branch_name = args.map(&:to_s).each_with_index.map do |arg, i|
arg.parameterize(preserve_case: i == 0).presence
@@ -584,15 +593,9 @@ class Issue < ApplicationRecord
confidential_changed?(from: true, to: false)
end
- # Ensure that the metrics association is safely created and respecting the unique constraint on issue_id
override :ensure_metrics
def ensure_metrics
- if !association(:metrics).loaded? || metrics.blank?
- metrics_record = Issue::Metrics.safe_find_or_create_by(issue: self)
- self.metrics = metrics_record
- end
-
- metrics.record!
+ Issue::Metrics.record!(self)
end
def record_create_action
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
index 86523bbd023..25afd9bf58d 100644
--- a/app/models/issue/metrics.rb
+++ b/app/models/issue/metrics.rb
@@ -9,25 +9,30 @@ class Issue::Metrics < ApplicationRecord
.or(where(arel_table['first_mentioned_in_commit_at'].gteq(timestamp)))
}
- def record!
- if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
- self.first_associated_with_milestone_at = Time.current
+ class << self
+ def record!(issue)
+ now = connection.quote(Time.current)
+ first_associated_with_milestone_at = issue.milestone_id.present? ? now : 'NULL'
+ first_added_to_board_at = issue_assigned_to_list_label?(issue) ? now : 'NULL'
+
+ sql = <<~SQL
+ INSERT INTO #{self.table_name} (issue_id, first_associated_with_milestone_at, first_added_to_board_at, created_at, updated_at)
+ VALUES (#{issue.id}, #{first_associated_with_milestone_at}, #{first_added_to_board_at}, NOW(), NOW())
+ ON CONFLICT (issue_id)
+ DO UPDATE SET
+ first_associated_with_milestone_at = LEAST(#{self.table_name}.first_associated_with_milestone_at, EXCLUDED.first_associated_with_milestone_at),
+ first_added_to_board_at = LEAST(#{self.table_name}.first_added_to_board_at, EXCLUDED.first_added_to_board_at),
+ updated_at = NOW()
+ RETURNING id
+ SQL
+
+ connection.execute(sql)
end
- if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
- self.first_added_to_board_at = Time.current
- end
-
- self.save
- end
+ private
- private
-
- def issue_assigned_to_list_label?
- # Avoid another DB lookup when issue.labels are empty by adding a guard clause here
- # We can't use issue.labels.empty? because that will cause a `Label Exists?` DB lookup
- return false if issue.labels.length == 0 # rubocop:disable Style/ZeroLengthPredicate
-
- issue.labels.includes(:lists).any? { |label| label.lists.present? }
+ def issue_assigned_to_list_label?(issue)
+ issue.labels.joins(:lists).exists?
+ end
end
end
diff --git a/app/models/loose_foreign_keys.rb b/app/models/loose_foreign_keys.rb
new file mode 100644
index 00000000000..0f45c0b5568
--- /dev/null
+++ b/app/models/loose_foreign_keys.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ def self.table_name_prefix
+ 'loose_foreign_keys_'
+ end
+end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
new file mode 100644
index 00000000000..a39d88b2e49
--- /dev/null
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class LooseForeignKeys::DeletedRecord < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+ include PartitionedTable
+
+ partitioned_by :created_at, strategy: :monthly, retain_for: 3.months, retain_non_empty_partitions: true
+
+ scope :ordered_by_primary_keys, -> { order(:created_at, :deleted_table_name, :deleted_table_primary_key_value) }
+
+ def self.load_batch(batch_size)
+ ordered_by_primary_keys
+ .limit(batch_size)
+ .to_a
+ end
+
+ # Because the table has composite primary keys, the delete_all or delete methods are not going to work.
+ # This method implements deletion that benefits from the primary key index, example:
+ #
+ # > DELETE
+ # > FROM "loose_foreign_keys_deleted_records"
+ # > WHERE (created_at,
+ # > deleted_table_name,
+ # > deleted_table_primary_key_value) IN
+ # > (SELECT created_at::TIMESTAMP WITH TIME ZONE,
+ # > deleted_table_name,
+ # > deleted_table_primary_key_value
+ # > FROM (VALUES (LIST_OF_VALUES)) AS primary_key_values (created_at, deleted_table_name, deleted_table_primary_key_value))
+ def self.delete_records(records)
+ values = records.pluck(:created_at, :deleted_table_name, :deleted_table_primary_key_value)
+
+ primary_keys = connection.primary_keys(table_name).join(', ')
+
+ primary_keys_with_type_cast = [
+ Arel.sql('created_at::timestamp with time zone'),
+ Arel.sql('deleted_table_name'),
+ Arel.sql('deleted_table_primary_key_value')
+ ]
+
+ value_list = Arel::Nodes::ValuesList.new(values)
+
+ # (SELECT primary keys FROM VALUES)
+ inner_query = Arel::SelectManager.new
+ inner_query.from("#{Arel::Nodes::Grouping.new([value_list]).as('primary_key_values').to_sql} (#{primary_keys})")
+ inner_query.projections = primary_keys_with_type_cast
+
+ where(Arel::Nodes::Grouping.new([Arel.sql(primary_keys)]).in(inner_query)).delete_all
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 397e60be3a8..beb4c05f2a6 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -147,7 +147,6 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) }
- scope :with_user_by_email, -> (email) { left_join_users.where(users: { email: email } ) }
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
@@ -278,12 +277,14 @@ class Member < ApplicationRecord
def accept_invite!(new_user)
return false unless invite?
+ return false unless new_user
+
+ self.user = new_user
+ return false unless self.user.save
self.invite_token = nil
self.invite_accepted_at = Time.current.utc
- self.user = new_user
-
saved = self.save
after_accept_invite if saved
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index b45c0b6a0cc..72cb831cc88 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -44,7 +44,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a090ac87cc9..db49ec6f412 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -615,8 +615,8 @@ class MergeRequest < ApplicationRecord
context_commits.count
end
- def commits(limit: nil)
- return merge_request_diff.commits(limit: limit) if merge_request_diff.persisted?
+ def commits(limit: nil, load_from_gitaly: false)
+ return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly) if merge_request_diff.persisted?
commits_arr = if compare_commits
reversed_commits = compare_commits.reverse
@@ -628,8 +628,8 @@ class MergeRequest < ApplicationRecord
CommitCollection.new(source_project, commits_arr, source_branch)
end
- def recent_commits
- commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE)
+ def recent_commits(load_from_gitaly: false)
+ commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: load_from_gitaly)
end
def commits_count
@@ -1349,7 +1349,9 @@ class MergeRequest < ApplicationRecord
def has_ci?
return false if has_no_commits?
- !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_integration)
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do
+ !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_integration)
+ end
end
def branch_missing?
@@ -1835,15 +1837,10 @@ class MergeRequest < ApplicationRecord
Ability.allowed?(user, :push_code, source_project)
end
- def squash_in_progress?
- # The source project can be deleted
- return false unless source_project
-
- source_project.repository.squash_in_progress?(id)
- end
-
def find_actual_head_pipeline
- all_pipelines.for_sha_or_source_sha(diff_head_sha).first
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do
+ all_pipelines.for_sha_or_source_sha(diff_head_sha).first
+ end
end
def etag_caching_enabled?
@@ -1860,25 +1857,29 @@ class MergeRequest < ApplicationRecord
override :ensure_metrics
def ensure_metrics
- # Backward compatibility: some merge request metrics records will not have target_project_id filled in.
- # In that case the first `safe_find_or_create_by` will return false.
- # The second finder call will be eliminated in https://gitlab.com/gitlab-org/gitlab/-/issues/233507
- metrics_record = MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id, target_project_id: target_project_id) || MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id)
-
- metrics_record.tap do |metrics_record|
- # Make sure we refresh the loaded association object with the newly created/loaded item.
- # This is needed in order to have the exact functionality than before.
- #
- # Example:
- #
- # merge_request.metrics.destroy
- # merge_request.ensure_metrics
- # merge_request.metrics # should return the metrics record and not nil
- # merge_request.metrics.merge_request # should return the same MR record
-
- metrics_record.target_project_id = target_project_id
- metrics_record.association(:merge_request).target = self
- association(:metrics).target = metrics_record
+ if Feature.enabled?(:use_upsert_query_for_mr_metrics)
+ MergeRequest::Metrics.record!(self)
+ else
+ # Backward compatibility: some merge request metrics records will not have target_project_id filled in.
+ # In that case the first `safe_find_or_create_by` will return false.
+ # The second finder call will be eliminated in https://gitlab.com/gitlab-org/gitlab/-/issues/233507
+ metrics_record = MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id, target_project_id: target_project_id) || MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id)
+
+ metrics_record.tap do |metrics_record|
+ # Make sure we refresh the loaded association object with the newly created/loaded item.
+ # This is needed in order to have the exact functionality than before.
+ #
+ # Example:
+ #
+ # merge_request.metrics.destroy
+ # merge_request.ensure_metrics
+ # merge_request.metrics # should return the metrics record and not nil
+ # merge_request.metrics.merge_request # should return the same MR record
+
+ metrics_record.target_project_id = target_project_id
+ metrics_record.association(:merge_request).target = self
+ association(:metrics).target = metrics_record
+ end
end
end
@@ -1917,6 +1918,20 @@ class MergeRequest < ApplicationRecord
end
end
+ def lazy_upvotes_count
+ BatchLoader.for(id).batch(default_value: 0) do |ids, loader|
+ counts = AwardEmoji
+ .where(awardable_id: ids)
+ .upvotes
+ .group(:awardable_id)
+ .count
+
+ counts.each do |id, count|
+ loader.call(id, count)
+ end
+ end
+ end
+
private
def set_draft_status
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index b9460afa8e7..b984228eb13 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -14,8 +14,23 @@ class MergeRequest::Metrics < ApplicationRecord
scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) }
scope :by_target_project, ->(project) { where(target_project_id: project) }
- def self.time_to_merge_expression
- Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))')
+ class << self
+ def time_to_merge_expression
+ Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))')
+ end
+
+ def record!(mr)
+ sql = <<~SQL
+ INSERT INTO #{self.table_name} (merge_request_id, target_project_id, updated_at, created_at)
+ VALUES (#{mr.id}, #{mr.target_project_id}, NOW(), NOW())
+ ON CONFLICT (merge_request_id)
+ DO UPDATE SET
+ target_project_id = EXCLUDED.target_project_id,
+ updated_at = NOW()
+ SQL
+
+ connection.execute(sql)
+ end
end
private
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bea75927b2c..d2b3ca753b1 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -288,9 +288,9 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def commits(limit: nil)
- strong_memoize(:"commits_#{limit || 'all'}") do
- load_commits(limit: limit)
+ def commits(limit: nil, load_from_gitaly: false)
+ strong_memoize(:"commits_#{limit || 'all'}_#{load_from_gitaly}") do
+ load_commits(limit: limit, load_from_gitaly: load_from_gitaly)
end
end
@@ -700,9 +700,14 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def load_commits(limit: nil)
- commits = merge_request_diff_commits.with_users.limit(limit)
- .map { |commit| Commit.from_hash(commit.to_hash, project) }
+ def load_commits(limit: nil, load_from_gitaly: false)
+ if load_from_gitaly
+ commits = Gitlab::Git::Commit.batch_by_oid(repository, merge_request_diff_commits.limit(limit).map(&:sha))
+ commits = Commit.decorate(commits, project)
+ else
+ commits = merge_request_diff_commits.with_users.limit(limit)
+ .map { |commit| Commit.from_hash(commit.to_hash, project) }
+ end
CommitCollection
.new(merge_request.source_project, commits, merge_request.source_branch)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 0e2842c3c11..868bee9961b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -61,18 +61,10 @@ class Milestone < ApplicationRecord
end
def self.reference_pattern
- if Feature.enabled?(:milestone_reference_pattern, default_enabled: :yaml)
- new_reference_pattern
- else
- old_reference_pattern
- end
- end
-
- def self.new_reference_pattern
# NOTE: The iid pattern only matches when all characters on the expression
# are digits, so it will match %2 but not %2.1 because that's probably a
# milestone name and we want it to be matched as such.
- @new_reference_pattern ||= %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
@@ -87,26 +79,6 @@ class Milestone < ApplicationRecord
}x
end
- # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
- def self.old_reference_pattern
- # NOTE: The iid pattern only matches when all characters on the expression
- # are digits, so it will match %2 but not %2.1 because that's probably a
- # milestone name and we want it to be matched as such.
- @old_reference_pattern ||= %r{
- (#{Project.reference_pattern})?
- #{Regexp.escape(reference_prefix)}
- (?:
- (?<milestone_iid>
- \d+(?!\S\w)\b # Integer-based milestone iid, or
- ) |
- (?<milestone_name>
- [^"\s]+\b | # String-based single-word milestone title, or
- "[^"]+" # String-based multi-word milestone surrounded in quotes
- )
- )
- }x
- end
-
def self.link_reference_pattern
@link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 261639a4ec1..0c160cedb4d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -18,6 +18,11 @@ class Namespace < ApplicationRecord
ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
+ # Tells ActiveRecord not to store the full class name, in order to space some space
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
+ self.store_full_sti_class = false
+ self.store_full_class_name = false
+
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
# Android repo (15) + some extra backup.
@@ -52,7 +57,7 @@ class Namespace < ApplicationRecord
has_one :admin_note, inverse_of: :namespace
accepts_nested_attributes_for :admin_note, update_only: true
- validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
+ validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
presence: true,
length: { maximum: 255 }
@@ -131,6 +136,21 @@ class Namespace < ApplicationRecord
attr_writer :root_ancestor, :emails_disabled_memoized
class << self
+ def sti_class_for(type_name)
+ case type_name
+ when 'Group'
+ Group
+ when 'Project'
+ Namespaces::ProjectNamespace
+ when 'User'
+ # TODO: We create a normal Namespace until
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68894 is ready
+ Namespace
+ else
+ Namespace
+ end
+ end
+
def by_path(path)
find_by('lower(path) = :value', value: path.downcase)
end
@@ -227,15 +247,27 @@ class Namespace < ApplicationRecord
end
def kind
- type == 'Group' ? 'group' : 'user'
+ return 'group' if group?
+ return 'project' if project?
+
+ 'user' # defaults to user
+ end
+
+ def group?
+ type == Group.sti_name
+ end
+
+ def project?
+ type == Namespaces::ProjectNamespace.sti_name
end
def user?
- kind == 'user'
+ # That last bit ensures we're considered a user namespace as a default
+ type.nil? || type == Namespaces::UserNamespace.sti_name || !(group? || project?)
end
- def group?
- type == 'Group'
+ def owner_required?
+ user?
end
def find_fork_of(project)
@@ -498,17 +530,27 @@ class Namespace < ApplicationRecord
def nesting_level_allowed
if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
- errors.add(:parent_id, 'has too deep level of nesting')
+ errors.add(:parent_id, _('has too deep level of nesting'))
end
end
def validate_parent_type
- return unless has_parent?
+ unless has_parent?
+ if project?
+ errors.add(:parent_id, _('must be set for a project namespace'))
+ end
+
+ return
+ end
+
+ if parent.project?
+ errors.add(:parent_id, _('project namespace cannot be the parent of another namespace'))
+ end
if user?
- errors.add(:parent_id, 'a user namespace cannot have a parent')
+ errors.add(:parent_id, _('cannot not be used for user namespace'))
elsif group?
- errors.add(:parent_id, 'a group cannot have a user namespace as its parent') if parent.user?
+ errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user?
end
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 4a39bfebda0..170b29e9e21 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -2,6 +2,7 @@
class NamespaceSetting < ApplicationRecord
include CascadingNamespaceSettingAttribute
+ include Sanitizable
cascading_attr :delayed_project_removal
@@ -16,12 +17,17 @@ class NamespaceSetting < ApplicationRecord
before_validation :normalize_default_branch_name
+ enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true
+
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
:lock_delayed_project_removal, :resource_access_token_creation_allowed,
- :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap].freeze
+ :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap,
+ :setup_for_company, :jobs_to_be_done].freeze
self.primary_key = :namespace_id
+ sanitizes! :default_branch_name
+
def prevent_sharing_groups_outside_hierarchy
return super if namespace.root?
@@ -31,11 +37,7 @@ class NamespaceSetting < ApplicationRecord
private
def normalize_default_branch_name
- self.default_branch_name = if default_branch_name.blank?
- nil
- else
- Sanitize.fragment(self.default_branch_name)
- end
+ self.default_branch_name = default_branch_name.presence
end
def default_branch_name_content
diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb
new file mode 100644
index 00000000000..d1806c1c088
--- /dev/null
+++ b/app/models/namespaces/project_namespace.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+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
+ end
+end
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 33e8c3e5172..d7130322ed1 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -74,7 +74,7 @@ module Namespaces
return super unless use_traversal_ids_for_root_ancestor?
strong_memoize(:root_ancestor) do
- if parent.nil?
+ if parent_id.nil?
self
else
Namespace.find_by(id: traversal_ids.first)
@@ -176,13 +176,14 @@ module Namespaces
# if you are walking up the ancestors or down the descendants.
if hierarchy_order
depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
- skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
+ skope = skope.select(skope.default_select_columns, "#{depth_sql} as depth")
# The SELECT includes an extra depth attribute. We wrap the SQL in a
# 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)
end
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 90fae8ef35d..2da0e48c2da 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -15,6 +15,28 @@ module Namespaces
select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
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)'))
+ .order_by_depth(hierarchy_order)
+ .normal_select
+
+ if include_self
+ records
+ else
+ records.where.not(id: all.as_ids)
+ end
+ end
+
+ def self_and_ancestor_ids(include_self: true)
+ return super unless use_traversal_ids_for_ancestor_scopes?
+
+ self_and_ancestors(include_self: include_self).as_ids
+ end
+
def self_and_descendants(include_self: true)
return super unless use_traversal_ids?
@@ -22,11 +44,7 @@ module Namespaces
distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
- # Produce a query of the form: SELECT * FROM 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
- unscoped.without_sti_condition.from(distinct, :namespaces)
+ distinct.normal_select
end
def self_and_descendant_ids(include_self: true)
@@ -42,12 +60,35 @@ module Namespaces
unscope(where: :type)
end
+ def order_by_depth(hierarchy_order)
+ return all unless hierarchy_order
+
+ depth_order = hierarchy_order == :asc ? :desc : :asc
+
+ all
+ .select(Arel.star, 'array_length(traversal_ids, 1) as depth')
+ .order(depth: depth_order, id: :asc)
+ end
+
+ # Produce a query of the form: SELECT * FROM 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)
+ end
+
private
def use_traversal_ids?
Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
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)
base_ids = select(:id)
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
index c1ada715d6d..8d2c5d3be5a 100644
--- a/app/models/namespaces/traversal/recursive.rb
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -7,12 +7,12 @@ module Namespaces
include RecursiveScopes
def root_ancestor
- return self if parent.nil?
-
- if persisted?
+ if persisted? && !parent_id.nil?
strong_memoize(:root_ancestor) do
- recursive_self_and_ancestors.reorder(nil).find_by(parent_id: nil)
+ recursive_ancestors.reorder(nil).find_by(parent_id: nil)
end
+ elsif parent.nil?
+ self
else
parent.root_ancestor
end
diff --git a/app/models/namespaces/traversal/recursive_scopes.rb b/app/models/namespaces/traversal/recursive_scopes.rb
index be49d5d9d55..6659cefe095 100644
--- a/app/models/namespaces/traversal/recursive_scopes.rb
+++ b/app/models/namespaces/traversal/recursive_scopes.rb
@@ -10,6 +10,22 @@ module Namespaces
select('id')
end
+ def self_and_ancestors(include_self: true, hierarchy_order: nil)
+ records = Gitlab::ObjectHierarchy.new(all).base_and_ancestors(hierarchy_order: hierarchy_order)
+
+ if include_self
+ records
+ else
+ records.where.not(id: all.as_ids)
+ end
+ end
+ alias_method :recursive_self_and_ancestors, :self_and_ancestors
+
+ def self_and_ancestor_ids(include_self: true)
+ self_and_ancestors(include_self: include_self).as_ids
+ end
+ alias_method :recursive_self_and_ancestor_ids, :self_and_ancestor_ids
+
def descendant_ids
recursive_descendants.as_ids
end
diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb
new file mode 100644
index 00000000000..517d68b118d
--- /dev/null
+++ b/app/models/namespaces/user_namespace.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# TODO: currently not created/mapped in the database, will be done in another issue
+# https://gitlab.com/gitlab-org/gitlab/-/issues/337102
+module Namespaces
+ class UserNamespace < Namespace
+ def self.sti_name
+ 'User'
+ end
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index 34ffd7c91af..a8f5c305d9b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -48,6 +48,9 @@ class Note < ApplicationRecord
# Attribute used to store the attributes that have been changed by quick actions.
attr_accessor :commands_changes
+ # Attribute used to determine whether keep_around_commits will be skipped for diff notes.
+ attr_accessor :skip_keep_around_commits
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -112,7 +115,6 @@ class Note < ApplicationRecord
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
scope :with_suggestions, -> { joins(:suggestions) }
- scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
scope :inc_relations_for_view, -> do
@@ -579,7 +581,8 @@ class Note < ApplicationRecord
end
def post_processed_cache_key
- cache_key_items = [cache_key, author.cache_key]
+ cache_key_items = [cache_key, author&.cache_key]
+ cache_key_items << project.team.human_max_access(author&.id) if author.present?
cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
cache_key_items.join(':')
@@ -603,14 +606,6 @@ class Note < ApplicationRecord
private
- # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
- # in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block.
- def model_user_mention
- return if user_mentions.is_a?(ActiveRecord::NullRelation)
-
- user_mentions.first_or_initialize
- end
-
def system_note_viewable_by?(user)
return true unless system_note_metadata
@@ -648,7 +643,7 @@ class Note < ApplicationRecord
user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
else
refs = all_references(user)
- refs.all.any? && refs.stateful_not_visible_counter == 0
+ refs.all.any? && refs.all_visible?
end
end
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
index 9185547d7cd..c12309d1852 100644
--- a/app/models/onboarding_progress.rb
+++ b/app/models/onboarding_progress.rb
@@ -45,7 +45,7 @@ class OnboardingProgress < ApplicationRecord
def onboard(namespace)
return unless root_namespace?(namespace)
- safe_find_or_create_by(namespace: namespace)
+ create(namespace: namespace)
end
def onboarding?(namespace)
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 450a5970ad8..46810749b18 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -17,6 +17,7 @@ module Operations
has_internal_id :iid, scope: :project
default_value_for :active, true
+ default_value_for :version, :new_version_flag
# scopes exists only for the first version
has_many :scopes, class_name: 'Operations::FeatureFlagScope'
@@ -39,8 +40,6 @@ module Operations
validate :first_default_scope, on: :create, if: :has_scopes?
validate :version_associations
- before_create :build_default_scope, if: -> { legacy_flag? && scopes.none? }
-
accepts_nested_attributes_for :scopes, allow_destroy: true
accepts_nested_attributes_for :strategies, allow_destroy: true
@@ -52,7 +51,6 @@ module Operations
scope :new_version_only, -> { where(version: :new_version_flag)}
enum version: {
- legacy_flag: 1,
new_version_flag: 2
}
@@ -127,8 +125,6 @@ module Operations
def version_associations
if new_version_flag? && scopes.any?
errors.add(:version_associations, 'version 2 feature flags may not have scopes')
- elsif legacy_flag? && strategies.any?
- errors.add(:version_associations, 'version 1 feature flags may not have strategies')
end
end
diff --git a/app/models/operations/feature_flag_scope.rb b/app/models/operations/feature_flag_scope.rb
index 78be29f2531..9068ca0f588 100644
--- a/app/models/operations/feature_flag_scope.rb
+++ b/app/models/operations/feature_flag_scope.rb
@@ -1,5 +1,9 @@
# frozen_string_literal: true
+# All of the legacy flags have been removed in 14.1, including all of the
+# `operations_feature_flag_scopes` rows. Therefore, this model and the database
+# table are unused and should be removed.
+
module Operations
class FeatureFlagScope < ApplicationRecord
prepend HasEnvironmentScope
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 4ea127fc222..34eae6ab5dc 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
class Packages::Package < ApplicationRecord
+ include EachBatch
include Sortable
include Gitlab::SQL::Pattern
include UsageStatistics
@@ -104,6 +105,7 @@ class Packages::Package < ApplicationRecord
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) }
scope :with_conan_channel, ->(package_channel) do
joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel })
@@ -291,6 +293,13 @@ class Packages::Package < ApplicationRecord
::Packages::Maven::Metadata::SyncWorker.perform_async(user.id, project.id, name)
end
+ def create_build_infos!(build)
+ return unless build&.pipeline
+
+ # TODO: use an upsert call when https://gitlab.com/gitlab-org/gitlab/-/issues/339093 is implemented
+ build_infos.find_or_create_by!(pipeline: build.pipeline)
+ end
+
private
def composer_tag_version?
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 8aa19397086..14701b8a800 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -77,6 +77,10 @@ class Packages::PackageFile < ApplicationRecord
.where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference })
end
+ def self.most_recent!
+ recent.first!
+ end
+
mount_file_store_uploader Packages::PackageFileUploader
update_project_statistics project_statistics_name: :packages_size
@@ -89,6 +93,24 @@ class Packages::PackageFile < ApplicationRecord
skip_callback :commit, :after, :remove_previously_stored_file, if: :execute_move_in_object_storage?
after_commit :move_in_object_storage, if: :execute_move_in_object_storage?
+ # Returns the most recent package files for *each* of the given packages.
+ # The order is not guaranteed.
+ def self.most_recent_for(packages, extra_join: nil, extra_where: nil)
+ cte_name = :packages_cte
+ cte = Gitlab::SQL::CTE.new(cte_name, packages.select(:id))
+
+ package_files = ::Packages::PackageFile.limit_recent(1)
+ .where(arel_table[:package_id].eq(Arel.sql("#{cte_name}.id")))
+
+ package_files = package_files.joins(extra_join) if extra_join
+ package_files = package_files.where(extra_where) if extra_where
+
+ query = select('finder.*')
+ .from([Arel.sql(cte_name.to_s), package_files.arel.lateral.as('finder')])
+
+ query.with(cte.to_arel)
+ end
+
def download_path
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index 294a4e85d1f..da6ef035c54 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -16,6 +16,7 @@ class PagesDeployment < ApplicationRecord
scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) }
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
+ scope :project_id_in, ->(ids) { where(project_id: ids) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
@@ -27,10 +28,6 @@ class PagesDeployment < ApplicationRecord
mount_file_store_uploader ::Pages::DeploymentUploader
- def log_geo_deleted_event
- # this is to be adressed in https://gitlab.com/groups/gitlab-org/-/epics/589
- end
-
def migrated?
file.filename == MIGRATED_FILE_NAME
end
@@ -41,3 +38,5 @@ class PagesDeployment < ApplicationRecord
self.size = file.size
end
end
+
+PagesDeployment.prepend_mod
diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb
index 76b299ff9d4..12b48895e0c 100644
--- a/app/models/postgresql/detached_partition.rb
+++ b/app/models/postgresql/detached_partition.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Postgresql
- class DetachedPartition < ApplicationRecord
+ class DetachedPartition < ::Gitlab::Database::SharedModel
scope :ready_to_drop, -> { where('drop_after < ?', Time.current) }
end
end
diff --git a/app/models/preloaders/commit_status_preloader.rb b/app/models/preloaders/commit_status_preloader.rb
new file mode 100644
index 00000000000..535dd24ba6b
--- /dev/null
+++ b/app/models/preloaders/commit_status_preloader.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class CommitStatusPreloader
+ CLASSES = [::Ci::Build, ::Ci::Bridge, ::GenericCommitStatus].freeze
+
+ def initialize(statuses)
+ @statuses = statuses
+ end
+
+ def execute(relations)
+ preloader = ActiveRecord::Associations::Preloader.new
+
+ CLASSES.each do |klass|
+ preloader.preload(objects(klass), associations(klass, relations))
+ end
+ end
+
+ private
+
+ def objects(klass)
+ @statuses.select { |job| job.is_a?(klass) }
+ end
+
+ def associations(klass, relations)
+ klass.reflections.keys.map(&:to_sym) & relations.map(&:to_sym)
+ end
+ end
+end
diff --git a/app/models/preloaders/merge_requests_preloader.rb b/app/models/preloaders/merge_requests_preloader.rb
new file mode 100644
index 00000000000..cefe8408cab
--- /dev/null
+++ b/app/models/preloaders/merge_requests_preloader.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class MergeRequestsPreloader
+ attr_reader :merge_requests
+
+ def initialize(merge_requests)
+ @merge_requests = merge_requests
+ end
+
+ def execute
+ preloader = ActiveRecord::Associations::Preloader.new
+ preloader.preload(merge_requests, { target_project: [:project_feature] })
+ merge_requests.each do |merge_request|
+ merge_request.lazy_upvotes_count
+ end
+ 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
new file mode 100644
index 00000000000..14f1d271572
--- /dev/null
+++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+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
+
+ def initialize(groups, user)
+ @groups = groups
+ @user = user
+ end
+
+ def execute
+ group_memberships = GroupMember.active_without_invites_and_requests
+ .non_minimal_access
+ .where(user: @user, source_id: @groups)
+ .group(:source_id)
+ .maximum(:access_level)
+
+ group_memberships.each do |group_id, max_access_level|
+ merge_value_to_request_store(User, @user.id, group_id, max_access_level)
+ end
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 81b04e1316c..74ffeef797e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -103,6 +103,8 @@ class Project < ApplicationRecord
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
+ after_save :save_topics
+
after_create -> { create_or_load_association(:project_feature) }
after_create -> { create_or_load_association(:ci_cd_settings) }
@@ -118,7 +120,6 @@ class Project < ApplicationRecord
use_fast_destroy :build_trace_chunks
- after_destroy -> { run_after_commit { legacy_remove_pages } }
after_destroy :remove_exports
after_validation :check_pending_delete
@@ -127,12 +128,31 @@ class Project < ApplicationRecord
after_initialize :use_hashed_storage
after_create :check_repository_absence!
+ # Required during the `ActsAsTaggableOn::Tag -> Topic` migration
+ # TODO: remove 'acts_as_ordered_taggable_on' and ':topics_acts_as_taggable' in the further process of the migration
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/335946
acts_as_ordered_taggable_on :topics
+ has_many :topics_acts_as_taggable, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ class_name: 'ActsAsTaggableOn::Tag',
+ through: :topic_taggings,
+ source: :tag
+
+ has_many :project_topics, -> { order(:id) }, class_name: 'Projects::ProjectTopic'
+ has_many :topics, through: :project_topics, class_name: 'Projects::Topic'
+
+ # Required during the `ActsAsTaggableOn::Tag -> Topic` migration
+ # TODO: remove 'topics' in the further process of the migration
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/335946
+ alias_method :topics_new, :topics
+ def topics
+ self.topics_acts_as_taggable + self.topics_new
+ end
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
attr_accessor :skip_disk_validation
+ attr_writer :topic_list
alias_attribute :title, :name
@@ -141,6 +161,9 @@ class Project < ApplicationRecord
belongs_to :creator, class_name: 'User'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
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, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
@@ -188,6 +211,7 @@ class Project < ApplicationRecord
has_one :unify_circuit_integration, class_name: 'Integrations::UnifyCircuit'
has_one :webex_teams_integration, class_name: 'Integrations::WebexTeams'
has_one :youtrack_integration, class_name: 'Integrations::Youtrack'
+ has_one :zentao_integration, class_name: 'Integrations::Zentao'
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
@@ -317,6 +341,7 @@ class Project < ApplicationRecord
# build traces. Currently there's no efficient way of removing this data in
# bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here.
+ has_many :pending_builds, class_name: 'Ci::PendingBuild'
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
@@ -355,6 +380,7 @@ class Project < ApplicationRecord
has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project
has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
+ has_many :ci_feature_usages, class_name: 'Projects::CiFeatureUsage'
has_many :repository_storage_moves, class_name: 'Projects::RepositoryStorageMove', inverse_of: :container
@@ -503,6 +529,7 @@ class Project < ApplicationRecord
scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) }
# Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
+ scope :projects_order_id_asc, -> { reorder(self.arel_table['id'].asc) }
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :sorted_by_similarity_desc, -> (search, include_in_select: false) do
@@ -623,6 +650,19 @@ class Project < ApplicationRecord
joins(:service_desk_setting).where('service_desk_settings.project_key' => key)
end
+ scope :with_topic, ->(topic_name) do
+ topic = Projects::Topic.find_by_name(topic_name)
+ acts_as_taggable_on_topic = ActsAsTaggableOn::Tag.find_by_name(topic_name)
+
+ return none unless topic || acts_as_taggable_on_topic
+
+ relations = []
+ relations << where(id: topic.project_topics.select(:project_id)) if topic
+ relations << where(id: acts_as_taggable_on_topic.taggings.select(:taggable_id)) if acts_as_taggable_on_topic
+
+ Project.from_union(relations)
+ end
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@@ -638,7 +678,7 @@ class Project < ApplicationRecord
mount_uploader :bfg_object_map, AttachmentUploader
def self.with_api_entity_associations
- preload(:project_feature, :route, :topics, :group, :timelogs, namespace: [:route, :owner])
+ preload(:project_feature, :route, :topics, :topics_acts_as_taggable, :group, :timelogs, namespace: [:route, :owner])
end
def self.with_web_entity_associations
@@ -1421,7 +1461,7 @@ class Project < ApplicationRecord
end
def disabled_integrations
- []
+ [:zentao]
end
def find_or_initialize_integration(name)
@@ -1640,6 +1680,10 @@ class Project < ApplicationRecord
end
end
+ def membership_locked?
+ false
+ end
+
def bots
users.project_bot
end
@@ -1747,6 +1791,9 @@ class Project < ApplicationRecord
Ci::Runner.from_union([runners, group_runners, available_shared_runners])
end
+ # Once issue 339937 is fixed, please search for all mentioned of
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/339937,
+ # and remove the allow_cross_joins_across_databases.
def active_runners
strong_memoize(:active_runners) do
all_available_runners.active
@@ -1754,7 +1801,9 @@ class Project < ApplicationRecord
end
def any_online_runners?(&block)
- online_runners_with_tags.any?(&block)
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do
+ online_runners_with_tags.any?(&block)
+ end
end
def valid_runners_token?(token)
@@ -1763,7 +1812,15 @@ class Project < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def open_issues_count(current_user = nil)
- Projects::OpenIssuesCountService.new(self, current_user).count
+ return Projects::OpenIssuesCountService.new(self, current_user).count unless current_user.nil?
+
+ BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
+ issues_count_per_project = ::Projects::BatchOpenIssuesCountService.new(projects).refresh_cache_and_retrieve_data
+
+ issues_count_per_project.each do |project, count|
+ loader.call(project, count)
+ end
+ end
end
# rubocop: enable CodeReuse/ServiceClass
@@ -1849,27 +1906,6 @@ class Project < ApplicationRecord
.delete_all
end
- # TODO: remove this method https://gitlab.com/gitlab-org/gitlab/-/issues/320775
- # rubocop: disable CodeReuse/ServiceClass
- def legacy_remove_pages
- return unless ::Settings.pages.local_store.enabled
-
- # Projects with a missing namespace cannot have their pages removed
- return unless namespace
-
- mark_pages_as_not_deployed unless destroyed?
-
- # 1. We rename pages to temporary directory
- # 2. We wait 5 minutes, due to NFS caching
- # 3. We asynchronously remove pages with force
- temp_path = "#{path}.#{SecureRandom.hex}.deleted"
-
- if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
- PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
- end
- end
- # rubocop: enable CodeReuse/ServiceClass
-
def mark_pages_as_deployed(artifacts_archive: nil)
ensure_pages_metadatum.update!(deployed: true, artifacts_archive: artifacts_archive)
end
@@ -2093,6 +2129,10 @@ class Project < ApplicationRecord
# Docker doesn't allow. The proxy expects it to be downcased.
value: "#{Gitlab.host_with_port}/#{namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}"
)
+ variables.append(
+ key: 'CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX',
+ value: "#{Gitlab.host_with_port}/#{namespace.full_path.downcase}#{DependencyProxy::URL_SUFFIX}"
+ )
end
end
@@ -2239,7 +2279,7 @@ class Project < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def forks_count
- BatchLoader.for(self).batch do |projects, loader|
+ BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
fork_count_per_project = ::Projects::BatchForksCountService.new(projects).refresh_cache_and_retrieve_data
fork_count_per_project.each do |project, count|
@@ -2491,6 +2531,10 @@ class Project < ApplicationRecord
ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
end
+ def uses_external_project_ci_config?
+ !!(ci_config_path =~ %r{@.+/.+})
+ end
+
def limited_protected_branches(limit)
protected_branches.limit(limit)
end
@@ -2599,6 +2643,10 @@ class Project < ApplicationRecord
repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
end
+ def ci_config_external_project
+ Project.find_by_full_path(ci_config_path.split('@', 2).last)
+ end
+
def enabled_group_deploy_keys
return GroupDeployKey.none unless group
@@ -2669,8 +2717,37 @@ class Project < ApplicationRecord
ci_cd_settings.group_runners_enabled?
end
+ def topic_list
+ self.topics.map(&:name)
+ end
+
+ override :after_change_head_branch_does_not_exist
+ def after_change_head_branch_does_not_exist(branch)
+ self.errors.add(:base, _("Could not change HEAD: branch '%{branch}' does not exist") % { branch: branch })
+ end
+
private
+ def save_topics
+ return if @topic_list.nil?
+
+ @topic_list = @topic_list.split(',') if @topic_list.instance_of?(String)
+ @topic_list = @topic_list.map(&:strip).uniq.reject(&:empty?)
+
+ if @topic_list != self.topic_list || self.topics_acts_as_taggable.any?
+ self.topics_new.delete_all
+ self.topics = @topic_list.map { |topic| Projects::Topic.find_or_create_by(name: topic) }
+
+ # Remove old topics (ActsAsTaggableOn::Tag)
+ # Required during the `ActsAsTaggableOn::Tag -> Topic` migration
+ # TODO: remove in the further process of the migration
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/335946
+ self.topic_taggings.clear
+ end
+
+ @topic_list = nil
+ end
+
def find_integration(integrations, name)
integrations.find { _1.to_param == name }
end
@@ -2832,12 +2909,8 @@ class Project < ApplicationRecord
update_column(:has_external_issue_tracker, integrations.external_issue_trackers.any?) if Gitlab::Database.read_write?
end
- def active_runners_with_tags
- @active_runners_with_tags ||= active_runners.with_tags
- end
-
def online_runners_with_tags
- @online_runners_with_tags ||= active_runners_with_tags.online
+ @online_runners_with_tags ||= active_runners.with_tags.online
end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index aea8abecd74..676c28d5e1b 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -54,7 +54,6 @@ class ProjectFeature < ApplicationRecord
validates :project, presence: true
validate :repository_children_level
- validate :allowed_access_levels
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
@@ -110,17 +109,6 @@ class ProjectFeature < ApplicationRecord
%i(merge_requests_access_level builds_access_level).each(&validator)
end
- # Validates access level for other than pages cannot be PUBLIC
- def allowed_access_levels
- validator = lambda do |field|
- level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
- not_allowed = level > ENABLED
- self.errors.add(field, "cannot have public visibility level") if not_allowed
- end
-
- (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")}
- end
-
def get_permission(user, feature)
case access_level(feature)
when DISABLED
@@ -142,6 +130,10 @@ class ProjectFeature < ApplicationRecord
project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
end
+
+ def feature_validation_exclusion
+ %i(pages)
+ end
end
ProjectFeature.prepend_mod_with('ProjectFeature')
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 4ae3bc01a01..774d81156b7 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -42,7 +42,7 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
diff --git a/app/models/projects/project_topic.rb b/app/models/projects/project_topic.rb
new file mode 100644
index 00000000000..d4b456ef482
--- /dev/null
+++ b/app/models/projects/project_topic.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Projects
+ class ProjectTopic < ApplicationRecord
+ belongs_to :project
+ belongs_to :topic
+ end
+end
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
new file mode 100644
index 00000000000..a17aa550edb
--- /dev/null
+++ b/app/models/projects/topic.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Projects
+ class Topic < ApplicationRecord
+ validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
+
+ has_many :project_topics, class_name: 'Projects::ProjectTopic'
+ has_many :projects, through: :project_topics
+ end
+end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 3df8fe31826..3d32144e0f8 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -26,7 +26,9 @@ class ProtectedBranch < ApplicationRecord
def self.protected?(project, ref_name)
return true if project.empty_repo? && project.default_branch_protected?
- self.matching(ref_name, protected_refs: protected_refs(project)).present?
+ Rails.cache.fetch("protected_ref-#{ref_name}-#{project.cache_key}") do
+ self.matching(ref_name, protected_refs: protected_refs(project)).present?
+ end
end
def self.allow_force_push?(project, ref_name)
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
index 8358be35470..441b94e1855 100644
--- a/app/models/push_event_payload.rb
+++ b/app/models/push_event_payload.rb
@@ -2,6 +2,9 @@
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 aad1cbeabdb..0dd71c6ebfb 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -33,7 +33,6 @@ class Release < ApplicationRecord
includes(:author, :evidences, :milestones, :links, :sorted_links,
project: [:project_feature, :route, { namespace: :route }])
}
- scope :with_project_and_namespace, -> { includes(project: :namespace) }
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 0164d6fed93..f20b306c806 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -161,8 +161,8 @@ class Repository
CommitCollection.new(container, commits, ref)
end
- def commits_between(from, to)
- commits = Gitlab::Git::Commit.between(raw_repository, from, to)
+ def commits_between(from, to, limit: nil)
+ commits = Gitlab::Git::Commit.between(raw_repository, from, to, limit: limit)
commits = Commit.decorate(commits, container) if commits.present?
commits
end
@@ -191,7 +191,11 @@ class Repository
end
def find_tag(name)
- tags.find { |tag| tag.name == name }
+ if @tags.blank? && Feature.enabled?(:find_tag_via_gitaly, project, default_enabled: :yaml)
+ raw_repository.find_tag(name)
+ else
+ tags.find { |tag| tag.name == name }
+ end
end
def ambiguous_ref?(ref)
@@ -627,7 +631,14 @@ class Repository
def license
return unless license_key
- Licensee::License.new(license_key)
+ licensee_object = Licensee::License.new(license_key)
+
+ return if licensee_object.name.blank?
+
+ licensee_object
+ rescue Licensee::InvalidLicense => ex
+ Gitlab::ErrorTracking.track_exception(ex)
+ nil
end
memoize_method :license
@@ -721,18 +732,9 @@ class Repository
end
def tags_sorted_by(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
+ return raw_repository.tags(sort_by: value) if Feature.enabled?(:gitaly_tags_finder, project, default_enabled: :yaml)
+
+ tags_ruby_sort(value)
end
# Params:
@@ -1125,11 +1127,16 @@ class Repository
copy_gitattributes(branch)
after_change_head
else
- container.errors.add(:base, _("Could not change HEAD: branch '%{branch}' does not exist") % { branch: branch })
+ container.after_change_head_branch_does_not_exist(branch)
+
false
end
end
+ def cache
+ @cache ||= Gitlab::RepositoryCache.new(self)
+ end
+
private
# TODO Genericize finder, later split this on finders by Ref or Oid
@@ -1144,10 +1151,6 @@ class Repository
::Commit.new(commit, container) if commit
end
- def cache
- @cache ||= Gitlab::RepositoryCache.new(self)
- end
-
def redis_set_cache
@redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
end
@@ -1160,6 +1163,23 @@ 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
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index 1c854cc9941..6dd7415d928 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -19,7 +19,11 @@ class ServiceDeskSetting < ApplicationRecord
strong_memoize(:issue_template_content) do
next unless issue_template_key.present?
- Gitlab::Template::IssueTemplate.find(issue_template_key, project).content
+ TemplateFinder.new(
+ :issues, project,
+ name: issue_template_key,
+ source_template_project: source_template_project
+ ).execute.content
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
end
end
@@ -42,6 +46,10 @@ class ServiceDeskSetting < ApplicationRecord
private
+ def source_template_project
+ nil
+ end
+
def projects_with_same_slug_and_key_exists?
return false unless project_key
@@ -53,3 +61,5 @@ class ServiceDeskSetting < ApplicationRecord
end
end
end
+
+ServiceDeskSetting.prepend_mod
diff --git a/app/models/shard.rb b/app/models/shard.rb
index 335a279c6aa..9f0039d8bf9 100644
--- a/app/models/shard.rb
+++ b/app/models/shard.rb
@@ -18,10 +18,6 @@ class Shard < ApplicationRecord
end
def self.by_name(name)
- transaction(requires_new: true) do
- find_or_create_by(name: name)
- end
- rescue ActiveRecord::RecordNotUnique
- retry
+ safe_find_or_create_by(name: name)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index cb0f15c04cb..b5f0251f639 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -39,6 +39,12 @@ class User < ApplicationRecord
MAX_USERNAME_LENGTH = 255
MIN_USERNAME_LENGTH = 2
+ SECONDARY_EMAIL_ATTRIBUTES = [
+ :commit_email,
+ :notification_email,
+ :public_email
+ ].freeze
+
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token
@@ -181,7 +187,7 @@ class User < ApplicationRecord
has_many :todos
has_many :notification_settings
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :triggers, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :issue_assignees, inverse_of: :assignee
has_many :merge_request_assignees, inverse_of: :assignee
@@ -194,6 +200,7 @@ class User < ApplicationRecord
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
+ has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -222,10 +229,9 @@ class User < ApplicationRecord
validates :first_name, length: { maximum: 127 }
validates :last_name, length: { maximum: 127 }
validates :email, confirmation: true
- validates :notification_email, presence: true
- validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email }
+ validates :notification_email, devise_email: true, allow_blank: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, uniqueness: true, devise_email: true, allow_blank: true
- validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email }
+ validates :commit_email, devise_email: true, allow_blank: true, if: ->(user) { user.commit_email != user.email && user.commit_email != Gitlab::PrivateCommitEmail::TOKEN }
validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
@@ -247,12 +253,10 @@ class User < ApplicationRecord
validates :color_scheme_id, allow_nil: true, inclusion: { in: Gitlab::ColorSchemes.valid_ids,
message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
+ validates :website_url, allow_blank: true, url: true, if: :website_url_changed?
+
before_validation :sanitize_attrs
- before_validation :set_public_email, if: :public_email_changed?
- before_validation :set_commit_email, if: :commit_email_changed?
before_save :default_private_profile_to_false
- before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
- before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
@@ -302,14 +306,13 @@ class User < ApplicationRecord
:gitpod_enabled, :gitpod_enabled=,
:setup_for_company, :setup_for_company=,
:render_whitespace_in_code, :render_whitespace_in_code=,
- :experience_level, :experience_level=,
:markdown_surround_selection, :markdown_surround_selection=,
to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
delegate :other_role, :other_role=, to: :user_detail, allow_nil: true
- delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
+ delegate :bio, :bio=, to: :user_detail, allow_nil: true
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
@@ -347,6 +350,10 @@ class User < ApplicationRecord
transition active: :banned
end
+ event :unban do
+ transition banned: :active
+ end
+
event :deactivate do
# Any additional changes to this event should be also
# reflected in app/workers/users/deactivate_dormant_users_worker.rb
@@ -374,7 +381,9 @@ class User < ApplicationRecord
end
after_transition any => :deactivated do |user|
- NotificationService.new.user_deactivated(user.name, user.notification_email)
+ next unless Gitlab::CurrentSettings.user_deactivation_emails_enabled
+
+ NotificationService.new.user_deactivated(user.name, user.notification_email_or_default)
end
# rubocop: enable CodeReuse/ServiceClass
@@ -922,51 +931,18 @@ class User < ApplicationRecord
end
end
- def notification_email_verified
- return if read_attribute(:notification_email).blank? || temp_oauth_email?
-
- errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
- end
-
- def public_email_verified
- return if public_email.blank?
-
- errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
- end
-
- def commit_email_verified
- return if read_attribute(:commit_email).blank?
-
- errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
- end
-
- # Define commit_email-related attribute methods explicitly instead of relying
- # on ActiveRecord to provide them. Some of the specs use the current state of
- # the model code but an older database schema, so we need to guard against the
- # possibility of the commit_email column not existing.
-
- def commit_email
- return self.email unless has_attribute?(:commit_email)
-
- if super == Gitlab::PrivateCommitEmail::TOKEN
+ def commit_email_or_default
+ if self.commit_email == Gitlab::PrivateCommitEmail::TOKEN
return private_commit_email
end
# The commit email is the same as the primary email if undefined
- super.presence || self.email
+ self.commit_email.presence || self.email
end
- def commit_email=(email)
- super if has_attribute?(:commit_email)
- end
-
- def commit_email_changed?
- has_attribute?(:commit_email) && super
- end
-
- def notification_email
+ def notification_email_or_default
# The notification email is the same as the primary email if undefined
- super.presence || self.email
+ self.notification_email.presence || self.email
end
def private_commit_email
@@ -1009,7 +985,11 @@ class User < ApplicationRecord
# Returns the groups a user is a member of, either directly or through a parent group
def membership_groups
- Gitlab::ObjectHierarchy.new(groups).base_and_descendants
+ if Feature.enabled?(:linear_user_membership_groups, self, default_enabled: :yaml)
+ groups.self_and_descendants
+ else
+ Gitlab::ObjectHierarchy.new(groups).base_and_descendants
+ end
end
# Returns a relation of groups the user has access to, including their parent
@@ -1292,29 +1272,15 @@ class User < ApplicationRecord
self.name = self.name.gsub(%r{</?[^>]*>}, '')
end
- def set_notification_email
- if notification_email.blank? || all_emails.exclude?(notification_email)
- self.notification_email = email
- end
- end
-
- def set_public_email
- if public_email.blank? || all_emails.exclude?(public_email)
- self.public_email = ''
- end
- end
-
- def set_commit_email
- if commit_email.blank? || verified_emails.exclude?(commit_email)
- self.commit_email = nil
+ def unset_secondary_emails_matching_deleted_email!(deleted_email)
+ secondary_email_attribute_changed = false
+ SECONDARY_EMAIL_ATTRIBUTES.each do |attribute|
+ if read_attribute(attribute) == deleted_email
+ self.write_attribute(attribute, nil)
+ secondary_email_attribute_changed = true
+ end
end
- end
-
- def update_secondary_emails!
- set_notification_email
- set_public_email
- set_commit_email
- save if notification_email_changed? || public_email_changed? || commit_email_changed?
+ save if secondary_email_attribute_changed
end
def admin_unsubscribe!
@@ -1569,7 +1535,11 @@ class User < ApplicationRecord
end
def manageable_groups(include_groups_with_developer_maintainer_access: false)
- owned_and_maintainer_group_hierarchy = Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
+ owned_and_maintainer_group_hierarchy = if Feature.enabled?(:linear_user_manageable_groups, self, default_enabled: :yaml)
+ owned_or_maintainers_groups.self_and_descendants
+ else
+ Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
+ end
if include_groups_with_developer_maintainer_access
union_sql = ::Gitlab::SQL::Union.new(
@@ -1628,6 +1598,8 @@ 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
@@ -1644,9 +1616,15 @@ class User < ApplicationRecord
end
end
+ def owns_runner?(runner)
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436') do
+ ci_owned_runners.exists?(runner.id)
+ end
+ end
+
def notification_email_for(notification_group)
# Return group-specific email address if present, otherwise return global notification email address
- notification_group&.notification_email_for(self) || notification_email
+ notification_group&.notification_email_for(self) || notification_email_or_default
end
def notification_settings_for(source, inherit: false)
@@ -1935,10 +1913,14 @@ class User < ApplicationRecord
def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil)
callout = callouts_by_feature_name[feature_name]
- return false unless callout
- return callout.dismissed_after?(ignore_dismissal_earlier_than) if ignore_dismissal_earlier_than
+ callout_dismissed?(callout, ignore_dismissal_earlier_than)
+ end
- true
+ def dismissed_callout_for_group?(feature_name:, group:, ignore_dismissal_earlier_than: nil)
+ source_feature_name = "#{feature_name}_#{group.id}"
+ callout = group_callouts_by_feature_name[source_feature_name]
+
+ callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
# Load the current highest access by looking directly at the user's memberships
@@ -1962,6 +1944,11 @@ class User < ApplicationRecord
callouts.find_or_initialize_by(feature_name: ::UserCallout.feature_names[feature_name])
end
+ def find_or_initialize_group_callout(feature_name, group_id)
+ group_callouts
+ .find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
+ end
+
def can_trigger_notifications?
confirmed? && !blocked? && !ghost?
end
@@ -2015,10 +2002,39 @@ class User < ApplicationRecord
private
+ def notification_email_verified
+ return if notification_email.blank? || temp_oauth_email?
+
+ errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email_or_default)
+ end
+
+ def public_email_verified
+ return if public_email.blank?
+
+ errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
+ end
+
+ def commit_email_verified
+ return if commit_email.blank?
+
+ errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email_or_default)
+ end
+
+ def callout_dismissed?(callout, ignore_dismissal_earlier_than)
+ return false unless callout
+ return callout.dismissed_after?(ignore_dismissal_earlier_than) if ignore_dismissal_earlier_than
+
+ true
+ end
+
def callouts_by_feature_name
@callouts_by_feature_name ||= callouts.index_by(&:feature_name)
end
+ def group_callouts_by_feature_name
+ @group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
+ end
+
def authorized_groups_without_shared_membership
Group.from_union([
groups.select(Namespace.arel_table[Arel.star]),
@@ -2080,7 +2096,7 @@ class User < ApplicationRecord
def check_username_format
return if username.blank? || Mime::EXTENSION_LOOKUP.keys.none? { |type| username.end_with?(".#{type}") }
- errors.add(:username, _('ending with MIME type format is not allowed.'))
+ errors.add(:username, _('ending with a file extension is not allowed.'))
end
def groups_with_developer_maintainer_project_access
@@ -2090,9 +2106,12 @@ class User < ApplicationRecord
project_creation_levels << nil
end
- developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
- ::Group.where(id: developer_groups_hierarchy.select(:id),
- project_creation_level: project_creation_levels)
+ if Feature.enabled?(:linear_user_groups_with_developer_maintainer_project_access, self, default_enabled: :yaml)
+ developer_groups.self_and_descendants.where(project_creation_level: project_creation_levels)
+ else
+ developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
+ ::Group.where(id: developer_groups_hierarchy.select(:id), project_creation_level: project_creation_levels)
+ end
end
def no_recent_activity?
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 1172b2ee5e8..04bc29755f8 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class UserCallout < ApplicationRecord
- belongs_to :user
+ include Calloutable
enum feature_name: {
gke_cluster_integration: 1,
@@ -15,7 +15,7 @@ class UserCallout < ApplicationRecord
suggest_popover_dismissed: 9,
tabs_position_highlight: 10,
threat_monitoring_info: 11, # EE-only
- account_recovery_regular_check: 12, # EE-only
+ two_factor_auth_recovery_settings_check: 12, # EE-only
web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
@@ -39,13 +39,8 @@ class UserCallout < ApplicationRecord
terraform_notification_dismissed: 38
}
- validates :user, presence: true
validates :feature_name,
presence: true,
uniqueness: { scope: :user_id },
inclusion: { in: UserCallout.feature_names.keys }
-
- def dismissed_after?(dismissed_after)
- dismissed_at > dismissed_after
- end
end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index b3cca1e0cc0..c41cff67864 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -2,7 +2,8 @@
class UserDetail < ApplicationRecord
extend ::Gitlab::Utils::Override
- include CacheMarkdownField
+ include IgnorableColumns
+ ignore_columns %i[bio_html cached_markdown_version], remove_with: '13.6', remove_after: '2021-10-22'
belongs_to :user
@@ -13,20 +14,6 @@ class UserDetail < ApplicationRecord
before_save :prevent_nil_bio
- cache_markdown_field :bio
-
- def bio_html
- read_attribute(:bio_html) || bio
- end
-
- # For backward compatibility.
- # Older migrations (and their tests) reference the `User.migration_bot` where the `bio` attribute is set.
- # Here we disable writing the markdown cache when the `bio_html` column does not exist.
- override :invalidated_markdown_cache?
- def invalidated_markdown_cache?
- self.class.column_names.include?('bio_html') && super
- end
-
private
def prevent_nil_bio
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 2735e169b5f..337ae7125f3 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -20,7 +20,7 @@ class UserPreference < ApplicationRecord
less_than_or_equal_to: Gitlab::TabWidth::MAX
}
- enum experience_level: { novice: 0, experienced: 1 }
+ ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
new file mode 100644
index 00000000000..540d1a1d242
--- /dev/null
+++ b/app/models/users/group_callout.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Users
+ class GroupCallout < ApplicationRecord
+ include Calloutable
+
+ self.table_name = 'user_group_callouts'
+
+ belongs_to :group
+
+ enum feature_name: {
+ invite_members_banner: 1
+ }
+
+ validates :group, presence: true
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: [:user_id, :group_id] },
+ inclusion: { in: GroupCallout.feature_names.keys }
+
+ def source_feature_name
+ "#{feature_name}_#{group_id}"
+ end
+ end
+end
diff --git a/app/models/work_item/type.rb b/app/models/work_item/type.rb
index 16cb7a8be45..7038beadd62 100644
--- a/app/models/work_item/type.rb
+++ b/app/models/work_item/type.rb
@@ -9,14 +9,18 @@ class WorkItem::Type < ApplicationRecord
include CacheMarkdownField
+ # Base types need to exist on the DB on app startup
+ # This constant is used by the DB seeder
+ BASE_TYPES = {
+ issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
+ incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
+ test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only
+ requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 } ## EE-only
+ }.freeze
+
cache_markdown_field :description, pipeline: :single_line
- enum base_type: {
- issue: 0,
- incident: 1,
- test_case: 2, ## EE-only
- requirement: 3 ## EE-only
- }
+ enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] }
belongs_to :namespace, optional: true
has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type
@@ -30,6 +34,14 @@ class WorkItem::Type < ApplicationRecord
validates :name, length: { maximum: 255 }
validates :icon_name, length: { maximum: 255 }
+ def self.default_by_type(type)
+ find_by(namespace_id: nil, base_type: type)
+ end
+
+ def self.default_issue_type
+ default_by_type(:issue)
+ end
+
private
def strip_whitespace
diff --git a/app/models/zoom_meeting.rb b/app/models/zoom_meeting.rb
index f684f9e6fe0..dd230b46d4b 100644
--- a/app/models/zoom_meeting.rb
+++ b/app/models/zoom_meeting.rb
@@ -10,7 +10,7 @@ class ZoomMeeting < ApplicationRecord
validates :project, presence: true, unless: :importing?
validates :issue, presence: true, unless: :importing?
- validates :url, presence: true, length: { maximum: 255 }, 'gitlab/utils/zoom_url': true
+ validates :url, presence: true, length: { maximum: 255 }, 'gitlab/zoom_url': true
validates :issue, same_project_association: true, unless: :importing?
enum issue_status: {