summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/active_session.rb40
-rw-r--r--app/models/alert_management/http_integration.rb3
-rw-r--r--app/models/appearance.rb20
-rw-r--r--app/models/application_setting.rb22
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/models/awareness_session.rb49
-rw-r--r--app/models/broadcast_message.rb4
-rw-r--r--app/models/ci/bridge.rb33
-rw-r--r--app/models/ci/build.rb95
-rw-r--r--app/models/ci/build_metadata.rb16
-rw-r--r--app/models/ci/build_trace_chunk.rb2
-rw-r--r--app/models/ci/group_variable.rb1
-rw-r--r--app/models/ci/instance_variable.rb1
-rw-r--r--app/models/ci/job_variable.rb1
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/ci/pipeline_metadata.rb2
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb1
-rw-r--r--app/models/ci/pipeline_variable.rb1
-rw-r--r--app/models/ci/processable.rb1
-rw-r--r--app/models/ci/secure_file.rb2
-rw-r--r--app/models/ci/variable.rb1
-rw-r--r--app/models/clusters/applications/cert_manager.rb11
-rw-r--r--app/models/clusters/applications/crossplane.rb7
-rw-r--r--app/models/clusters/applications/helm.rb2
-rw-r--r--app/models/clusters/applications/ingress.rb5
-rw-r--r--app/models/clusters/applications/jupyter.rb2
-rw-r--r--app/models/clusters/applications/knative.rb2
-rw-r--r--app/models/clusters/applications/prometheus.rb8
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/clusters/integrations/prometheus.rb6
-rw-r--r--app/models/clusters/platforms/kubernetes.rb30
-rw-r--r--app/models/clusters/providers/aws.rb6
-rw-r--r--app/models/clusters/providers/gcp.rb8
-rw-r--r--app/models/commit_collection.rb7
-rw-r--r--app/models/commit_signatures/gpg_signature.rb4
-rw-r--r--app/models/commit_status.rb13
-rw-r--r--app/models/concerns/ci/has_status.rb4
-rw-r--r--app/models/concerns/ci/metadatable.rb10
-rw-r--r--app/models/concerns/ci/partitionable.rb9
-rw-r--r--app/models/concerns/ci/partitionable/switch.rb57
-rw-r--r--app/models/concerns/ci/raw_variable.rb17
-rw-r--r--app/models/concerns/ci/track_environment_usage.rb2
-rw-r--r--app/models/concerns/encrypted_user_password.rb82
-rw-r--r--app/models/concerns/enums/sbom.rb15
-rw-r--r--app/models/concerns/file_store_mounter.rb34
-rw-r--r--app/models/concerns/issuable.rb17
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/mirror_authentication.rb2
-rw-r--r--app/models/concerns/noteable.rb22
-rw-r--r--app/models/concerns/packages/debian/distribution.rb9
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb47
-rw-r--r--app/models/concerns/project_features_compatibility.rb4
-rw-r--r--app/models/concerns/protected_ref.rb1
-rw-r--r--app/models/concerns/protected_ref_access.rb4
-rw-r--r--app/models/concerns/redis_cacheable.rb10
-rw-r--r--app/models/concerns/repository_storage_movable.rb4
-rw-r--r--app/models/concerns/subquery.rb24
-rw-r--r--app/models/concerns/ttl_expirable.rb2
-rw-r--r--app/models/container_repository.rb23
-rw-r--r--app/models/cycle_analytics/project_level_stage_adapter.rb2
-rw-r--r--app/models/dependency_proxy/group_setting.rb4
-rw-r--r--app/models/deploy_token.rb2
-rw-r--r--app/models/deployment.rb5
-rw-r--r--app/models/diff_discussion.rb8
-rw-r--r--app/models/diff_viewer/server_side.rb8
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb11
-rw-r--r--app/models/event.rb4
-rw-r--r--app/models/event_collection.rb25
-rw-r--r--app/models/experiment.rb77
-rw-r--r--app/models/experiment_subject.rb32
-rw-r--r--app/models/experiment_user.rb14
-rw-r--r--app/models/group.rb21
-rw-r--r--app/models/hooks/active_hook_filter.rb28
-rw-r--r--app/models/hooks/system_hook.rb6
-rw-r--r--app/models/hooks/web_hook.rb48
-rw-r--r--app/models/hooks/web_hook_log.rb2
-rw-r--r--app/models/incident_management/timeline_event.rb2
-rw-r--r--app/models/incident_management/timeline_event_tag.rb11
-rw-r--r--app/models/instance_metadata.rb5
-rw-r--r--app/models/integration.rb32
-rw-r--r--app/models/integrations/assembla.rb5
-rw-r--r--app/models/integrations/bamboo.rb9
-rw-r--r--app/models/integrations/base_chat_notification.rb8
-rw-r--r--app/models/integrations/base_ci.rb2
-rw-r--r--app/models/integrations/base_issue_tracker.rb2
-rw-r--r--app/models/integrations/base_monitoring.rb2
-rw-r--r--app/models/integrations/base_slack_notification.rb62
-rw-r--r--app/models/integrations/base_slash_commands.rb2
-rw-r--r--app/models/integrations/base_third_party_wiki.rb2
-rw-r--r--app/models/integrations/buildkite.rb2
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb9
-rw-r--r--app/models/integrations/datadog.rb7
-rw-r--r--app/models/integrations/discord.rb3
-rw-r--r--app/models/integrations/drone_ci.rb1
-rw-r--r--app/models/integrations/hangouts_chat.rb6
-rw-r--r--app/models/integrations/jenkins.rb20
-rw-r--r--app/models/integrations/jira.rb6
-rw-r--r--app/models/integrations/mattermost.rb3
-rw-r--r--app/models/integrations/microsoft_teams.rb6
-rw-r--r--app/models/integrations/packagist.rb7
-rw-r--r--app/models/integrations/pivotaltracker.rb2
-rw-r--r--app/models/integrations/pumble.rb4
-rw-r--r--app/models/integrations/slack.rb63
-rw-r--r--app/models/integrations/teamcity.rb9
-rw-r--r--app/models/integrations/unify_circuit.rb8
-rw-r--r--app/models/integrations/webex_teams.rb4
-rw-r--r--app/models/issue.rb41
-rw-r--r--app/models/iteration.rb3
-rw-r--r--app/models/label.rb3
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/last_group_owner_assigner.rb2
-rw-r--r--app/models/members/member_task.rb5
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb22
-rw-r--r--app/models/merge_request_assignee.rb3
-rw-r--r--app/models/merge_request_diff.rb16
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/merge_request_diff_file.rb7
-rw-r--r--app/models/merge_request_reviewer.rb3
-rw-r--r--app/models/ml/candidate.rb9
-rw-r--r--app/models/ml/candidate_metric.rb2
-rw-r--r--app/models/ml/experiment.rb2
-rw-r--r--app/models/namespace.rb14
-rw-r--r--app/models/namespace_setting.rb5
-rw-r--r--app/models/network/graph.rb2
-rw-r--r--app/models/note.rb13
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/oauth_access_token.rb4
-rw-r--r--app/models/operations/feature_flag.rb4
-rw-r--r--app/models/packages/go/module_version.rb5
-rw-r--r--app/models/pages_domain.rb12
-rw-r--r--app/models/personal_access_token.rb33
-rw-r--r--app/models/postgresql/detached_partition.rb4
-rw-r--r--app/models/preloaders/project_root_ancestor_preloader.rb2
-rw-r--r--app/models/preloaders/user_max_access_level_in_projects_preloader.rb6
-rw-r--r--app/models/project.rb31
-rw-r--r--app/models/project_authorization.rb10
-rw-r--r--app/models/project_ci_cd_setting.rb4
-rw-r--r--app/models/project_feature.rb1
-rw-r--r--app/models/project_setting.rb6
-rw-r--r--app/models/project_statistics.rb7
-rw-r--r--app/models/projects/import_export/relation_export.rb15
-rw-r--r--app/models/projects/wiki_repository.rb13
-rw-r--r--app/models/protected_branch.rb16
-rw-r--r--app/models/protected_tag.rb1
-rw-r--r--app/models/repository.rb7
-rw-r--r--app/models/serverless/domain_cluster.rb8
-rw-r--r--app/models/terraform/state.rb2
-rw-r--r--app/models/terraform/state_version.rb2
-rw-r--r--app/models/time_tracking/timelog_category.rb3
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb73
-rw-r--r--app/models/users/callout.rb3
-rw-r--r--app/models/users/ghost_user_migration.rb2
-rw-r--r--app/models/users/namespace_commit_email.rb14
-rw-r--r--app/models/users_star_project.rb16
-rw-r--r--app/models/wiki.rb108
-rw-r--r--app/models/work_item.rb10
-rw-r--r--app/models/work_items/type.rb15
-rw-r--r--app/models/work_items/widgets/hierarchy.rb4
-rw-r--r--app/models/work_items/widgets/milestone.rb9
163 files changed, 1179 insertions, 859 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 7dbc95c251b..b16c4a2b353 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -83,24 +83,26 @@ class ActiveSession
is_impersonated: request.session[:impersonator_id].present?
)
- redis.pipelined do |pipeline|
- pipeline.setex(
- key_name(user.id, session_private_id),
- expiry,
- active_user_session.dump
- )
-
- # Deprecated legacy format - temporary to support mixed deployments
- pipeline.setex(
- key_name_v1(user.id, session_private_id),
- expiry,
- Marshal.dump(active_user_session)
- )
-
- pipeline.sadd(
- lookup_key_name(user.id),
- session_private_id
- )
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.pipelined do |pipeline|
+ pipeline.setex(
+ key_name(user.id, session_private_id),
+ expiry,
+ active_user_session.dump
+ )
+
+ # Deprecated legacy format - temporary to support mixed deployments
+ pipeline.setex(
+ key_name_v1(user.id, session_private_id),
+ expiry,
+ Marshal.dump(active_user_session)
+ )
+
+ pipeline.sadd?(
+ lookup_key_name(user.id),
+ session_private_id
+ )
+ end
end
end
end
@@ -298,7 +300,7 @@ class ActiveSession
session_ids_and_entries.each do |session_id, entry|
next if entry
- pipeline.srem(lookup_key, session_id)
+ pipeline.srem?(lookup_key, session_id)
removed << session_id
end
end
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index 0c3b1679dc3..b2686924363 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -13,8 +13,7 @@ module AlertManagement
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
- default_value_for(:endpoint_identifier, allows_nil: false) { SecureRandom.hex(8) }
- default_value_for(:token) { generate_token }
+ attribute :endpoint_identifier, default: -> { SecureRandom.hex(8) }
validates :project, presence: true
validates :active, inclusion: { in: [true, false] }
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 00a95070691..bd948c2c32a 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -6,6 +6,16 @@ class Appearance < ApplicationRecord
include ObjectStorage::BackgroundMove
include WithUploads
+ attribute :title, default: ''
+ attribute :description, default: ''
+ attribute :new_project_guidelines, default: ''
+ attribute :profile_image_guidelines, default: ''
+ attribute :header_message, default: ''
+ attribute :footer_message, default: ''
+ attribute :message_background_color, default: '#E75E40'
+ attribute :message_font_color, default: '#FFFFFF'
+ attribute :email_header_and_footer_enabled, default: false
+
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
cache_markdown_field :profile_image_guidelines
@@ -20,16 +30,6 @@ class Appearance < ApplicationRecord
validate :single_appearance_row, on: :create
- default_value_for :title, ''
- default_value_for :description, ''
- default_value_for :new_project_guidelines, ''
- default_value_for :profile_image_guidelines, ''
- default_value_for :header_message, ''
- default_value_for :footer_message, ''
- default_value_for :message_background_color, '#E75E40'
- default_value_for :message_font_color, '#FFFFFF'
- default_value_for :email_header_and_footer_enabled, false
-
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
mount_uploader :favicon, FaviconUploader
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 361b1a8dca9..adbbddd635c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -20,6 +20,7 @@ class ApplicationSetting < ApplicationRecord
'Admin Area > Settings > General > Kroki'
enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
+ enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
@@ -74,9 +75,9 @@ class ApplicationSetting < ApplicationRecord
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
cache_markdown_field :after_sign_up_text
- default_value_for :id, 1
- default_value_for :repository_storages_weighted, {}
- default_value_for :kroki_formats, {}
+ attribute :id, default: 1
+ attribute :repository_storages_weighted, default: -> { {} }
+ attribute :kroki_formats, default: -> { {} }
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
@@ -317,6 +318,7 @@ class ApplicationSetting < ApplicationRecord
less_than_or_equal_to: Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND }
validates :user_default_internal_regex, js_regex: true, allow_nil: true
+ validates :default_preferred_language, presence: true, inclusion: { in: Gitlab::I18n.available_locales }
validates :personal_access_token_prefix,
format: { with: %r{\A[a-zA-Z0-9_+=/@:.-]+\z},
@@ -527,6 +529,11 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') },
allow_blank: true
+ validates :jira_connect_proxy_url,
+ length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') },
+ allow_blank: true,
+ public_url: true
+
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
@@ -632,10 +639,6 @@ class ApplicationSetting < ApplicationRecord
validates :inactive_projects_send_warning_email_after_months,
numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months }
- validates :cube_api_base_url,
- addressable_url: { allow_localhost: true, allow_local_network: false },
- allow_blank: true
-
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -675,10 +678,15 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :jitsu_administrator_password, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :telesign_customer_xid, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
+ attr_encrypted :telesign_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
validates :disable_feed_token,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :disable_admin_oauth_scopes,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index dee4bd07fd9..308c05d638c 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -62,6 +62,7 @@ module ApplicationSettingImplementation
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
diff_max_files: Commit::DEFAULT_MAX_DIFF_FILES_SETTING,
diff_max_lines: Commit::DEFAULT_MAX_DIFF_LINES_SETTING,
+ disable_admin_oauth_scopes: false,
disable_feed_token: false,
disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
@@ -103,6 +104,7 @@ module ApplicationSettingImplementation
invisible_captcha_enabled: false,
issues_create_limit: 300,
jira_connect_application_key: nil,
+ jira_connect_proxy_url: nil,
local_markdown_version: 0,
login_recaptcha_protection_enabled: false,
mailgun_signing_key: nil,
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb
index a84a3454a27..0b652984630 100644
--- a/app/models/awareness_session.rb
+++ b/app/models/awareness_session.rb
@@ -63,16 +63,18 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
user_key = user_sessions_key(user.id)
with_redis do |redis|
- redis.pipelined do |pipeline|
- pipeline.sadd(user_key, id_i)
- pipeline.expire(user_key, USER_LIFETIME.to_i)
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.pipelined do |pipeline|
+ pipeline.sadd?(user_key, id_i)
+ pipeline.expire(user_key, USER_LIFETIME.to_i)
- pipeline.zadd(users_key, timestamp.to_f, user.id)
+ pipeline.zadd(users_key, timestamp.to_f, user.id)
- # We also mark for expiry when a session key is created (first user joins),
- # because some users might never actively leave a session and the key could
- # therefore become stale, w/o us noticing.
- reset_session_expiry(pipeline)
+ # We also mark for expiry when a session key is created (first user joins),
+ # because some users might never actively leave a session and the key could
+ # therefore become stale, w/o us noticing.
+ reset_session_expiry(pipeline)
+ end
end
end
@@ -83,26 +85,33 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
user_key = user_sessions_key(user.id)
with_redis do |redis|
- redis.pipelined do |pipeline|
- pipeline.srem(user_key, id_i)
- pipeline.zrem(users_key, user.id)
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.pipelined do |pipeline|
+ pipeline.srem?(user_key, id_i)
+ pipeline.zrem(users_key, user.id)
+ end
end
# cleanup orphan sessions and users
#
# this needs to be a second pipeline due to the delete operations being
# dependent on the result of the cardinality checks
- user_sessions_count, session_users_count = redis.pipelined do |pipeline|
- pipeline.scard(user_key)
- pipeline.zcard(users_key)
- end
+ user_sessions_count, session_users_count =
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.pipelined do |pipeline|
+ pipeline.scard(user_key)
+ pipeline.zcard(users_key)
+ end
+ end
- redis.pipelined do |pipeline|
- pipeline.del(user_key) unless user_sessions_count > 0
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.pipelined do |pipeline|
+ pipeline.del(user_key) unless user_sessions_count > 0
- unless session_users_count > 0
- pipeline.del(users_key)
- @id = nil
+ unless session_users_count > 0
+ pipeline.del(users_key)
+ @id = nil
+ end
end
end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 1f921c71984..c5a234ffa69 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -23,8 +23,8 @@ class BroadcastMessage < ApplicationRecord
validates :color, allow_blank: true, color: true
validates :font, allow_blank: true, color: true
- default_value_for :color, '#E75E40'
- default_value_for :font, '#FFFFFF'
+ attribute :color, default: '#E75E40'
+ attribute :font, default: '#FFFFFF'
CACHE_KEY = 'broadcast_message_current_json'
BANNER_CACHE_KEY = 'broadcast_message_current_banner_json'
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 323d759510e..d6051d70503 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -21,7 +21,6 @@ module Ci
has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
foreign_key: :source_job_id
- has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id
has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
validates :ref, presence: true
@@ -58,11 +57,7 @@ module Ci
end
def retryable?
- return false unless Feature.enabled?(:ci_recreate_downstream_pipeline, project)
-
- return false if failed? && (pipeline_loop_detected? || reached_max_descendant_pipelines_depth?)
-
- super
+ false
end
def self.with_preloads
@@ -183,7 +178,7 @@ module Ci
false
end
- def prevent_rollback_deployment?
+ def outdated_deployment?
false
end
@@ -288,7 +283,11 @@ module Ci
return [] unless forward_yaml_variables?
yaml_variables.to_a.map do |hash|
- { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) }
+ if hash[:raw] && ci_raw_variables_in_yaml_config_enabled?
+ { key: hash[:key], value: hash[:value], raw: true }
+ else
+ { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) }
+ end
end
end
@@ -296,7 +295,11 @@ module Ci
return [] unless forward_pipeline_variables?
pipeline.variables.to_a.map do |variable|
- { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
+ if variable.raw? && ci_raw_variables_in_yaml_config_enabled?
+ { key: variable.key, value: variable.value, raw: true }
+ else
+ { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
+ end
end
end
@@ -305,7 +308,11 @@ module Ci
return [] unless pipeline.pipeline_schedule
pipeline.pipeline_schedule.variables.to_a.map do |variable|
- { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
+ if variable.raw? && ci_raw_variables_in_yaml_config_enabled?
+ { key: variable.key, value: variable.value, raw: true }
+ else
+ { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
+ end
end
end
@@ -324,6 +331,12 @@ module Ci
result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result
end
end
+
+ def ci_raw_variables_in_yaml_config_enabled?
+ strong_memoize(:ci_raw_variables_in_yaml_config_enabled) do
+ ::Feature.enabled?(:ci_raw_variables_in_yaml_config, project)
+ end
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b8511536e32..f44ba124fe2 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -72,33 +72,6 @@ module Ci
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
- ##
- # Since Gitlab 11.5, deployments records started being created right after
- # `ci_builds` creation. We can look up a relevant `environment` through
- # `deployment` relation today.
- # (See more https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22380)
- #
- # Since Gitlab 12.9, we started persisting the expanded environment name to
- # avoid repeated variables expansion in `action: stop` builds as well.
- def persisted_environment
- return unless has_environment?
-
- strong_memoize(:persisted_environment) do
- # This code path has caused N+1s in the past, since environments are only indirectly
- # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
- # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
- BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
- Environment.where(name: names, project: args[:key]).find_each do |environment|
- loader.call(environment.name, environment)
- end
- end
- 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
@@ -199,7 +172,7 @@ module Ci
add_authentication_token_field :token, encrypted: :required
- before_save :ensure_token
+ before_save :ensure_token, unless: :assign_token_on_scheduling?
after_save :stick_build_if_status_changed
@@ -218,10 +191,6 @@ module Ci
preload(:job_artifacts_archive, :job_artifacts, :tags, project: [:namespace])
end
- def extra_accessors
- []
- end
-
def clone_accessors
%i[pipeline project ref tag options name
allow_failure stage stage_idx trigger_request
@@ -278,6 +247,14 @@ module Ci
!build.waiting_for_deployment_approval? # If false is returned, it stops the transition
end
+ before_transition any => [:pending] do |build, transition|
+ if build.assign_token_on_scheduling?
+ build.ensure_token
+ end
+
+ true
+ end
+
after_transition created: :scheduled do |build|
build.run_after_commit do
Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id)
@@ -445,9 +422,10 @@ module Ci
manual? && starts_environment? && deployment&.blocked?
end
- def prevent_rollback_deployment?
- strong_memoize(:prevent_rollback_deployment) do
+ def outdated_deployment?
+ strong_memoize(:outdated_deployment) do
starts_environment? &&
+ incomplete? &&
project.ci_forward_deployment_enabled? &&
deployment&.older_than_last_successful_deployment?
end
@@ -494,8 +472,34 @@ module Ci
Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet
end
+ def persisted_environment
+ return unless has_environment_keyword?
+
+ strong_memoize(:persisted_environment) do
+ # This code path has caused N+1s in the past, since environments are only indirectly
+ # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
+ # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
+ BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
+ Environment.where(name: names, project: args[:key]).find_each do |environment|
+ loader.call(environment.name, environment)
+ end
+ end
+ end
+ end
+
+ def persisted_environment=(environment)
+ strong_memoize(:persisted_environment) { environment }
+ end
+
+ # If build.persisted_environment is a BatchLoader, we need to remove
+ # the method proxy in order to clone into new item here
+ # https://github.com/exAspArk/batch-loader/issues/31
+ def actual_persisted_environment
+ persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment
+ end
+
def expanded_environment_name
- return unless has_environment?
+ return unless has_environment_keyword?
strong_memoize(:expanded_environment_name) do
# We're using a persisted expanded environment name in order to avoid
@@ -509,7 +513,7 @@ module Ci
end
def expanded_kubernetes_namespace
- return unless has_environment?
+ return unless has_environment_keyword?
namespace = options.dig(:environment, :kubernetes, :namespace)
@@ -520,16 +524,16 @@ module Ci
end
end
- def has_environment?
+ def has_environment_keyword?
environment.present?
end
def starts_environment?
- has_environment? && self.environment_action == 'start'
+ has_environment_keyword? && self.environment_action == 'start'
end
def stops_environment?
- has_environment? && self.environment_action == 'stop'
+ has_environment_keyword? && self.environment_action == 'stop'
end
def environment_action
@@ -971,7 +975,7 @@ module Ci
def collect_codequality_reports!(codequality_report)
each_report(Ci::JobArtifact.file_types_for_report(:codequality)) do |file_type, blob|
- Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report)
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report, { project: project, commit_sha: pipeline.sha })
end
codequality_report
@@ -1043,7 +1047,8 @@ module Ci
# TODO: Have `debug_mode?` check against data on sent back from runner
# to capture all the ways that variables can be set.
# See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955)
- variables['CI_DEBUG_TRACE']&.value&.casecmp('true') == 0
+ variables['CI_DEBUG_TRACE']&.value&.casecmp('true') == 0 ||
+ variables['CI_DEBUG_SERVICES']&.value&.casecmp('true') == 0
end
def drop_with_exit_code!(failure_reason, exit_code)
@@ -1131,6 +1136,10 @@ module Ci
end
end
+ def assign_token_on_scheduling?
+ ::Feature.enabled?(:ci_assign_job_token_on_scheduling, project)
+ end
+
protected
def run_status_commit_hooks!
@@ -1185,7 +1194,7 @@ module Ci
def environment_status
strong_memoize(:environment_status) do
- if has_environment? && merge_request
+ if has_environment_keyword? && merge_request
EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
end
end
@@ -1205,8 +1214,6 @@ module Ci
def legacy_jwt_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless Feature.enabled?(:ci_job_jwt, project)
-
jwt = Gitlab::Ci::Jwt.for_build(self)
jwt_v2 = Gitlab::Ci::JwtV2.for_build(self)
variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true)
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 33092e881f0..2f28509f812 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -5,6 +5,7 @@ module Ci
# Data that should be persisted forever, should be stored with Ci::Build model.
class BuildMetadata < Ci::ApplicationRecord
BuildTimeout = Struct.new(:value, :source)
+ ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_metadata_routing_table
include Ci::Partitionable
include Presentable
@@ -13,7 +14,12 @@ module Ci
self.table_name = 'ci_builds_metadata'
self.primary_key = 'id'
- partitionable scope: :build
+ self.sequence_name = 'ci_builds_metadata_id_seq'
+
+ partitionable scope: :build, through: {
+ table: :p_ci_builds_metadata,
+ flag: ROUTING_FEATURE_FLAG
+ }
belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
@@ -24,9 +30,9 @@ module Ci
validates :id_tokens, json_schema: { filename: 'build_metadata_id_tokens' }
validates :secrets, json_schema: { filename: 'build_metadata_secrets' }
- serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
- serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
- serialize :runtime_runner_features, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :config_options, :sym_jsonb
+ attribute :config_variables, :sym_jsonb
+ attribute :runtime_runner_features, :sym_jsonb
chronic_duration_attr_reader :timeout_human_readable, :timeout
@@ -50,7 +56,7 @@ module Ci
end
def set_cancel_gracefully
- runtime_runner_features.merge!( { cancel_gracefully: true } )
+ runtime_runner_features.merge!({ cancel_gracefully: true })
end
def cancel_gracefully?
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 221a2284106..7baa98b59f9 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -10,7 +10,7 @@ module Ci
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
- default_value_for :data_store, :redis_trace_chunks
+ attribute :data_store, default: :redis_trace_chunks
after_create { metrics.increment_trace_operation(operation: :chunked) }
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index e11edbda6dc..508aaa5a63c 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -5,6 +5,7 @@ module Ci
include Ci::HasVariable
include Presentable
include Ci::Maskable
+ include Ci::RawVariable
prepend HasEnvironmentScope
belongs_to :group, class_name: "::Group"
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index da9d4dea537..3e572dbe18f 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -5,6 +5,7 @@ module Ci
extend Gitlab::ProcessMemoryCache::Helper
include Ci::NewHasVariable
include Ci::Maskable
+ include Ci::RawVariable
include Limitable
self.limit_name = 'ci_instance_level_variables'
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index 44bd3fe8901..332a78b66ae 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -3,6 +3,7 @@
module Ci
class JobVariable < Ci::ApplicationRecord
include Ci::NewHasVariable
+ include Ci::RawVariable
include BulkInsertSafe
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index cc5ba41191b..020f5cf9d8e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -121,7 +121,7 @@ module Ci
accepts_nested_attributes_for :variables, reject_if: :persisted?
delegate :full_path, to: :project, prefix: true
- delegate :title, to: :pipeline_metadata, allow_nil: true
+ delegate :name, to: :pipeline_metadata, allow_nil: true
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
@@ -183,7 +183,11 @@ module Ci
end
event :succeed do
- transition any - [:success] => :success
+ # A success pipeline can also be retried, for example; a pipeline with a failed manual job.
+ # When retrying the pipeline, the status of the pipeline is not changed because the failed
+ # manual job transitions to the `manual` status.
+ # More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98967#note_1144718316
+ transition any => :success
end
event :cancel do
diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb
index c96b395b45f..2bd206c5ca5 100644
--- a/app/models/ci/pipeline_metadata.rb
+++ b/app/models/ci/pipeline_metadata.rb
@@ -9,6 +9,6 @@ module Ci
validates :pipeline, presence: true
validates :project, presence: true
- validates :title, presence: true, length: { minimum: 1, maximum: 255 }
+ validates :name, presence: true, length: { minimum: 1, maximum: 255 }
end
end
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index 84a24609cc7..718ed14edeb 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -3,6 +3,7 @@
module Ci
class PipelineScheduleVariable < Ci::ApplicationRecord
include Ci::HasVariable
+ include Ci::RawVariable
belongs_to :pipeline_schedule
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 6e4418bc360..8e83b41cd0b 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -4,6 +4,7 @@ module Ci
class PipelineVariable < Ci::ApplicationRecord
include Ci::Partitionable
include Ci::HasVariable
+ include Ci::RawVariable
belongs_to :pipeline
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 09dc9d4bce1..eb805ffae0a 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -7,6 +7,7 @@ module Ci
extend ::Gitlab::Utils::Override
has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
+ has_one :sourced_pipeline, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id, inverse_of: :source_job
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb
index ffff7eebbee..df38398e5a9 100644
--- a/app/models/ci/secure_file.rb
+++ b/app/models/ci/secure_file.rb
@@ -27,7 +27,7 @@ module Ci
serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
- default_value_for(:file_store) { Ci::SecureFileUploader.default_store }
+ attribute :file_store, default: -> { Ci::SecureFileUploader.default_store }
mount_file_store_uploader Ci::SecureFileUploader
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index c80c2ebe69a..f4e17b5d812 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -5,6 +5,7 @@ module Ci
include Ci::HasVariable
include Presentable
include Ci::Maskable
+ include Ci::RawVariable
prepend HasEnvironmentScope
belongs_to :project
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 2a051233de2..11f84940c38 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -15,11 +15,8 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
- default_value_for :version, VERSION
-
- default_value_for :email do |cert_manager|
- cert_manager.cluster&.user&.email
- end
+ attribute :version, default: VERSION
+ after_initialize :set_default_email, if: :new_record?
validates :email, presence: true
@@ -55,6 +52,10 @@ module Clusters
private
+ def set_default_email
+ self.email ||= self.cluster&.user&.email
+ end
+
def pre_install_script
[
apply_file("https://raw.githubusercontent.com/jetstack/cert-manager/release-#{CRD_VERSION}/deploy/manifests/00-crds.yaml"),
diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb
index 07378b4e8dc..a7b4fb57149 100644
--- a/app/models/clusters/applications/crossplane.rb
+++ b/app/models/clusters/applications/crossplane.rb
@@ -14,11 +14,8 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
- default_value_for :version, VERSION
-
- default_value_for :stack do |crossplane|
- ''
- end
+ attribute :version, default: VERSION
+ attribute :stack, default: ""
validates :stack, presence: true
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index e89cb8be1e7..9fac852ed5b 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -18,7 +18,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationStatus
include ::Gitlab::Utils::StrongMemoize
- default_value_for :version, Gitlab::Kubernetes::Helm::V2::BaseCommand::HELM_VERSION
+ attribute :version, default: Gitlab::Kubernetes::Helm::V2::BaseCommand::HELM_VERSION
before_create :create_keys_and_certs
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 27550616002..034b178d67d 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -17,12 +17,11 @@ module Clusters
include AfterCommitQueue
include UsageStatistics
- default_value_for :ingress_type, :nginx
- default_value_for :version, VERSION
+ attribute :version, default: VERSION
enum ingress_type: {
nginx: 1
- }
+ }, _default: :nginx
FETCH_IP_ADDRESS_DELAY = 30.seconds
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index 8d7d9c20bfa..9c0e90d59ed 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -18,7 +18,7 @@ module Clusters
belongs_to :oauth_application, class_name: 'Doorkeeper::Application'
- default_value_for :version, VERSION
+ attribute :version, default: VERSION
def set_initial_status
return unless not_installable?
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 0e7cbb35e47..64366594583 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -43,7 +43,7 @@ module Clusters
end
end
- default_value_for :version, VERSION
+ attribute :version, default: VERSION
validates :hostname, presence: true, hostname: true
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index d1e169a1f78..a076c871824 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -15,7 +15,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
- default_value_for :version, VERSION
+ attribute :version, default: VERSION
scope :preload_cluster_platform, -> { preload(cluster: [:platform_kubernetes]) }
@@ -24,7 +24,7 @@ module Clusters
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
- default_value_for(:alert_manager_token) { SecureRandom.hex }
+ after_initialize :set_alert_manager_token, if: :new_record?
after_destroy do
cluster.find_or_build_integration_prometheus.destroy
@@ -101,6 +101,10 @@ module Clusters
private
+ def set_alert_manager_token
+ self.alert_manager_token = SecureRandom.hex
+ end
+
def install_knative_metrics
return [] unless cluster.application_knative_available?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 1ac4cbac1da..b8ed33828bc 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -15,7 +15,7 @@ module Clusters
belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id
delegate :project, :group, to: :cluster
- default_value_for :version, VERSION
+ attribute :version, default: VERSION
def chart
"#{name}/gitlab-runner"
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index ad1e7dc305f..25d41d68b9e 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -79,7 +79,7 @@ module Clusters
validates :namespace_per_environment, inclusion: { in: [true, false] }
validates :helm_major_version, inclusion: { in: [2, 3] }
- default_value_for :helm_major_version, 3
+ attribute :helm_major_version, default: 3
validate :restrict_modification, on: :update
validate :no_groups, unless: :group_type?
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index 899529ff49f..935d6238dba 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -26,7 +26,7 @@ module Clusters
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
- default_value_for(:alert_manager_token) { SecureRandom.hex }
+ after_initialize :set_alert_manager_token, if: :new_record?
scope :enabled, -> { where(enabled: true) }
@@ -54,6 +54,10 @@ module Clusters
private
+ def set_alert_manager_token
+ self.alert_manager_token = SecureRandom.hex
+ end
+
def activate_project_integrations
::Clusters::Applications::ActivateIntegrationWorker
.perform_async(cluster_id, ::Integrations::Prometheus.to_param)
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 9d4f0a89403..165285b34b2 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -10,6 +10,7 @@ module Clusters
include NullifyIfBlank
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
+ REQUIRED_K8S_MIN_VERSION = 23
IGNORED_CONNECTION_EXCEPTIONS = [
Gitlab::UrlBlocker::BlockedUrlError,
@@ -21,6 +22,8 @@ module Clusters
OpenSSL::SSL::SSLError
].freeze
+ FailedVersionCheckError = Class.new(StandardError)
+
self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_work_type = :external_dependency
@@ -64,9 +67,7 @@ module Clusters
unknown_authorization: nil,
rbac: 1,
abac: 2
- }
-
- default_value_for :authorization_type, :rbac
+ }, _default: :rbac
nullify_if_blank :namespace
@@ -208,6 +209,29 @@ module Clusters
kubeclient.get_ingresses(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
+ rescue NoMethodError => e
+ # We get NoMethodError for Kubernetes versions < 1.19. Since we only support >= 1.23
+ # we will ignore this error for previous versions. For more details read:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/371249#note_1079866043
+ return [] if server_version < REQUIRED_K8S_MIN_VERSION
+
+ raise e
+ end
+
+ def server_version
+ full_url = Gitlab::UrlSanitizer.new("#{api_url}/version").full_url
+
+ # We can't use `kubeclient` to check the cluster version because it does not support it
+ # https://github.com/ManageIQ/kubeclient/issues/309
+ response = Gitlab::HTTP.perform_request(
+ Net::HTTP::Get, full_url,
+ headers: { "Authorization" => "Bearer #{token}" },
+ cert_store: kubeclient_ssl_options[:cert_store])
+
+ Gitlab::ErrorTracking.track_exception(FailedVersionCheckError.new) unless response.success?
+
+ json_response = Gitlab::Json.parse(response.body)
+ json_response["minor"].to_i
end
def build_kube_client!
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index af2eba42721..f0f56d9ebd9 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -12,9 +12,9 @@ module Clusters
belongs_to :cluster, inverse_of: :provider_aws, class_name: 'Clusters::Cluster'
- default_value_for :region, DEFAULT_REGION
- default_value_for :num_nodes, 3
- default_value_for :instance_type, 'm5.large'
+ attribute :region, default: DEFAULT_REGION
+ attribute :num_nodes, default: 3
+ attribute :instance_type, default: "m5.large"
attr_encrypted :secret_access_key,
mode: :per_attribute_iv,
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index 2ca7d0249dc..fde5ed592cb 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -9,10 +9,10 @@ module Clusters
belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
- default_value_for :zone, 'us-central1-a'
- default_value_for :num_nodes, 3
- default_value_for :machine_type, 'n1-standard-2'
- default_value_for :cloud_run, false
+ attribute :zone, default: 'us-central1-a'
+ attribute :num_nodes, default: 3
+ attribute :machine_type, default: 'n1-standard-2'
+ attribute :cloud_run, default: false
scope :cloud_run, -> { where(cloud_run: true) }
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
index a3ee8e4f364..7d89ddde0cb 100644
--- a/app/models/commit_collection.rb
+++ b/app/models/commit_collection.rb
@@ -13,10 +13,11 @@ class CommitCollection
# container - The object the commits belong to.
# commits - The Commit instances to store.
# ref - The name of the ref (e.g. "master").
- def initialize(container, commits, ref = nil)
+ def initialize(container, commits, ref = nil, page: nil, per_page: nil, count: nil)
@container = container
@commits = commits
@ref = ref
+ @pagination = Gitlab::PaginationDelegate.new(page: page, per_page: per_page, count: count)
end
def each(&block)
@@ -113,4 +114,8 @@ class CommitCollection
def method_missing(message, *args, &block)
commits.public_send(message, *args, &block)
end
+
+ def next_page
+ @pagination.next_page
+ end
end
diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb
index 1ce76b53da4..2ae59853520 100644
--- a/app/models/commit_signatures/gpg_signature.rb
+++ b/app/models/commit_signatures/gpg_signature.rb
@@ -49,5 +49,9 @@ module CommitSignatures
Gitlab::Gpg::Commit.new(commit)
end
+
+ def user
+ gpg_key&.user
+ end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 05a258e6e26..2470eada62e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -9,11 +9,9 @@ class CommitStatus < Ci::ApplicationRecord
include EnumWithNil
include BulkInsertableAssociations
include TaggableQueries
- include IgnorableColumns
self.table_name = 'ci_builds'
partitionable scope: :pipeline
- ignore_column :trace, remove_with: '15.6', remove_after: '2022-10-22'
belongs_to :user
belongs_to :project
@@ -23,7 +21,12 @@ class CommitStatus < Ci::ApplicationRecord
has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
+ attribute :retried, default: false
+
enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true
+ # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily
+ # extend this `Hash` with new values.
+ enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons
delegate :commit, to: :pipeline
delegate :sha, :short_sha, :before_sha, to: :pipeline
@@ -98,12 +101,6 @@ class CommitStatus < Ci::ApplicationRecord
merge(or_conditions)
end
- # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily
- # extend this `Hash` with new values.
- enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons
-
- default_value_for :retried, false
-
##
# We still create some CommitStatuses outside of CreatePipelineService.
#
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 910885c833f..9a04776f1c6 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -110,6 +110,10 @@ module Ci
COMPLETED_STATUSES.include?(status)
end
+ def incomplete?
+ COMPLETED_STATUSES.exclude?(status)
+ end
+
def blocked?
BLOCKED_STATUS.include?(status)
end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index ff884984099..d93f4a150d5 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -87,6 +87,16 @@ module Ci
ensure_metadata.id_tokens = value
end
+ def enqueue_immediately?
+ !!options[:enqueue_immediately]
+ end
+
+ def set_enqueue_immediately!
+ # ensures that even if `config_options: nil` in the database we set the
+ # new value correctly.
+ self.options = options.merge(enqueue_immediately: true)
+ end
+
private
def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index df803180e77..68a6714c892 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -57,9 +57,14 @@ module Ci
end
class_methods do
- private
+ def partitionable(scope:, through: nil)
+ if through
+ define_singleton_method(:routing_table_name) { through[:table] }
+ define_singleton_method(:routing_table_name_flag) { through[:flag] }
+
+ include Partitionable::Switch
+ end
- def partitionable(scope:)
define_method(:partition_scope_value) do
strong_memoize(:partition_scope_value) do
next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing?
diff --git a/app/models/concerns/ci/partitionable/switch.rb b/app/models/concerns/ci/partitionable/switch.rb
new file mode 100644
index 00000000000..c1bbd107e9f
--- /dev/null
+++ b/app/models/concerns/ci/partitionable/switch.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Ci
+ module Partitionable
+ module Switch
+ extend ActiveSupport::Concern
+
+ # These methods are cached at the class level and depend on the value
+ # of `table_name`, changing that value resets them.
+ # `cached_find_by_statement` is used to cache SQL statements which can
+ # include the table name.
+ #
+ SWAPABLE_METHODS = %i[table_name quoted_table_name arel_table
+ predicate_builder cached_find_by_statement].freeze
+
+ included do |base|
+ partitioned = Class.new(base) do
+ self.table_name = base.routing_table_name
+
+ def self.routing_class?
+ true
+ end
+ end
+
+ base.const_set(:Partitioned, partitioned)
+ end
+
+ class_methods do
+ def routing_class?
+ false
+ end
+
+ def routing_table_enabled?
+ return false if routing_class?
+
+ Gitlab::SafeRequestStore.fetch(routing_table_name_flag) do
+ ::Feature.enabled?(routing_table_name_flag)
+ end
+ end
+
+ # We're delegating them to the `Partitioned` model.
+ # They do not require any check override since they come from AR core
+ # (are always defined) and we're using `super` to get the value.
+ #
+ SWAPABLE_METHODS.each do |name|
+ define_method(name) do |*args, &block|
+ if routing_table_enabled?
+ self::Partitioned.public_send(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
+ else
+ super(*args, &block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ci/raw_variable.rb b/app/models/concerns/ci/raw_variable.rb
new file mode 100644
index 00000000000..5cfc781c9f1
--- /dev/null
+++ b/app/models/concerns/ci/raw_variable.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Ci
+ module RawVariable
+ extend ActiveSupport::Concern
+
+ included do
+ validates :raw, inclusion: { in: [true, false] }
+ end
+
+ private
+
+ def uncached_runner_variable
+ super.merge(raw: raw?)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/track_environment_usage.rb b/app/models/concerns/ci/track_environment_usage.rb
index 45d9cdeeb59..fe548c77590 100644
--- a/app/models/concerns/ci/track_environment_usage.rb
+++ b/app/models/concerns/ci/track_environment_usage.rb
@@ -17,7 +17,7 @@ module Ci
end
def verifies_environment?
- has_environment? && environment_action == 'verify'
+ has_environment_keyword? && environment_action == 'verify'
end
def count_user_deployment?
diff --git a/app/models/concerns/encrypted_user_password.rb b/app/models/concerns/encrypted_user_password.rb
new file mode 100644
index 00000000000..97e6592f442
--- /dev/null
+++ b/app/models/concerns/encrypted_user_password.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+# Support for both BCrypt and PBKDF2+SHA512 user passwords
+# Meant to be used exclusively with User model but extracted
+# to a concern for isolation and clarity.
+module EncryptedUserPassword
+ extend ActiveSupport::Concern
+
+ BCRYPT_PREFIX = '$2a$'
+ PBKDF2_SHA512_PREFIX = '$pbkdf2-sha512$'
+
+ BCRYPT_STRATEGY = :bcrypt
+ PBKDF2_SHA512_STRATEGY = :pbkdf2_sha512
+
+ # Use Devise DatabaseAuthenticatable#authenticatable_salt
+ # unless encrypted password is PBKDF2+SHA512.
+ def authenticatable_salt
+ return super unless pbkdf2_password?
+
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.split_digest(encrypted_password)[:salt]
+ end
+
+ # Called by Devise during database authentication.
+ # Also migrates the user password to the configured
+ # encryption type (BCrypt or PBKDF2+SHA512), if needed.
+ def valid_password?(password)
+ return false unless password_matches?(password)
+
+ migrate_password!(password)
+ end
+
+ def password=(new_password)
+ @password = new_password # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ return unless new_password.present?
+
+ self.encrypted_password = if Gitlab::FIPS.enabled?
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(
+ new_password,
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512::STRETCHES,
+ Devise.friendly_token[0, 16])
+ else
+ Devise::Encryptor.digest(self.class, new_password)
+ end
+ end
+
+ private
+
+ def password_strategy
+ return BCRYPT_STRATEGY if encrypted_password.starts_with?(BCRYPT_PREFIX)
+ return PBKDF2_SHA512_STRATEGY if encrypted_password.starts_with?(PBKDF2_SHA512_PREFIX)
+
+ :unknown
+ end
+
+ def pbkdf2_password?
+ password_strategy == PBKDF2_SHA512_STRATEGY
+ end
+
+ def bcrypt_password?
+ password_strategy == BCRYPT_STRATEGY
+ end
+
+ def password_matches?(password)
+ if bcrypt_password?
+ Devise::Encryptor.compare(self.class, encrypted_password, password)
+ elsif pbkdf2_password?
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.compare(encrypted_password, password)
+ end
+ end
+
+ def migrate_password!(password)
+ return true if password_strategy == encryptor
+
+ update_attribute(:password, password)
+ end
+
+ def encryptor
+ return BCRYPT_STRATEGY unless Gitlab::FIPS.enabled?
+
+ PBKDF2_SHA512_STRATEGY
+ end
+end
diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb
index 518efa669ad..8848c0c5555 100644
--- a/app/models/concerns/enums/sbom.rb
+++ b/app/models/concerns/enums/sbom.rb
@@ -6,8 +6,23 @@ module Enums
library: 0
}.with_indifferent_access.freeze
+ PURL_TYPES = {
+ composer: 1, # refered to as `packagist` in gemnasium-db
+ conan: 2,
+ gem: 3,
+ golang: 4, # refered to as `go` in gemnasium-db
+ maven: 5,
+ npm: 6,
+ nuget: 7,
+ pypi: 8
+ }.with_indifferent_access.freeze
+
def self.component_types
COMPONENT_TYPES
end
+
+ def self.purl_types
+ PURL_TYPES
+ end
end
end
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
index f1ac734635d..4d267dc69d0 100644
--- a/app/models/concerns/file_store_mounter.rb
+++ b/app/models/concerns/file_store_mounter.rb
@@ -1,31 +1,35 @@
# frozen_string_literal: true
module FileStoreMounter
+ ALLOWED_FILE_FIELDS = %i[file signed_file].freeze
+
extend ActiveSupport::Concern
class_methods do
- # When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!`
- def mount_file_store_uploader(uploader, skip_store_file: false)
- mount_uploader(:file, uploader)
+ # When `skip_store_file: true` is used, the model MUST explicitly call `store_#{file_field}_now!`
+ def mount_file_store_uploader(uploader, skip_store_file: false, file_field: :file)
+ raise ArgumentError, "file_field not allowed: #{file_field}" unless ALLOWED_FILE_FIELDS.include?(file_field)
+
+ mount_uploader(file_field, uploader)
+
+ define_method("update_#{file_field}_store") do
+ # The file.object_store is set during `uploader.store!` and `uploader.migrate!`
+ update_column("#{file_field}_store", public_send(file_field).object_store) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ define_method("store_#{file_field}_now!") do
+ public_send("store_#{file_field}!") # rubocop:disable GitlabSecurity/PublicSend
+ public_send("update_#{file_field}_store") # rubocop:disable GitlabSecurity/PublicSend
+ end
if skip_store_file
- skip_callback :save, :after, :store_file!
+ skip_callback :save, :after, "store_#{file_field}!".to_sym
return
end
# This hook is a no-op when the file is uploaded after_commit
- after_save :update_file_store, if: :saved_change_to_file?
+ after_save "update_#{file_field}_store".to_sym, if: "saved_change_to_#{file_field}?".to_sym
end
end
-
- def update_file_store
- # The file.object_store is set during `uploader.store!` and `uploader.migrate!`
- update_column(:file_store, file.object_store)
- end
-
- def store_file_now!
- store_file!
- update_file_store
- end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f8389865f91..31b2a8d7cc1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -217,6 +217,10 @@ module Issuable
false
end
+ def supports_confidentiality?
+ false
+ end
+
def severity
return IssuableSeverity::DEFAULT unless supports_severity?
@@ -236,7 +240,6 @@ module Issuable
end
def validate_assignee_size_length
- return true unless Feature.enabled?(:limit_assignees_per_issuable)
return true unless assignees.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
errors.add :assignees,
@@ -460,18 +463,6 @@ module Issuable
end
end
- def today?
- Date.today == created_at.to_date
- end
-
- def created_hours_ago
- (Time.now.utc.to_i - created_at.utc.to_i) / 3600
- end
-
- def new?
- created_hours_ago < 24
- end
-
def open?
opened?
end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index 14c54d99ef3..a95bed7ad42 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -18,7 +18,7 @@ module Milestoneable
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) }
scope :any_release, -> { joins_milestone_releases }
- scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
+ scope :with_release, -> (tag, project_id) { joins_milestone_releases.where(milestones: { releases: { tag: tag, project_id: project_id } }) }
scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
diff --git a/app/models/concerns/mirror_authentication.rb b/app/models/concerns/mirror_authentication.rb
index 14c8be93ce0..e3bfeaf7f95 100644
--- a/app/models/concerns/mirror_authentication.rb
+++ b/app/models/concerns/mirror_authentication.rb
@@ -11,7 +11,7 @@ module MirrorAuthentication
# We should generate a key even if there's no SSH URL present
before_validation :generate_ssh_private_key!, if: -> {
- regenerate_ssh_private_key || ( auth_method == 'ssh_public_key' && ssh_private_key.blank? )
+ regenerate_ssh_private_key || (auth_method == 'ssh_public_key' && ssh_private_key.blank?)
}
credentials_field :auth_method, reader: false
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index c1aac235d33..492d55c74e2 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -210,11 +210,23 @@ module Noteable
# Synthetic system notes don't have discussion IDs because these are generated dynamically
# in Ruby. These are always root notes anyway so we don't need to group by discussion ID.
def synthetic_note_ids_relations
- [
- resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at),
- resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at),
- resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
- ]
+ relations = []
+
+ # currently multiple models include Noteable concern, but not all of them support
+ # all resource events, so we check if given model supports given resource event.
+ if respond_to?(:resource_label_events)
+ relations << resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at)
+ end
+
+ if respond_to?(:resource_state_events)
+ relations << resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
+ end
+
+ if respond_to?(:resource_milestone_events)
+ relations << resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at)
+ end
+
+ relations
end
end
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 1520ec0828e..75fd45d13a9 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -85,8 +85,7 @@ module Packages
scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) }
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
- mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader
- after_save :update_signed_file_store, if: :saved_change_to_signed_file?
+ mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader, file_field: :signed_file
def component_names
components.pluck(:name).sort
@@ -119,12 +118,6 @@ module Packages
self.class.with_container(container).with_codename(suite).exists?
end
-
- def update_signed_file_store
- # The signed_file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:signed_file_store, signed_file.object_store)
- end
end
end
end
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
index 335fcec2611..562c8cf23f3 100644
--- a/app/models/concerns/pg_full_text_searchable.rb
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -25,6 +25,7 @@ module PgFullTextSearchable
TSVECTOR_MAX_LENGTH = 1.megabyte.freeze
TEXT_SEARCH_DICTIONARY = 'english'
URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}.freeze
+ TSQUERY_DISALLOWED_CHARACTERS_REGEX = %r{[^a-zA-Z0-9 .@/\-_"]}.freeze
def update_search_data!
tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight|
@@ -102,21 +103,16 @@ module PgFullTextSearchable
end
end
- def pg_full_text_search(search_term)
+ def pg_full_text_search(query, matched_columns: [])
search_data_table = reflect_on_association(:search_data).klass.arel_table
- # This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920
- search_term = remove_url_scheme(search_term)
- search_term = ActiveSupport::Inflector.transliterate(search_term)
-
joins(:search_data).where(
Arel::Nodes::InfixOperation.new(
'@@',
search_data_table[:search_vector],
Arel::Nodes::NamedFunction.new(
- 'websearch_to_tsquery',
- [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(search_term)]
+ 'to_tsquery',
+ [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), build_tsquery(query, matched_columns)]
)
)
)
@@ -124,8 +120,39 @@ module PgFullTextSearchable
private
- def remove_url_scheme(search_term)
- search_term.gsub(URL_SCHEME_REGEX, '')
+ def build_tsquery(query, matched_columns)
+ # URLs get broken up into separate words when : is removed below, so we just remove the whole scheme.
+ query = remove_url_scheme(query)
+ # Remove accents from search term to match indexed data
+ query = ActiveSupport::Inflector.transliterate(query)
+ # Prevent users from using tsquery operators that can cause syntax errors.
+ query = filter_allowed_characters(query)
+
+ weights = matched_columns.map do |column_name|
+ pg_full_text_searchable_columns[column_name]
+ end.compact.join
+ prefix_search_suffix = ":*#{weights}"
+
+ tsquery = Gitlab::SQL::Pattern.split_query_to_search_terms(query).map do |search_term|
+ case search_term
+ when /\A\d+\z/ # Handles https://gitlab.com/gitlab-org/gitlab/-/issues/375337
+ "(#{search_term + prefix_search_suffix} | -#{search_term + prefix_search_suffix})"
+ when /\s/
+ search_term.split.map { |t| "#{t}:#{weights}" }.join(' <-> ')
+ else
+ search_term + prefix_search_suffix
+ end
+ end.join(' & ')
+
+ Arel::Nodes.build_quoted(tsquery)
+ end
+
+ def remove_url_scheme(query)
+ query.gsub(URL_SCHEME_REGEX, '')
+ end
+
+ def filter_allowed_characters(query)
+ query.gsub(TSQUERY_DISALLOWED_CHARACTERS_REGEX, ' ')
end
end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 2976b6f02a7..d37f20e2e7c 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -110,6 +110,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:releases_access_level, value)
end
+ def infrastructure_access_level=(value)
+ write_feature_attribute_string(:infrastructure_access_level, value)
+ end
+
# TODO: Remove this method after we drop support for project create/edit APIs to set the
# container_registry_enabled attribute. They can instead set the container_registry_access_level
# attribute.
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index ec56f4a32af..7e1ebd1eba3 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -7,7 +7,6 @@ module ProtectedRef
belongs_to :project, touch: true
validates :name, presence: true
- validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 618ad96905d..facf0808e7a 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -21,8 +21,8 @@ module ProtectedRefAccess
included do
scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
- scope :by_user, -> (user) { where(user_id: user ) }
- scope :by_group, -> (group) { where(group_id: group ) }
+ scope :by_user, -> (user) { where(user_id: user) }
+ scope :by_group, -> (group) { where(group_id: group) }
scope :for_role, -> { where(user_id: nil, group_id: nil) }
scope :for_user, -> { where.not(user_id: nil) }
scope :for_group, -> { where.not(group_id: nil) }
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index 2d4ed51ce3b..f1d29ad5a90 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -26,8 +26,8 @@ module RedisCacheable
end
def cache_attributes(values)
- Gitlab::Redis::Cache.with do |redis|
- redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
+ with_redis do |redis|
+ redis.set(cache_attribute_key, Gitlab::Json.dump(values), ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
end
clear_memoization(:cached_attributes)
@@ -41,13 +41,17 @@ module RedisCacheable
def cached_attributes
strong_memoize(:cached_attributes) do
- Gitlab::Redis::Cache.with do |redis|
+ with_redis do |redis|
data = redis.get(cache_attribute_key)
Gitlab::Json.parse(data, symbolize_names: true) if data
end
end
end
+ def with_redis(&block)
+ Gitlab::Redis::Cache.with(&block) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
def cast_value_from_cache(attribute, value)
self.class.type_for_attribute(attribute.to_s).cast(value)
end
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index b7fd52ab305..87ff413f2c1 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -19,9 +19,7 @@ module RepositoryStorageMovable
inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
validate :container_repository_writable, on: :create
- default_value_for(:destination_storage_name, allows_nil: false) do
- Repository.pick_storage_shard
- end
+ attribute :destination_storage_name, default: -> { Repository.pick_storage_shard }
state_machine initial: :initial do
event :schedule do
diff --git a/app/models/concerns/subquery.rb b/app/models/concerns/subquery.rb
new file mode 100644
index 00000000000..ae92d2137c1
--- /dev/null
+++ b/app/models/concerns/subquery.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Distinguish between a top level query and a subselect.
+#
+# Retrieve column values when the relation has already been loaded, otherwise reselect the relation.
+# Useful for preload query patterns where the typical Rails #preload does not fit. Such as:
+#
+# projects = Project.where(...)
+# projects.load
+# ...
+# options[members] = ProjectMember.where(...).where(source_id: projects.select(:id))
+module Subquery
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def subquery(*column_names, max_limit: 5_000)
+ if current_scope.loaded? && current_scope.size <= max_limit
+ current_scope.pluck(*column_names)
+ else
+ current_scope.reselect(*column_names)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb
index 1c2147beedd..d09ce4873b1 100644
--- a/app/models/concerns/ttl_expirable.rb
+++ b/app/models/concerns/ttl_expirable.rb
@@ -4,8 +4,8 @@ module TtlExpirable
extend ActiveSupport::Concern
included do
+ attribute :read_at, default: -> { Time.zone.now }
validates :status, presence: true
- default_value_for :read_at, Time.zone.now
enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 }
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 14520b2da26..7da4e31b472 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -6,6 +6,7 @@ class ContainerRepository < ApplicationRecord
include EachBatch
include Sortable
include AfterCommitQueue
+ include Packages::Destructible
WAITING_CLEANUP_STATUSES = %i[cleanup_scheduled cleanup_unfinished].freeze
REQUIRING_CLEANUP_STATUSES = %i[cleanup_unscheduled cleanup_scheduled].freeze
@@ -34,7 +35,7 @@ class ContainerRepository < ApplicationRecord
numericality: { greater_than_or_equal_to: 0 },
allow_nil: false
- enum status: { delete_scheduled: 0, delete_failed: 1 }
+ enum status: { delete_scheduled: 0, delete_failed: 1, delete_ongoing: 2 }
enum expiration_policy_cleanup_status: { cleanup_unscheduled: 0, cleanup_scheduled: 1, cleanup_unfinished: 2, cleanup_ongoing: 3 }
enum migration_skipped_reason: {
@@ -69,6 +70,7 @@ class ContainerRepository < ApplicationRecord
scope :with_migration_pre_import_started_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_pre_import_started_at, '01-01-1970') < ?", timestamp) }
scope :with_migration_pre_import_done_at_nil_or_before, ->(timestamp) { where("COALESCE(migration_pre_import_done_at, '01-01-1970') < ?", timestamp) }
scope :with_stale_ongoing_cleanup, ->(threshold) { cleanup_ongoing.where('expiration_policy_started_at < ?', threshold) }
+ scope :with_stale_delete_at, ->(threshold) { where('delete_started_at < ?', threshold) }
scope :import_in_process, -> { where(migration_state: %w[pre_importing pre_import_done importing]) }
scope :recently_done_migration_step, -> do
@@ -224,6 +226,13 @@ class ContainerRepository < ApplicationRecord
end
end
+ # Container Repository model and the code that makes API calls
+ # are tied. Sometimes (mainly in Geo) we need to work with Registry
+ # when Container Repository record doesn't even exist.
+ # The ability to create a not-persisted record with a certain "path" parameter
+ # is very useful
+ attr_writer :path
+
def self.exists_by_path?(path)
where(
project: path.repository_project,
@@ -278,6 +287,10 @@ class ContainerRepository < ApplicationRecord
all
end
+ class << self
+ alias_method :pending_destruction, :delete_scheduled # needed by Packages::Destructible
+ end
+
def skip_import(reason:)
self.migration_skipped_reason = reason
@@ -507,6 +520,14 @@ class ContainerRepository < ApplicationRecord
end
end
+ def set_delete_ongoing_status
+ update_columns(status: :delete_ongoing, delete_started_at: Time.zone.now)
+ end
+
+ def set_delete_scheduled_status
+ update_columns(status: :delete_scheduled, delete_started_at: nil)
+ end
+
def migration_in_active_state?
migration_state.in?(ACTIVE_MIGRATION_STATES)
end
diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb
index 5538e93a39e..9b9c0822f63 100644
--- a/app/models/cycle_analytics/project_level_stage_adapter.rb
+++ b/app/models/cycle_analytics/project_level_stage_adapter.rb
@@ -4,7 +4,7 @@
# compatible with the old value stream controller actions.
module CycleAnalytics
class ProjectLevelStageAdapter
- ProjectLevelStage = Struct.new(:title, :description, :legend, :name, :project_median, keyword_init: true )
+ ProjectLevelStage = Struct.new(:title, :description, :legend, :name, :project_median, keyword_init: true)
def initialize(stage, options)
@stage = stage
diff --git a/app/models/dependency_proxy/group_setting.rb b/app/models/dependency_proxy/group_setting.rb
index bcf09b27129..3a7ae66a263 100644
--- a/app/models/dependency_proxy/group_setting.rb
+++ b/app/models/dependency_proxy/group_setting.rb
@@ -3,7 +3,7 @@
class DependencyProxy::GroupSetting < ApplicationRecord
belongs_to :group
- validates :group, presence: true
+ attribute :enabled, default: true
- default_value_for :enabled, true
+ validates :group, presence: true
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 20d19ec9541..66d1ce01814 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -13,7 +13,7 @@ class DeployToken < ApplicationRecord
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze
- default_value_for(:expires_at) { Forever.date }
+ attribute :expires_at, default: -> { Forever.date }
# Do NOT use this `user` for the authentication/authorization of the deploy tokens.
# It's for the auditing purpose on Credential Inventory, only.
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 20841bc14cd..ea92b978d3a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -306,8 +306,8 @@ class Deployment < ApplicationRecord
last_deployment_id = environment.last_deployment&.id
return false unless last_deployment_id.present?
-
return false if self.id == last_deployment_id
+ return false if self.sha == environment.last_deployment&.sha
self.id < last_deployment_id
end
@@ -439,8 +439,9 @@ class Deployment < ApplicationRecord
end
# default tag limit is 100, 0 means no limit
+ # when refs_by_oid is passed an SHA, returns refs for that commit
def tags(limit: 100)
- project.repository.tag_names_contains(sha, limit: limit)
+ project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || []
end
strong_memoize_attr :tags
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index f4d665cf279..041ec98ffc9 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -37,18 +37,18 @@ class DiffDiscussion < Discussion
def reply_attributes
super.merge(
- original_position: original_position.to_json,
- position: position.to_json
+ original_position: Gitlab::Json.dump(original_position),
+ position: Gitlab::Json.dump(position)
)
end
def cache_key
- positions_json = diff_note_positions.map { |dnp| dnp.position.to_json }
+ positions_json = diff_note_positions.map { |dnp| Gitlab::Json.dump(dnp.position) }
positions_sha = Digest::SHA1.hexdigest(positions_json.join(':')) if positions_json.any?
[
super,
- Digest::SHA1.hexdigest(position.to_json),
+ Digest::SHA1.hexdigest(Gitlab::Json.dump(position)),
positions_sha
].join(':')
end
diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb
index a1defb2594f..fb127de2bc7 100644
--- a/app/models/diff_viewer/server_side.rb
+++ b/app/models/diff_viewer/server_side.rb
@@ -9,14 +9,6 @@ module DiffViewer
self.size_limit = 5.megabytes
end
- def prepare!
- return if Feature.enabled?(:disable_load_entire_blob_for_diff_viewer, diff_file.repository.project)
-
- # TODO: remove this after resolving #342703
- diff_file.old_blob&.load_all_data!
- diff_file.new_blob&.load_all_data!
- end
-
def render_error
# Files that are not stored in the repository, like LFS files and
# build artifacts, can only be rendered using a client-side viewer,
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 12d73ef0d72..1c7a8d93e6e 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -104,18 +104,9 @@ module ErrorTracking
api_host
end
- def sentry_response_limit_enabled?
- Feature.enabled?(:error_tracking_sentry_limit, project)
- end
-
- def reactive_cache_limit_enabled?
- sentry_response_limit_enabled?
- end
-
def sentry_client
strong_memoize(:sentry_client) do
- ::ErrorTracking::SentryClient
- .new(api_url, token, validate_size_guarded_by_feature_flag: sentry_response_limit_enabled?)
+ ::ErrorTracking::SentryClient.new(api_url, token)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 4c1793d3f13..a1417db3410 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -358,7 +358,7 @@ class Event < ApplicationRecord
# hence we add the extra WHERE clause for last_activity_at.
Project.unscoped.where(id: project_id)
.where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago)
- .touch_all(:last_activity_at, time: created_at) # rubocop: disable Rails/SkipsModelValidations
+ .touch_all(:last_activity_at, time: created_at)
Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset
end
@@ -441,7 +441,7 @@ class Event < ApplicationRecord
def set_last_repository_updated_at
Project.unscoped.where(id: project_id)
.where("last_repository_updated_at < ? OR last_repository_updated_at IS NULL", REPOSITORY_UPDATED_AT_INTERVAL.ago)
- .touch_all(:last_repository_updated_at, time: created_at) # rubocop: disable Rails/SkipsModelValidations
+ .touch_all(:last_repository_updated_at, time: created_at)
end
def design_action_names
diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb
index 4258027aa56..72e1d28a297 100644
--- a/app/models/event_collection.rb
+++ b/app/models/event_collection.rb
@@ -62,21 +62,12 @@ class EventCollection
end
def in_operator_optimized_relation(parent_column, parents, parent_model)
- query_builder_params = if Feature.enabled?(:optimized_project_and_group_activity_queries)
- array_data = {
- scope_ids: parents.pluck(:id),
- scope_model: parent_model,
- mapping_column: parent_column
- }
- filter.in_operator_query_builder_params(array_data)
- else
- {
- scope: filtered_events,
- array_scope: parents.select(:id),
- array_mapping_scope: -> (parent_id_expression) { Event.where(Event.arel_table[parent_column].eq(parent_id_expression)).reorder(id: :desc) },
- finder_query: -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }
- }
- end
+ array_data = {
+ scope_ids: parents.pluck(:id),
+ scope_model: parent_model,
+ mapping_column: parent_column
+ }
+ query_builder_params = filter.in_operator_query_builder_params(array_data)
Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
.new(**query_builder_params)
@@ -84,10 +75,6 @@ class EventCollection
.limit(@limit + @offset)
end
- def filtered_events
- filter.apply_filter(base_relation)
- end
-
def paginate_events(events)
events.limit(@limit).offset(@offset)
end
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
deleted file mode 100644
index 2300ec2996d..00000000000
--- a/app/models/experiment.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-class Experiment < ApplicationRecord
- has_many :experiment_users
- has_many :experiment_subjects, inverse_of: :experiment
-
- validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
-
- def self.add_user(name, group_type, user, context = {})
- by_name(name).record_user_and_group(user, group_type, context)
- end
-
- def self.add_group(name, variant:, group:)
- add_subject(name, variant: variant, subject: group)
- end
-
- def self.add_subject(name, variant:, subject:)
- by_name(name).record_subject_and_variant!(subject, variant)
- end
-
- def self.record_conversion_event(name, user, context = {})
- by_name(name).record_conversion_event_for_user(user, context)
- end
-
- def self.by_name(name)
- find_or_create_by!(name: name)
- end
-
- # Create or update the recorded experiment_user row for the user in this experiment.
- def record_user_and_group(user, group_type, context = {})
- experiment_user = experiment_users.find_or_initialize_by(user: user)
- experiment_user.assign_attributes(group_type: group_type, context: merged_context(experiment_user, context))
- # We only call save when necessary because this causes the request to stick to the primary DB
- # even when the save is a no-op
- # https://gitlab.com/gitlab-org/gitlab/-/issues/324649
- experiment_user.save! if experiment_user.changed?
-
- experiment_user
- end
-
- def record_conversion_event_for_user(user, context = {})
- experiment_user = experiment_users.find_by(user: user)
- return unless experiment_user
-
- experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context))
- end
-
- def record_conversion_event_for_subject(subject, context = {})
- raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
-
- attr_name = subject.class.table_name.singularize.to_sym
- experiment_subject = experiment_subjects.find_by(attr_name => subject)
- return unless experiment_subject
-
- experiment_subject.update!(converted_at: Time.current, context: merged_context(experiment_subject, context))
- end
-
- def record_subject_and_variant!(subject, variant)
- raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
-
- attr_name = subject.class.table_name.singularize.to_sym
- experiment_subject = experiment_subjects.find_or_initialize_by(attr_name => subject)
- experiment_subject.assign_attributes(variant: variant)
- # We only call save when necessary because this causes the request to stick to the primary DB
- # even when the save is a no-op
- # https://gitlab.com/gitlab-org/gitlab/-/issues/324649
- experiment_subject.save! if experiment_subject.changed?
-
- experiment_subject
- end
-
- private
-
- def merged_context(experiment_subject, new_context)
- experiment_subject.context.deep_merge(new_context.deep_stringify_keys)
- end
-end
diff --git a/app/models/experiment_subject.rb b/app/models/experiment_subject.rb
deleted file mode 100644
index 2a7b9017a51..00000000000
--- a/app/models/experiment_subject.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-class ExperimentSubject < ApplicationRecord
- include ::Gitlab::Experimentation::GroupTypes
-
- belongs_to :experiment, inverse_of: :experiment_subjects
- belongs_to :user
- belongs_to :namespace
- belongs_to :project
-
- validates :experiment, presence: true
- validates :variant, presence: true
- validate :must_have_one_subject_present
-
- enum variant: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 }
-
- def self.valid_subject?(subject)
- subject.is_a?(Namespace) || subject.is_a?(User) || subject.is_a?(Project)
- end
-
- private
-
- def must_have_one_subject_present
- if non_nil_subjects.length != 1
- errors.add(:base, s_("ExperimentSubject|Must have exactly one of User, Namespace, or Project."))
- end
- end
-
- def non_nil_subjects
- @non_nil_subjects ||= [user, namespace, project].reject(&:blank?)
- end
-end
diff --git a/app/models/experiment_user.rb b/app/models/experiment_user.rb
deleted file mode 100644
index e447becc1bd..00000000000
--- a/app/models/experiment_user.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class ExperimentUser < ApplicationRecord
- include ::Gitlab::Experimentation::GroupTypes
-
- belongs_to :experiment
- belongs_to :user
-
- enum group_type: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 }
-
- validates :experiment_id, presence: true
- validates :user_id, presence: true
- validates :group_type, presence: true
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index 38623d91705..098116ed800 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -119,6 +119,8 @@ class Group < Namespace
has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id
+ has_many :protected_branches, inverse_of: :group
+
has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting'
delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, :setup_for_company, :jobs_to_be_done, to: :namespace_settings
@@ -465,9 +467,10 @@ class Group < Namespace
end
# Check if user is a last owner of the group.
+ # Excludes non-direct owners for top-level group
# Excludes project_bots
def last_owner?(user)
- has_owner?(user) && all_owners_excluding_project_bots.size == 1
+ has_owner?(user) && member_owners_excluding_project_bots.size == 1
end
def member_last_owner?(member)
@@ -476,8 +479,14 @@ class Group < Namespace
last_owner?(member.user)
end
- def all_owners_excluding_project_bots
- members_with_parents.owners.merge(User.without_project_bot)
+ # Excludes non-direct owners for top-level group
+ # Excludes project_bots
+ def member_owners_excluding_project_bots
+ if root?
+ members
+ else
+ members_with_parents
+ end.owners.merge(User.without_project_bot)
end
def single_blocked_owner?
@@ -487,7 +496,7 @@ class Group < Namespace
def member_last_blocked_owner?(member)
return member.last_blocked_owner unless member.last_blocked_owner.nil?
- return false if members_with_parents.owners.any?
+ return false if member_owners_excluding_project_bots.any?
single_blocked_owner? && blocked_owners.exists?(user_id: member.user)
end
@@ -1010,10 +1019,6 @@ class Group < Namespace
Arel::Nodes::SqlLiteral.new(column_alias))
end
- def self.groups_including_descendants_by(group_ids)
- Group.where(id: group_ids).self_and_descendants
- end
-
def disable_shared_runners!
update!(
shared_runners_enabled: false,
diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb
index 283e2d680f4..cdcfd3f3ff5 100644
--- a/app/models/hooks/active_hook_filter.rb
+++ b/app/models/hooks/active_hook_filter.rb
@@ -3,14 +3,36 @@
class ActiveHookFilter
def initialize(hook)
@hook = hook
- @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter)
end
def matches?(hooks_scope, data)
- return true if hooks_scope != :push_hooks
+ return true unless hooks_scope == :push_hooks
+
+ matches_branch?(data)
+ end
+
+ private
+
+ def matches_branch?(data)
return true if @hook.push_events_branch_filter.blank?
branch_name = Gitlab::Git.branch_name(data[:ref])
- @push_events_filter_matcher.matches?(branch_name)
+
+ if Feature.disabled?(:enhanced_webhook_support_regex)
+ return RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name)
+ end
+
+ case @hook.branch_filter_strategy
+ when 'all_branches'
+ true
+ when 'wildcard'
+ RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name)
+ when 'regex'
+ begin
+ Gitlab::UntrustedRegexp.new(@hook.push_events_branch_filter) === branch_name
+ rescue RegexpError
+ false
+ end
+ end
end
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index c0073f9a9b8..3c7f0ef9ffc 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -10,9 +10,9 @@ class SystemHook < WebHook
:merge_request_hooks
]
- default_value_for :push_events, false
- default_value_for :repository_update_events, true
- default_value_for :merge_requests_events, false
+ attribute :push_events, default: false
+ attribute :repository_update_events, default: true
+ attribute :merge_requests_events, default: false
validates :url, system_hook_url: true
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 71794964c99..05e50c17988 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -7,9 +7,11 @@ class WebHook < ApplicationRecord
MAX_FAILURES = 100
FAILURE_THRESHOLD = 3 # three strikes
+ EXCEEDED_FAILURE_THRESHOLD = FAILURE_THRESHOLD + 1
INITIAL_BACKOFF = 1.minute
MAX_BACKOFF = 1.day
BACKOFF_GROWTH_FACTOR = 2.0
+ SECRET_MASK = '************'
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -33,14 +35,26 @@ class WebHook < ApplicationRecord
has_many :web_hook_logs
validates :url, presence: true
- validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) }
+ validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) || hook.url_variables? }
validates :token, format: { without: /\n/ }
- validates :push_events_branch_filter, branch_filter: true
+ after_initialize :initialize_url_variables
+ before_validation :set_branch_filter_nil, \
+ if: -> { branch_filter_strategy_all_branches? && enhanced_webhook_support_regex? }
+ validates :push_events_branch_filter, \
+ untrusted_regexp: true, if: -> { branch_filter_strategy_regex? && enhanced_webhook_support_regex? }
+ validates :push_events_branch_filter, \
+ "web_hooks/wildcard_branch_filter": true, if: -> { branch_filter_strategy_wildcard? }
+
validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' }
validate :no_missing_url_variables
+ validates :interpolated_url, public_url: true, if: ->(hook) { hook.url_variables? && hook.errors.empty? }
- after_initialize :initialize_url_variables
+ enum branch_filter_strategy: {
+ wildcard: 0,
+ regex: 1,
+ all_branches: 2
+ }, _prefix: true
scope :executable, -> do
next all unless Feature.enabled?(:web_hooks_disable_failed)
@@ -108,7 +122,7 @@ class WebHook < ApplicationRecord
def disable!
return if permanently_disabled?
- update_attribute(:recent_failures, FAILURE_THRESHOLD + 1)
+ update_attribute(:recent_failures, EXCEEDED_FAILURE_THRESHOLD)
end
def enable!
@@ -123,10 +137,10 @@ class WebHook < ApplicationRecord
def backoff!
return if permanently_disabled? || (backoff_count >= MAX_FAILURES && temporarily_disabled?)
- attrs = { recent_failures: recent_failures + 1 }
+ attrs = { recent_failures: next_failure_count }
if recent_failures >= FAILURE_THRESHOLD
- attrs[:backoff_count] = backoff_count.succ.clamp(1, MAX_FAILURES)
+ attrs[:backoff_count] = next_backoff_count
attrs[:disabled_until] = next_backoff.from_now
end
@@ -137,7 +151,7 @@ class WebHook < ApplicationRecord
def failed!
return unless recent_failures < MAX_FAILURES
- assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: recent_failures + 1)
+ assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count)
save(validate: false)
end
@@ -198,8 +212,20 @@ class WebHook < ApplicationRecord
# Overridden in child classes.
end
+ def masked_token
+ token.present? ? SECRET_MASK : nil
+ end
+
private
+ def next_failure_count
+ recent_failures.succ.clamp(1, MAX_FAILURES)
+ end
+
+ def next_backoff_count
+ backoff_count.succ.clamp(1, MAX_FAILURES)
+ end
+
def web_hooks_disable_failed?
self.class.web_hooks_disable_failed?(self)
end
@@ -224,4 +250,12 @@ class WebHook < ApplicationRecord
errors.add(:url, "Invalid URL template. Missing keys: #{missing}")
end
+
+ def enhanced_webhook_support_regex?
+ Feature.enabled?(:enhanced_webhook_support_regex)
+ end
+
+ def set_branch_filter_nil
+ self.push_events_branch_filter = nil
+ end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index c32957fbef9..2b26147b494 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -56,7 +56,7 @@ class WebHookLog < ApplicationRecord
def redact_user_emails
self.request_data.deep_transform_values! do |value|
- value =~ URI::MailTo::EMAIL_REGEXP ? _('[REDACTED]') : value
+ value.to_s =~ URI::MailTo::EMAIL_REGEXP ? _('[REDACTED]') : value
end
end
end
diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb
index 735d4e4298c..e70209f1538 100644
--- a/app/models/incident_management/timeline_event.rb
+++ b/app/models/incident_management/timeline_event.rb
@@ -18,6 +18,8 @@ module IncidentManagement
validates :project, :incident, :occurred_at, presence: true
validates :action, presence: true, length: { maximum: 128 }
+ # `user_input` is a note filled in by a user via API. Not auto generated by GitLab
+ validates :note, presence: true, length: { maximum: 280 }, on: :user_input
validates :note, presence: true, length: { maximum: 10_000 }
validates :note_html, length: { maximum: 10_000 }
diff --git a/app/models/incident_management/timeline_event_tag.rb b/app/models/incident_management/timeline_event_tag.rb
index cde3afcaa16..d1e3fbc2a6a 100644
--- a/app/models/incident_management/timeline_event_tag.rb
+++ b/app/models/incident_management/timeline_event_tag.rb
@@ -4,6 +4,9 @@ module IncidentManagement
class TimelineEventTag < ApplicationRecord
self.table_name = 'incident_management_timeline_event_tags'
+ START_TIME_TAG_NAME = 'Start time'
+ END_TIME_TAG_NAME = 'End time'
+
belongs_to :project, inverse_of: :incident_management_timeline_event_tags
has_many :timeline_event_tag_links,
@@ -14,7 +17,13 @@ module IncidentManagement
through: :timeline_event_tag_links
validates :name, presence: true, format: { with: /\A[^,]+\z/ }
- validates :name, uniqueness: { scope: :project_id }
+ validates :name, uniqueness: { scope: :project_id, case_sensitive: false }
validates :name, length: { maximum: 255 }
+
+ scope :by_names, -> (tag_names) { where('lower(name) in (?)', tag_names.map(&:downcase)) }
+
+ def self.pluck_names
+ pluck(:name)
+ end
end
end
diff --git a/app/models/instance_metadata.rb b/app/models/instance_metadata.rb
index 6cac78178e0..47460c85671 100644
--- a/app/models/instance_metadata.rb
+++ b/app/models/instance_metadata.rb
@@ -1,11 +1,12 @@
# frozen_string_literal: true
class InstanceMetadata
- attr_reader :version, :revision, :kas
+ attr_reader :version, :revision, :kas, :enterprise
- def initialize(version: Gitlab::VERSION, revision: Gitlab.revision)
+ def initialize(version: Gitlab::VERSION, revision: Gitlab.revision, enterprise: Gitlab.ee?)
@version = version
@revision = revision
@kas = ::InstanceMetadata::Kas.new
+ @enterprise = enterprise
end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 23688a87cbd..41278dce22d 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -71,20 +71,20 @@ class Integration < ApplicationRecord
alias_attribute :type, :type_new
- default_value_for :active, false
- default_value_for :alert_events, true
- default_value_for :category, 'common'
- default_value_for :commit_events, true
- default_value_for :confidential_issues_events, true
- default_value_for :confidential_note_events, true
- default_value_for :issues_events, true
- default_value_for :job_events, true
- default_value_for :merge_requests_events, true
- default_value_for :note_events, true
- default_value_for :pipeline_events, true
- default_value_for :push_events, true
- default_value_for :tag_push_events, true
- default_value_for :wiki_page_events, true
+ attribute :active, default: false
+ attribute :alert_events, default: true
+ attribute :category, default: 'common'
+ attribute :commit_events, default: true
+ attribute :confidential_issues_events, default: true
+ attribute :confidential_note_events, default: true
+ attribute :issues_events, default: true
+ attribute :job_events, default: true
+ attribute :merge_requests_events, default: true
+ attribute :note_events, default: true
+ attribute :pipeline_events, default: true
+ attribute :push_events, default: true
+ attribute :tag_push_events, default: true
+ attribute :wiki_page_events, default: true
after_initialize :initialize_properties
@@ -589,6 +589,10 @@ class Integration < ApplicationRecord
false
end
+ def chat?
+ category == :chat
+ end
+
private
# Ancestors sorted by hierarchy depth in bottom-top order.
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
index 88dbf2915ef..536d5584bf6 100644
--- a/app/models/integrations/assembla.rb
+++ b/app/models/integrations/assembla.rb
@@ -12,6 +12,7 @@ module Integrations
required: true
field :subdomain,
+ exposes_secrets: true,
placeholder: ''
def title
@@ -34,7 +35,9 @@ module Integrations
return unless supported_events.include?(data[:object_kind])
url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
- Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
+ body = { payload: data }
+
+ Gitlab::HTTP.post(url, body: Gitlab::Json.dump(body), headers: { 'Content-Type' => 'application/json' })
end
end
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index c3a4b84bb2d..b4e97f0871e 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -9,6 +9,7 @@ module Integrations
title: -> { s_('BambooService|Bamboo URL') },
placeholder: -> { s_('https://bamboo.example.com') },
help: -> { s_('BambooService|Bamboo service root URL.') },
+ exposes_secrets: true,
required: true
field :build_key,
@@ -37,14 +38,6 @@ module Integrations
attr_accessor :response
- before_validation :reset_password
-
- def reset_password
- if bamboo_url_changed? && !password_touched?
- self.password = nil
- end
- end
-
def title
s_('BambooService|Atlassian Bamboo')
end
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index c7992e4083c..750aa60b185 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -22,7 +22,9 @@ module Integrations
MATCH_ALL_LABELS = 'match_all'
].freeze
- default_value_for :category, 'chat'
+ SECRET_MASK = '************'
+
+ attribute :category, default: 'chat'
prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
@@ -71,7 +73,7 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
+ { type: 'text', name: 'webhook', help: "#{webhook_help}", required: true }.freeze,
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{
@@ -147,7 +149,7 @@ module Integrations
raise NotImplementedError
end
- def webhook_placeholder
+ def webhook_help
raise NotImplementedError
end
diff --git a/app/models/integrations/base_ci.rb b/app/models/integrations/base_ci.rb
index 4f8732da703..db29f228e60 100644
--- a/app/models/integrations/base_ci.rb
+++ b/app/models/integrations/base_ci.rb
@@ -5,7 +5,7 @@
# working with GitLab merge requests
module Integrations
class BaseCi < Integration
- default_value_for :category, 'ci'
+ attribute :category, default: 'ci'
def valid_token?(token)
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb
index a4cec5f927b..e0994305e9d 100644
--- a/app/models/integrations/base_issue_tracker.rb
+++ b/app/models/integrations/base_issue_tracker.rb
@@ -4,7 +4,7 @@ module Integrations
class BaseIssueTracker < Integration
validate :one_issue_tracker, if: :activated?, on: :manual_change
- default_value_for :category, 'issue_tracker'
+ attribute :category, default: 'issue_tracker'
before_validation :handle_properties
before_validation :set_default_data, on: :create
diff --git a/app/models/integrations/base_monitoring.rb b/app/models/integrations/base_monitoring.rb
index 280eeda7c6c..b0bebb5a859 100644
--- a/app/models/integrations/base_monitoring.rb
+++ b/app/models/integrations/base_monitoring.rb
@@ -6,7 +6,7 @@
# to provide additional features for environments.
module Integrations
class BaseMonitoring < Integration
- default_value_for :category, 'monitoring'
+ attribute :category, default: 'monitoring'
def self.supported_events
%w()
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
new file mode 100644
index 00000000000..cb785afdcfe
--- /dev/null
+++ b/app/models/integrations/base_slack_notification.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Integrations
+ class BaseSlackNotification < BaseChatNotification
+ SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
+ push issue confidential_issue merge_request note confidential_note tag_push wiki_page deployment
+ ].freeze
+
+ prop_accessor EVENT_CHANNEL['alert']
+
+ override :default_channel_placeholder
+ def default_channel_placeholder
+ _('#general, #development')
+ end
+
+ override :get_message
+ def get_message(object_kind, data)
+ return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+
+ super
+ end
+
+ override :supported_events
+ def supported_events
+ additional = ['alert']
+
+ super + additional
+ end
+
+ override :configurable_channels?
+ def configurable_channels?
+ true
+ end
+
+ override :log_usage
+ def log_usage(event, user_id)
+ return unless user_id
+
+ return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
+
+ key = "i_ecosystem_slack_service_#{event}_notification"
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
+
+ return unless Feature.enabled?(:route_hll_to_snowplow_phase2)
+
+ optional_arguments = {
+ project: project,
+ namespace: group || project&.namespace
+ }.compact
+
+ Gitlab::Tracking.event(
+ self.class.name,
+ Integration::SNOWPLOW_EVENT_ACTION,
+ label: Integration::SNOWPLOW_EVENT_LABEL,
+ property: key,
+ user: User.find(user_id),
+ **optional_arguments
+ )
+ end
+ end
+end
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
index e51d748b562..314f0a6ee5d 100644
--- a/app/models/integrations/base_slash_commands.rb
+++ b/app/models/integrations/base_slash_commands.rb
@@ -4,7 +4,7 @@
# This class is not meant to be used directly, but only to inherrit from.
module Integrations
class BaseSlashCommands < Integration
- default_value_for :category, 'chat'
+ attribute :category, default: 'chat'
prop_accessor :token
diff --git a/app/models/integrations/base_third_party_wiki.rb b/app/models/integrations/base_third_party_wiki.rb
index 24f5bec93cf..8df172e9a53 100644
--- a/app/models/integrations/base_third_party_wiki.rb
+++ b/app/models/integrations/base_third_party_wiki.rb
@@ -2,7 +2,7 @@
module Integrations
class BaseThirdPartyWiki < Integration
- default_value_for :category, 'third_party_wiki'
+ attribute :category, default: 'third_party_wiki'
validate :only_one_third_party_wiki, if: :activated?, on: :manual_change
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index f2d2aca3ffe..5c08eac8557 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -6,13 +6,13 @@ module Integrations
class Buildkite < BaseCi
include HasWebHook
include ReactivelyCached
- extend Gitlab::Utils::Override
ENDPOINT = "https://buildkite.com"
field :project_url,
title: -> { _('Pipeline URL') },
placeholder: "#{ENDPOINT}/example-org/test-pipeline",
+ exposes_secrets: true,
required: true
field :token,
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
index b3502905bf7..88db40bea7f 100644
--- a/app/models/integrations/chat_message/pipeline_message.rb
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -126,6 +126,14 @@ module Integrations
}
end
+ def pipeline_name_field
+ {
+ title: s_("ChatMessage|Pipeline name"),
+ value: pipeline.name,
+ short: false
+ }
+ end
+
def attachments_fields
fields = [
{
@@ -143,6 +151,7 @@ module Integrations
fields << failed_stages_field if failed_stages.any?
fields << failed_jobs_field if failed_jobs.any?
fields << yaml_error_field if pipeline.has_yaml_errors?
+ fields << pipeline_name_field if Feature.enabled?(:pipeline_name, project) && pipeline.name.present?
fields
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index ab0fdbd777f..27bed5d3f76 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -3,7 +3,6 @@
module Integrations
class Datadog < Integration
include HasWebHook
- extend Gitlab::Utils::Override
DEFAULT_DOMAIN = 'datadoghq.com'
URL_TEMPLATE = 'https://webhook-intake.%{datadog_domain}/api/v2/webhook'
@@ -91,7 +90,7 @@ module Integrations
with_options if: :activated? do
validates :api_key, presence: true, format: { with: /\A\w+\z/ }
- validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
+ validates :datadog_site, format: { with: %r{\A\w+([-.]\w+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?\z}, allow_blank: true }
validates :api_url, public_url: { allow_blank: true }
validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
@@ -169,8 +168,8 @@ module Integrations
result = execute(data)
{
- success: (200..299).cover?(result[:http_status]),
- result: result[:message]
+ success: (200..299).cover?(result.payload[:http_status]),
+ result: result.message
}
end
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index d0389b82410..061c491034d 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -10,8 +10,7 @@ module Integrations
field :webhook,
section: SECTION_TYPE_CONNECTION,
- placeholder: 'https://discordapp.com/api/webhooks/…',
- help: 'URL to the webhook for the Discord channel.',
+ help: 'e.g. https://discordapp.com/api/webhooks/…',
required: true
field :notify_only_broken_pipelines,
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index d1a64aa96d4..781acf65c47 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -6,7 +6,6 @@ module Integrations
include PushDataValidations
include ReactivelyCached
prepend EnableSslVerification
- extend Gitlab::Utils::Override
DRONE_SAAS_HOSTNAME = 'cloud.drone.io'
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 6e7f31aa030..c903e8d9eb8 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -22,10 +22,6 @@ module Integrations
def default_channel_placeholder
end
- def webhook_placeholder
- 'https://chat.googleapis.com/v1/spaces…'
- end
-
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
@@ -33,7 +29,7 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
+ { type: 'text', name: 'webhook', help: 'https://chat.googleapis.com/v1/spaces…' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{
type: 'select',
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 74a6449f4f9..d2e8393ef95 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -5,10 +5,10 @@ module Integrations
include HasWebHook
prepend EnableSslVerification
- extend Gitlab::Utils::Override
field :jenkins_url,
title: -> { s_('ProjectService|Jenkins server URL') },
+ exposes_secrets: true,
required: true,
placeholder: 'http://jenkins.example.com',
help: -> { s_('The URL of the Jenkins server.') }
@@ -27,21 +27,13 @@ module Integrations
non_empty_password_title: -> { s_('ProjectService|Enter new password.') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password.') }
- before_validation :reset_password
-
validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
+ validates :password, presence: true, if: ->(service) { service.activated? && service.username.present? }
- default_value_for :merge_requests_events, false
- default_value_for :tag_push_events, false
-
- def reset_password
- # don't reset the password if a new one is provided
- if (jenkins_url_changed? || username.blank?) && !password_touched?
- self.password = nil
- end
- end
+ attribute :merge_requests_events, default: false
+ attribute :tag_push_events, default: false
def execute(data)
return unless supported_events.include?(data[:object_kind])
@@ -52,12 +44,12 @@ module Integrations
def test(data)
begin
result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 200
+ return { success: false, result: result.message } if result.payload[:http_status] != 200
rescue StandardError => e
return { success: false, result: e }
end
- { success: true, result: result[:message] }
+ { success: true, result: result.message }
end
override :hook_url
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 3ca514ab1fd..30497c0110e 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -3,7 +3,6 @@
# Accessible as Project#external_issue_tracker
module Integrations
class Jira < BaseIssueTracker
- extend ::Gitlab::Utils::Override
include Gitlab::Routing
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
@@ -533,13 +532,14 @@ module Integrations
end
def build_entity_meta(entity)
- if entity.is_a?(Commit)
+ case entity
+ when Commit
{
id: entity.short_id,
description: entity.safe_message,
branch: branch_name(entity)
}
- elsif entity.is_a?(MergeRequest)
+ when MergeRequest
{
id: entity.to_reference,
branch: entity.source_branch
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index dae11b99bc5..dd1c98ee06b 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -3,7 +3,6 @@
module Integrations
class Mattermost < BaseChatNotification
include SlackMattermostNotifier
- extend ::Gitlab::Utils::Override
def title
s_('Mattermost notifications')
@@ -26,7 +25,7 @@ module Integrations
'my-channel'
end
- def webhook_placeholder
+ def webhook_help
'http://mattermost.example.com/hooks/'
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 69863f164cd..d6cbe5760e8 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -18,10 +18,6 @@ module Integrations
'<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>'
end
- def webhook_placeholder
- 'https://outlook.office.com/webhook/…'
- end
-
def default_channel_placeholder
end
@@ -32,7 +28,7 @@ module Integrations
def default_fields
[
- { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', required: true, placeholder: "#{webhook_placeholder}" },
+ { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', help: 'https://outlook.office.com/webhook/…', required: true },
{
type: 'checkbox',
section: SECTION_TYPE_CONFIGURATION,
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index 7177c82a167..7148de66aee 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -3,7 +3,6 @@
module Integrations
class Packagist < Integration
include HasWebHook
- extend Gitlab::Utils::Override
field :username,
title: -> { s_('Username') },
@@ -55,12 +54,12 @@ module Integrations
def test(data)
begin
result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 202
+ return { success: false, result: result.message } if result.payload[:http_status] != 202
rescue StandardError => e
- return { success: false, result: e }
+ return { success: false, result: e.message }
end
- { success: true, result: result[:message] }
+ { success: true, result: result.message }
end
override :hook_url
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index d32fb974339..1acdbbbf9bc 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -56,7 +56,7 @@ module Integrations
}
Gitlab::HTTP.post(
API_ENDPOINT,
- body: message.to_json,
+ body: Gitlab::Json.dump(message),
headers: {
'Content-Type' => 'application/json',
'X-TrackerToken' => token
diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb
index 17026410eb1..e08dc6d0f51 100644
--- a/app/models/integrations/pumble.rb
+++ b/app/models/integrations/pumble.rb
@@ -36,7 +36,7 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "https://api.pumble.com/workspaces/x/...", required: true },
+ { type: 'text', name: 'webhook', help: 'https://api.pumble.com/workspaces/x/...', required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{
type: 'select',
@@ -51,7 +51,7 @@ module Integrations
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
- response = Gitlab::HTTP.post(webhook, headers: header, body: { text: message.summary }.to_json)
+ response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump({ text: message.summary }))
response if response.success?
end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index c254ea379bb..89326b8174f 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -1,17 +1,8 @@
# frozen_string_literal: true
module Integrations
- class Slack < BaseChatNotification
+ class Slack < BaseSlackNotification
include SlackMattermostNotifier
- extend ::Gitlab::Utils::Override
-
- SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
- push issue confidential_issue merge_request note confidential_note
- tag_push wiki_page deployment
- ].freeze
- SNOWPLOW_EVENT_CATEGORY = self.name
-
- prop_accessor EVENT_CHANNEL['alert']
def title
'Slack notifications'
@@ -25,57 +16,9 @@ module Integrations
'slack'
end
- def default_channel_placeholder
- _('#general, #development')
- end
-
- def webhook_placeholder
+ override :webhook_help
+ def webhook_help
'https://hooks.slack.com/services/…'
end
-
- def supported_events
- additional = []
- additional << 'alert'
-
- super + additional
- end
-
- def get_message(object_kind, data)
- return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
-
- super
- end
-
- override :log_usage
- def log_usage(event, user_id)
- return unless user_id
-
- return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
-
- key = "i_ecosystem_slack_service_#{event}_notification"
-
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
-
- return unless Feature.enabled?(:route_hll_to_snowplow_phase2)
-
- optional_arguments = {
- project: project,
- namespace: group || project&.namespace
- }.compact
-
- Gitlab::Tracking.event(
- SNOWPLOW_EVENT_CATEGORY,
- Integration::SNOWPLOW_EVENT_ACTION,
- label: Integration::SNOWPLOW_EVENT_LABEL,
- property: key,
- user: User.find(user_id),
- **optional_arguments
- )
- end
-
- override :configurable_channels?
- def configurable_channels?
- true
- end
end
end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index ca7a715f4b3..af629d6ef1e 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -11,6 +11,7 @@ module Integrations
field :teamcity_url,
title: -> { s_('ProjectService|TeamCity server URL') },
placeholder: 'https://teamcity.example.com',
+ exposes_secrets: true,
required: true
field :build_type,
@@ -36,8 +37,6 @@ module Integrations
attr_accessor :response
- before_validation :reset_password
-
class << self
def to_param
'teamcity'
@@ -48,12 +47,6 @@ module Integrations
end
end
- def reset_password
- if teamcity_url_changed? && !password_touched?
- self.password = nil
- end
- end
-
def title
'JetBrains TeamCity'
end
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index f10a75fac5d..aa19133b8c2 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -29,7 +29,7 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true },
+ { type: 'text', name: 'webhook', help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…', required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{
type: 'select',
@@ -43,11 +43,13 @@ module Integrations
private
def notify(message, opts)
- response = Gitlab::HTTP.post(webhook, body: {
+ body = {
subject: message.project_name,
text: message.summary,
markdown: true
- }.to_json)
+ }
+
+ response = Gitlab::HTTP.post(webhook, body: Gitlab::Json.dump(body))
response if response.success?
end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 75be457dcf5..8e6f5ca6d17 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -29,7 +29,7 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
+ { type: 'text', name: 'webhook', help: 'https://api.ciscospark.com/v1/webhooks/incoming/...', required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{
type: 'select',
@@ -44,7 +44,7 @@ module Integrations
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
- response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
+ response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump({ markdown: message.summary }))
response if response.success?
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index ea7acf9a5d1..fc083002c41 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -39,13 +39,14 @@ class Issue < ApplicationRecord
DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
SORTING_PREFERENCE_FIELD = :issues_sort
+ MAX_BRANCH_TEMPLATE = 255
# Types of issues that should be displayed on issue lists across the app
# for example, project issues list, group issues list, and issues dashboard.
#
# This should be kept consistent with the enums used for the GraphQL issue list query in
# https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158
- TYPES_FOR_LIST = %w(issue incident test_case task).freeze
+ TYPES_FOR_LIST = %w(issue incident test_case task objective).freeze
# Types of issues that should be displayed on issue board lists
TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
@@ -90,6 +91,7 @@ class Issue < ApplicationRecord
has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
+ has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue
has_many :prometheus_alerts, through: :prometheus_alert_events
has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
@@ -210,6 +212,7 @@ class Issue < ApplicationRecord
end
scope :with_null_relative_position, -> { where(relative_position: nil) }
scope :with_non_null_relative_position, -> { where.not(relative_position: nil) }
+ scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') }
before_validation :ensure_namespace_id, :ensure_work_item_type
@@ -270,9 +273,14 @@ class Issue < ApplicationRecord
reorder(upvotes_count: :asc)
end
- override :pg_full_text_search
- def pg_full_text_search(search_term)
- super.where('issue_search_data.project_id = issues.project_id')
+ override :full_search
+ def full_search(query, matched_columns: nil, use_minimum_char_limit: true)
+ return super if query.match?(IssuableFinder::FULL_TEXT_SEARCH_TERM_REGEX)
+
+ super.where(
+ 'issues.title NOT SIMILAR TO :pattern OR issues.description NOT SIMILAR TO :pattern',
+ pattern: IssuableFinder::FULL_TEXT_SEARCH_TERM_PATTERN
+ )
end
end
@@ -393,10 +401,21 @@ class Issue < ApplicationRecord
)
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
- end.compact.join('-')
+ def self.to_branch_name(id, title, project: nil)
+ params = {
+ 'id' => id.to_s.parameterize(preserve_case: true),
+ 'title' => title.to_s.parameterize
+ }
+ template = project&.issue_branch_template
+
+ branch_name =
+ if template.present?
+ Gitlab::StringPlaceholderReplacer.replace_string_placeholders(template, /(#{params.keys.join('|')})/) do |arg|
+ params[arg]
+ end
+ else
+ params.values.select(&:present?).join('-')
+ end
if branch_name.length > 100
truncated_string = branch_name[0, 100]
@@ -474,7 +493,7 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
- self.class.to_branch_name(iid, title)
+ self.class.to_branch_name(iid, title, project: project)
end
end
@@ -653,6 +672,10 @@ class Issue < ApplicationRecord
Gitlab::EtagCaching::Store.new.touch(key)
end
+ def supports_confidentiality?
+ true
+ end
+
private
def due_date_after_start_date
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index ed73793c78f..c6269313d8b 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -4,9 +4,8 @@
class Iteration < ApplicationRecord
include IgnorableColumns
- # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372125
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372126
- ignore_column :project_id, remove_with: '15.6', remove_after: '2022-09-17'
+ ignore_column :project_id, remove_with: '15.7', remove_after: '2022-11-18'
self.table_name = 'sprints'
diff --git a/app/models/label.rb b/app/models/label.rb
index 483d51099b1..aa53c0e0f3f 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -14,8 +14,7 @@ class Label < ApplicationRecord
DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
- attribute :color, ::Gitlab::Database::Type::Color.new
- default_value_for :color, DEFAULT_COLOR
+ attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
diff --git a/app/models/member.rb b/app/models/member.rb
index ff1d8f18c25..80c5fd7e468 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -286,7 +286,7 @@ class Member < ApplicationRecord
refresh_member_authorized_projects(blocking: false)
end
- default_value_for :notification_level, NotificationSetting.levels[:global]
+ attribute :notification_level, default: -> { NotificationSetting.levels[:global] }
class << self
def search(query)
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 2b35f7da7b4..ad1ad1e74fe 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -13,7 +13,7 @@ class GroupMember < Member
delegate :update_two_factor_requirement, to: :user, allow_nil: true
# Make sure group member points only to group as it source
- default_value_for :source_type, SOURCE_TYPE
+ attribute :source_type, default: SOURCE_TYPE
validates :source_type, format: { with: SOURCE_TYPE_FORMAT }
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
index e411a0ef5eb..48c9bcb9a70 100644
--- a/app/models/members/last_group_owner_assigner.rb
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -40,6 +40,6 @@ class LastGroupOwnerAssigner
end
def owners
- @owners ||= group.all_owners_excluding_project_bots.load
+ @owners ||= group.member_owners_excluding_project_bots.load
end
end
diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb
index f093619ff36..6cf6b1adb45 100644
--- a/app/models/members/member_task.rb
+++ b/app/models/members/member_task.rb
@@ -34,9 +34,10 @@ class MemberTask < ApplicationRecord
end
def project_in_member_source
- if member.is_a?(GroupMember)
+ case member
+ when GroupMember
errors.add(:project, _('is not in the member group')) unless project.namespace == member.source
- elsif member.is_a?(ProjectMember)
+ when ProjectMember
errors.add(:project, _('is not the member project')) unless project == member.source
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 8fd82fcb34a..1099e0f48c0 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -10,7 +10,7 @@ class ProjectMember < Member
delegate :namespace_id, to: :project
# Make sure project member points only to project as it source
- default_value_for :source_type, SOURCE_TYPE
+ attribute :source_type, default: SOURCE_TYPE
validates :source_type, format: { with: SOURCE_TYPE_FORMAT }
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index fb20d91fa20..735c0df1529 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -136,7 +136,7 @@ class MergeRequest < ApplicationRecord
before_validation :set_draft_status
- after_create :ensure_merge_request_diff
+ after_create :ensure_merge_request_diff, unless: :skip_ensure_merge_request_diff
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
after_commit :ensure_metrics, on: [:create, :update], unless: :importing?
@@ -146,6 +146,10 @@ class MergeRequest < ApplicationRecord
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
+ # Temporary flag to skip merge_request_diff creation on create.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100390
+ attr_accessor :skip_ensure_merge_request_diff
+
# Temporary fields to store compare vars
# when creating new merge request
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
@@ -242,9 +246,7 @@ class MergeRequest < ApplicationRecord
end
after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition|
- if Feature.enabled?(:trigger_mr_subscription_on_merge_status_change, merge_request.project)
- GraphqlTriggers.merge_request_merge_status_updated(merge_request)
- end
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
end
# rubocop: disable CodeReuse/ServiceClass
@@ -649,8 +651,8 @@ class MergeRequest < ApplicationRecord
context_commits.count
end
- 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?
+ def commits(limit: nil, load_from_gitaly: false, page: nil)
+ return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page) if merge_request_diff.persisted?
commits_arr = if compare_commits
reversed_commits = compare_commits.reverse
@@ -662,8 +664,8 @@ class MergeRequest < ApplicationRecord
CommitCollection.new(source_project, commits_arr, source_branch)
end
- def recent_commits(load_from_gitaly: false)
- commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: load_from_gitaly)
+ def recent_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: false, page: nil)
+ commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page)
end
def commits_count
@@ -1130,7 +1132,7 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def diffable_merge_ref?
- open? && merge_head_diff.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?)
+ open? && merge_head_diff.present? && can_be_merged?
end
# Returns boolean indicating the merge_status should be rechecked in order to
@@ -1673,7 +1675,7 @@ class MergeRequest < ApplicationRecord
# TODO: consider renaming this as with exposed artifacts we generate reports,
# not always compare
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
- def compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {} )
+ def compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {})
with_reactive_cache(service_class.name, current_user&.id, report_type) do |data|
unless service_class.new(project, current_user, id: id, report_type: report_type, additional_params: additional_params)
.latest?(comparison_base_pipeline(service_class.name), actual_head_pipeline, data)
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index be3a1d42eac..3e481e35deb 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -1,9 +1,6 @@
# frozen_string_literal: true
class MergeRequestAssignee < ApplicationRecord
- include IgnorableColumns
- ignore_column %i[state updated_state_by_user_id], remove_with: '15.6', remove_after: '2022-10-22'
-
belongs_to :merge_request, touch: true
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 9f7e98dc04b..98a9ccc2040 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -292,9 +292,9 @@ class MergeRequestDiff < ApplicationRecord
end
end
- 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)
+ def commits(limit: nil, load_from_gitaly: false, page: nil)
+ strong_memoize(:"commits_#{limit || 'all'}_#{load_from_gitaly}_page_#{page}") do
+ load_commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page)
end
end
@@ -725,17 +725,19 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def load_commits(limit: nil, load_from_gitaly: false)
+ def load_commits(limit: nil, load_from_gitaly: false, page: nil)
+ diff_commits = page.present? ? merge_request_diff_commits.page(page).per(limit) : merge_request_diff_commits.limit(limit)
+
if load_from_gitaly
- commits = Gitlab::Git::Commit.batch_by_oid(repository, merge_request_diff_commits.limit(limit).map(&:sha))
+ commits = Gitlab::Git::Commit.batch_by_oid(repository, diff_commits.map(&:sha))
commits = Commit.decorate(commits, project)
else
- commits = merge_request_diff_commits.with_users.limit(limit)
+ commits = diff_commits.with_users
.map { |commit| Commit.from_hash(commit.to_hash, project) }
end
CommitCollection
- .new(merge_request.target_project, commits, merge_request.target_branch)
+ .new(merge_request.target_project, commits, merge_request.target_branch, page: page.to_i, per_page: limit, count: commits_count)
end
def save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 66f1e45fd49..152fb195c97 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -70,7 +70,7 @@ class MergeRequestDiffCommit < ApplicationRecord
sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]),
- trailers: commit_hash.fetch(:trailers, {}).to_json
+ trailers: Gitlab::Json.dump(commit_hash.fetch(:trailers, {}))
)
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 04b322ef5a6..5a98131a6fd 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -15,12 +15,7 @@ class MergeRequestDiffFile < ApplicationRecord
end
def utf8_diff
- fetched_diff = if Feature.enabled?(:externally_stored_diffs_caching_export) &&
- merge_request_diff&.stored_externally?
- diff_export
- else
- diff
- end
+ fetched_diff = merge_request_diff&.stored_externally? ? diff_export : diff
return '' if fetched_diff.blank?
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index 4b5b71481d3..e1e2805cd8f 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -2,8 +2,7 @@
class MergeRequestReviewer < ApplicationRecord
include MergeRequestReviewerState
- include IgnorableColumns
- ignore_column :updated_state_by_user_id, remove_with: '15.6', remove_after: '2022-10-22'
+ include BulkInsertSafe # must be included _last_ i.e. after any other concerns
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index 29e1ba88528..f7da4418624 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -11,8 +11,15 @@ module Ml
belongs_to :user
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
+ has_many :latest_metrics, -> { latest }, class_name: 'Ml::CandidateMetric', inverse_of: :candidate
- default_value_for(:iid) { SecureRandom.uuid }
+ attribute :iid, default: -> { SecureRandom.uuid }
+
+ scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) }
+
+ def artifact_root
+ "/ml_candidate_#{iid}/-/"
+ end
class << self
def with_project_id_and_iid(project_id, iid)
diff --git a/app/models/ml/candidate_metric.rb b/app/models/ml/candidate_metric.rb
index e03a8b83ee6..8e13a46d6b4 100644
--- a/app/models/ml/candidate_metric.rb
+++ b/app/models/ml/candidate_metric.rb
@@ -6,5 +6,7 @@ module Ml
validates :name, length: { maximum: 250 }, presence: true
belongs_to :candidate, class_name: 'Ml::Candidate'
+
+ scope :latest, -> { select('DISTINCT ON (candidate_id, name) *').order('candidate_id, name, id DESC') }
end
end
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index a32099e8a0c..05b238b960d 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -23,7 +23,7 @@ module Ml
end
def by_project_id(project_id)
- where(project_id: project_id)
+ where(project_id: project_id).order(id: :desc)
end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 42f362876bb..51c39ad4ec3 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -40,9 +40,9 @@ class Namespace < ApplicationRecord
PATH_TRAILING_VIOLATIONS = %w[.git .atom .].freeze
- # The first date in https://docs.gitlab.com/ee/user/usage_quotas.html#namespace-storage-limit-enforcement-schedule
- # Determines when we start enforcing namespace storage
- MIN_STORAGE_ENFORCEMENT_DATE = Date.new(2022, 10, 19)
+ # This date is just a placeholder until namespace storage enforcement timeline is confirmed at which point
+ # this should be replaced, see https://about.gitlab.com/pricing/faq-efficient-free-tier/#user-limits-on-gitlab-saas-free-tier
+ MIN_STORAGE_ENFORCEMENT_DATE = 3.months.from_now.to_date
# https://gitlab.com/gitlab-org/gitlab/-/issues/367531
MIN_STORAGE_ENFORCEMENT_USAGE = 5.gigabytes
@@ -91,6 +91,7 @@ class Namespace < ApplicationRecord
validates :name,
presence: true,
length: { maximum: 255 }
+ validates :name, uniqueness: { scope: [:type, :parent_id] }, if: -> { parent_id.present? }
validates :description, length: { maximum: 255 }
@@ -550,11 +551,12 @@ class Namespace < ApplicationRecord
end
def shared_runners_setting_higher_than?(other_setting)
- if other_setting == SR_ENABLED
+ case other_setting
+ when SR_ENABLED
false
- elsif other_setting == SR_DISABLED_WITH_OVERRIDE
+ when SR_DISABLED_WITH_OVERRIDE
shared_runners_setting == SR_ENABLED
- elsif other_setting == SR_DISABLED_AND_UNOVERRIDABLE
+ when SR_DISABLED_AND_UNOVERRIDABLE
shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE
else
raise ArgumentError
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 6a87fba57ac..3e6371b0c4d 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -4,11 +4,6 @@ class NamespaceSetting < ApplicationRecord
include CascadingNamespaceSettingAttribute
include Sanitizable
include ChronicDurationAttribute
- include IgnorableColumns
-
- ignore_columns %i[exclude_from_free_user_cap include_for_free_user_cap_preview],
- remove_with: '15.5',
- remove_after: '2022-09-23'
cascading_attr :delayed_project_removal
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index a034d97a6bb..7ffcb8b9219 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -23,6 +23,8 @@ module Network
protected
def collect_notes
+ return {} if Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment)
+
h = Hash.new(0)
@project
diff --git a/app/models/note.rb b/app/models/note.rb
index e444111119b..8e1f4979602 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -60,7 +60,7 @@ class Note < ApplicationRecord
# 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
+ attribute :system, default: false
attr_mentionable :note, pipeline: :note
participant :author
@@ -361,14 +361,6 @@ class Note < ApplicationRecord
super(noteable_type.to_s.classify.constantize.base_class.to_s)
end
- def noteable_assignee_or_author?(user)
- return false unless user
- return false unless noteable.respond_to?(:author_id)
- return noteable.assignee_or_author?(user) if [MergeRequest, Issue].include?(noteable.class)
-
- noteable.author_id == user.id
- end
-
def contributor?
project&.team&.contributor?(self.author_id)
end
@@ -756,7 +748,8 @@ class Note < ApplicationRecord
if user_visible_reference_count.present? && total_reference_count.present?
# if they are not equal, then there are private/confidential references as well
- user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
+ total_reference_count == 0 ||
+ user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
else
refs = all_references(user)
refs.all.any? && refs.all_visible?
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 2e45753c182..cde7b92e74a 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -3,9 +3,7 @@
class NotificationSetting < ApplicationRecord
include FromUnion
- enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
-
- default_value_for :level, NotificationSetting.levels[:global]
+ enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }, _default: :global
belongs_to :user
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index eac99e8d441..8e79a750793 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -31,8 +31,6 @@ class OauthAccessToken < Doorkeeper::AccessToken
# have `reuse_access_tokens` disabled and we also hash tokens.
# This ensures we don't accidentally return a hashed token value.
def self.matching_token_for(application, resource_owner, scopes)
- return if Feature.enabled?(:hash_oauth_tokens)
-
- super
+ # no-op
end
end
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index e36c59366fe..0df8c87f73f 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -16,8 +16,8 @@ module Operations
has_internal_id :iid, scope: :project
- default_value_for :active, true
- default_value_for :version, :new_version_flag
+ attribute :active, default: true
+ attribute :version, default: :new_version_flag
# strategies exists only for the second version
has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy'
diff --git a/app/models/packages/go/module_version.rb b/app/models/packages/go/module_version.rb
index c442b2416f1..5869a03e081 100644
--- a/app/models/packages/go/module_version.rb
+++ b/app/models/packages/go/module_version.rb
@@ -21,9 +21,10 @@ module Packages
raise ArgumentError, "mod is required" unless mod
raise ArgumentError, "commit is required" unless commit
- if type == :ref
+ case type
+ when :ref
raise ArgumentError, "ref is required" unless ref
- elsif type == :pseudo
+ when :pseudo
raise ArgumentError, "name is required" unless name
raise ArgumentError, "semver is required" unless semver
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 16d5492a65e..328c67a0711 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -10,8 +10,8 @@ class PagesDomain < ApplicationRecord
SSL_RENEWAL_THRESHOLD = 30.days.freeze
enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate
- enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope
- enum usage: { pages: 0, serverless: 1 }, _prefix: :usage
+ enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project
+ enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages
belongs_to :project
has_many :acme_orders, class_name: "PagesDomainAcmeOrder"
@@ -35,10 +35,8 @@ class PagesDomain < ApplicationRecord
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
validate :validate_custom_domain_count_per_project, on: :create
- default_value_for(:auto_ssl_enabled, allows_nil: false) { ::Gitlab::LetsEncrypt.enabled? }
- default_value_for :scope, allows_nil: false, value: :project
- default_value_for :wildcard, allows_nil: false, value: false
- default_value_for :usage, allows_nil: false, value: :pages
+ attribute :auto_ssl_enabled, default: -> { ::Gitlab::LetsEncrypt.enabled? }
+ attribute :wildcard, default: false
attr_encrypted :key,
mode: :per_attribute_iv_and_salt,
@@ -50,7 +48,7 @@ class PagesDomain < ApplicationRecord
scope :for_project, ->(project) { where(project: project) }
- scope :enabled, -> { where('enabled_until >= ?', Time.current ) }
+ scope :enabled, -> { where('enabled_until >= ?', Time.current) }
scope :needs_verification, -> do
verified_at = arel_table[:verified_at]
enabled_until = arel_table[:enabled_until]
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index f0ed1822da6..3126dba9d6d 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -11,8 +11,6 @@ class PersonalAccessToken < ApplicationRecord
add_authentication_token_field :token, digest: true
- REDIS_EXPIRY_TIME = 3.minutes
-
# PATs are 20 characters + optional configurable settings prefix (0..20)
TOKEN_LENGTH_RANGE = (20..40).freeze
@@ -34,8 +32,6 @@ class PersonalAccessToken < ApplicationRecord
scope :for_user, -> (user) { where(user: user) }
scope :for_users, -> (users) { where(user: users) }
scope :preload_users, -> { preload(:user) }
- scope :order_expires_at_asc, -> { reorder(expires_at: :asc) }
- scope :order_expires_at_desc, -> { reorder(expires_at: :desc) }
scope :order_expires_at_asc_id_desc, -> { reorder(expires_at: :asc, id: :desc) }
scope :project_access_token, -> { includes(:user).where(user: { user_type: :project_bot }) }
scope :owner_is_human, -> { includes(:user).where(user: { user_type: :human }) }
@@ -55,35 +51,10 @@ class PersonalAccessToken < ApplicationRecord
!revoked? && !expired?
end
- def self.redis_getdel(user_id)
- Gitlab::Redis::SharedState.with do |redis|
- redis_key = redis_shared_state_key(user_id)
- encrypted_token = redis.get(redis_key)
- redis.del(redis_key)
-
- begin
- Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
- rescue StandardError => e
- logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{e.class}"
- encrypted_token
- end
- end
- end
-
- def self.redis_store!(user_id, token)
- encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
-
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME)
- end
- end
-
override :simple_sorts
def self.simple_sorts
super.merge(
{
- 'expires_at_asc' => -> { order_expires_at_asc },
- 'expires_at_desc' => -> { order_expires_at_desc },
'expires_at_asc_id_desc' => -> { order_expires_at_asc_id_desc }
}
)
@@ -121,10 +92,6 @@ class PersonalAccessToken < ApplicationRecord
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end
-
- def self.redis_shared_state_key(user_id)
- "gitlab:personal_access_token:#{user_id}"
- end
end
PersonalAccessToken.prepend_mod_with('PersonalAccessToken')
diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb
index 12b48895e0c..b0dd52c9657 100644
--- a/app/models/postgresql/detached_partition.rb
+++ b/app/models/postgresql/detached_partition.rb
@@ -3,5 +3,9 @@
module Postgresql
class DetachedPartition < ::Gitlab::Database::SharedModel
scope :ready_to_drop, -> { where('drop_after < ?', Time.current) }
+
+ def fully_qualified_table_name
+ "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}"
+ end
end
end
diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb
index 1e935249407..6192f79ce2c 100644
--- a/app/models/preloaders/project_root_ancestor_preloader.rb
+++ b/app/models/preloaders/project_root_ancestor_preloader.rb
@@ -9,7 +9,7 @@ module Preloaders
end
def execute
- return if @projects.is_a?(ActiveRecord::NullRelation)
+ return unless @projects.is_a?(ActiveRecord::Relation)
return unless ::Feature.enabled?(:use_traversal_ids)
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
index 2e2272a2ef5..c9fd5e7718a 100644
--- a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -7,9 +7,11 @@ module Preloaders
def initialize(projects, user)
@projects = if projects.is_a?(Array)
Project.where(id: projects)
- else
+ elsif Feature.enabled?(:projects_preloader_fix)
# Push projects base query in to a sub-select to avoid
# table name clashes. Performs better than aliasing.
+ Project.where(id: projects.subquery(:id))
+ else
Project.where(id: projects.reselect(:id))
end
@@ -17,6 +19,8 @@ module Preloaders
end
def execute
+ return unless @user
+
project_authorizations = ProjectAuthorization.arel_table
auths = @projects
diff --git a/app/models/project.rb b/app/models/project.rb
index 7b61010ab01..a07d4147228 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -39,6 +39,7 @@ class Project < ApplicationRecord
include BulkUsersByEmailLoad
include RunnerTokenExpirationInterval
include BlocksUnsafeSerialization
+ include Subquery
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
@@ -222,6 +223,7 @@ class Project < ApplicationRecord
has_one :youtrack_integration, class_name: 'Integrations::Youtrack'
has_one :zentao_integration, class_name: 'Integrations::Zentao'
+ has_one :wiki_repository, class_name: 'Projects::WikiRepository', inverse_of: :project
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
inverse_of: :root_project,
@@ -451,7 +453,7 @@ class Project < ApplicationRecord
:metrics_dashboard_access_level, :analytics_access_level,
:operations_access_level, :security_and_compliance_access_level,
:container_registry_access_level, :environments_access_level, :feature_flags_access_level,
- :monitor_access_level, :releases_access_level,
+ :monitor_access_level, :releases_access_level, :infrastructure_access_level,
to: :project_feature, allow_nil: true
delegate :show_default_award_emojis, :show_default_award_emojis=,
@@ -491,6 +493,7 @@ class Project < ApplicationRecord
to: :project_setting
delegate :merge_commit_template, :merge_commit_template=, to: :project_setting, allow_nil: true
delegate :squash_commit_template, :squash_commit_template=, to: :project_setting, allow_nil: true
+ delegate :issue_branch_template, :issue_branch_template=, to: :project_setting, allow_nil: true
delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
@@ -1616,7 +1619,7 @@ class Project < ApplicationRecord
end
def all_clusters
- group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )
+ group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto })
instance_clusters = Clusters::Cluster.instance_type
Clusters::Cluster.from_union([clusters, group_clusters, instance_clusters])
@@ -1705,7 +1708,11 @@ class Project < ApplicationRecord
end
def has_active_integrations?(hooks_scope = :push_hooks)
- integrations.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
+ @has_active_integrations ||= {} # rubocop: disable Gitlab/PredicateMemoization
+
+ return @has_active_integrations[hooks_scope] if @has_active_integrations.key?(hooks_scope)
+
+ @has_active_integrations[hooks_scope] = integrations.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
end
def feature_usage
@@ -2729,11 +2736,6 @@ class Project < ApplicationRecord
ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
end
- # DO NOT USE. This method will be deprecated soon
- def uses_external_project_ci_config?
- !!(ci_config_path =~ %r{@.+/.+})
- end
-
def limited_protected_branches(limit)
protected_branches.limit(limit)
end
@@ -2784,7 +2786,7 @@ class Project < ApplicationRecord
return unless service_desk_enabled?
config = Gitlab.config.incoming_email
- wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
+ wildcard = Gitlab::Email::Common::WILDCARD_PLACEHOLDER
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
end
@@ -2854,11 +2856,6 @@ class Project < ApplicationRecord
repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
end
- # DO NOT USE. This method will be deprecated soon
- 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
@@ -2927,10 +2924,6 @@ class Project < ApplicationRecord
ci_cd_settings.keep_latest_artifact?
end
- def runner_token_expiration_interval
- ci_cd_settings&.runner_token_expiration_interval
- end
-
def group_runners_enabled?
return false unless ci_cd_settings
@@ -3006,7 +2999,7 @@ class Project < ApplicationRecord
end
def work_items_create_from_markdown_feature_flag_enabled?
- work_items_feature_flag_enabled? && (group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown))
+ group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown)
end
def enqueue_record_project_target_platforms
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 8b43e5e5d63..3623b3be20d 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -31,6 +31,7 @@ class ProjectAuthorization < ApplicationRecord
def self.insert_all_in_batches(attributes, per_batch = BATCH_SIZE)
add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: per_batch)
+ log_details(entire_size: attributes.size) if add_delay
attributes.each_slice(per_batch) do |attributes_batch|
insert_all(attributes_batch)
@@ -40,6 +41,7 @@ class ProjectAuthorization < ApplicationRecord
def self.delete_all_in_batches_for_project(project:, user_ids:, per_batch: BATCH_SIZE)
add_delay = add_delay_between_batches?(entire_size: user_ids.size, batch_size: per_batch)
+ log_details(entire_size: user_ids.size) if add_delay
user_ids.each_slice(per_batch) do |user_ids_batch|
project.project_authorizations.where(user_id: user_ids_batch).delete_all
@@ -49,6 +51,7 @@ class ProjectAuthorization < ApplicationRecord
def self.delete_all_in_batches_for_user(user:, project_ids:, per_batch: BATCH_SIZE)
add_delay = add_delay_between_batches?(entire_size: project_ids.size, batch_size: per_batch)
+ log_details(entire_size: project_ids.size) if add_delay
project_ids.each_slice(per_batch) do |project_ids_batch|
user.project_authorizations.where(project_id: project_ids_batch).delete_all
@@ -65,6 +68,13 @@ class ProjectAuthorization < ApplicationRecord
Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh)
end
+ private_class_method def self.log_details(entire_size:)
+ Gitlab::AppLogger.info(
+ entire_size: entire_size,
+ message: 'Project authorizations refresh performed with delay'
+ )
+ end
+
private_class_method def self.perform_delay
sleep(SLEEP_DELAY)
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index d7a5d0d9d84..cc9003423be 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -17,8 +17,8 @@ class ProjectCiCdSetting < ApplicationRecord
},
allow_nil: true
- default_value_for :forward_deployment_enabled, true
- default_value_for :separated_caches, true
+ attribute :forward_deployment_enabled, default: true
+ attribute :separated_caches, default: true
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index dad8aaf0625..11f4a3f3b6f 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -25,6 +25,7 @@ class ProjectFeature < ApplicationRecord
environments
feature_flags
releases
+ infrastructure
].freeze
EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 6d40544fad4..7116ccd9824 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -2,6 +2,7 @@
class ProjectSetting < ApplicationRecord
include ::Gitlab::Utils::StrongMemoize
+ include EachBatch
ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze
@@ -20,12 +21,13 @@ class ProjectSetting < ApplicationRecord
validates :merge_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :squash_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
+ validates :issue_branch_template, length: { maximum: Issue::MAX_BRANCH_TEMPLATE }
validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS }
validates :suggested_reviewers_enabled, inclusion: { in: [true, false] }
validate :validates_mr_default_target_self
- default_value_for(:legacy_open_source_license_available) do
+ attribute :legacy_open_source_license_available, default: -> do
Feature.enabled?(:legacy_open_source_license_available, type: :ops)
end
@@ -57,7 +59,7 @@ class ProjectSetting < ApplicationRecord
!!super
end
end
- strong_memoize_attr :show_diff_preview_in_email
+ strong_memoize_attr :show_diff_preview_in_email?, :show_diff_preview_in_email
private
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index f108e43015e..0570be85ad1 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -7,8 +7,8 @@ class ProjectStatistics < ApplicationRecord
belongs_to :project
belongs_to :namespace
- default_value_for :wiki_size, 0
- default_value_for :snippets_size, 0
+ attribute :wiki_size, default: 0
+ attribute :snippets_size, default: 0
counter_attribute :build_artifacts_size
@@ -95,8 +95,7 @@ class ProjectStatistics < ApplicationRecord
# and the column can be nil.
# This means that, when the columns were added, all rows had nil
# values on them.
- # Therefore, any call to any of those methods will return nil instead
- # of 0, because `default_value_for` works with new records, not existing ones.
+ # Therefore, any call to any of those methods will return nil instead of 0.
#
# These two methods provide consistency and avoid returning nil.
def wiki_size
diff --git a/app/models/projects/import_export/relation_export.rb b/app/models/projects/import_export/relation_export.rb
index 15198049f87..9bdf10d7c0e 100644
--- a/app/models/projects/import_export/relation_export.rb
+++ b/app/models/projects/import_export/relation_export.rb
@@ -34,11 +34,18 @@ module Projects
scope :by_relation, -> (relation) { where(relation: relation) }
+ STATUS = {
+ queued: 0,
+ started: 1,
+ finished: 2,
+ failed: 3
+ }.freeze
+
state_machine :status, initial: :queued do
- state :queued, value: 0
- state :started, value: 1
- state :finished, value: 2
- state :failed, value: 3
+ state :queued, value: STATUS[:queued]
+ state :started, value: STATUS[:started]
+ state :finished, value: STATUS[:finished]
+ state :failed, value: STATUS[:failed]
event :start do
transition queued: :started
diff --git a/app/models/projects/wiki_repository.rb b/app/models/projects/wiki_repository.rb
new file mode 100644
index 00000000000..88382adbcb7
--- /dev/null
+++ b/app/models/projects/wiki_repository.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Projects
+ class WikiRepository < ApplicationRecord
+ self.table_name = :project_wiki_repositories
+
+ belongs_to :project, inverse_of: :wiki_repository
+
+ validates :project, presence: true, uniqueness: true
+ end
+end
+
+Projects::WikiRepository.prepend_mod_with('Projects::WikiRepository')
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index dfd5c315f6e..80967c1b072 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -4,6 +4,10 @@ class ProtectedBranch < ApplicationRecord
include ProtectedRef
include Gitlab::SQL::Pattern
+ belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches
+
+ validate :validate_either_project_or_top_group
+
scope :requiring_code_owner_approval,
-> { where(code_owner_approval_required: true) }
@@ -99,6 +103,18 @@ class ProtectedBranch < ApplicationRecord
def default_branch?
name == project.default_branch
end
+
+ private
+
+ def validate_either_project_or_top_group
+ if !project && !group
+ errors.add(:base, _('must be associated with a Group or a Project'))
+ elsif project && group
+ errors.add(:base, _('cannot be associated with both a Group and a Project'))
+ elsif group && group.root_ancestor != group
+ errors.add(:base, _('cannot be associated with a subgroup'))
+ end
+ end
end
ProtectedBranch.prepend_mod_with('ProtectedBranch')
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index 5b2467daddc..e89cb3aabc7 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -4,6 +4,7 @@ class ProtectedTag < ApplicationRecord
include ProtectedRef
validates :name, uniqueness: { scope: :project_id }
+ validates :project, presence: true
protected_ref_access_levels :create
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3413b3e3424..95d1b815e74 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -98,6 +98,10 @@ class Repository
alias_method :raw, :raw_repository
+ def flipper_id
+ raw_repository.flipper_id
+ end
+
# Don't use this! It's going away. Use Gitaly to read or write from repos.
def path_to_repo
@path_to_repo ||=
@@ -1232,7 +1236,8 @@ class Repository
Gitlab::Git::Repository.new(shard,
disk_path + '.git',
repo_type.identifier_for_container(container),
- container.full_path)
+ container.full_path,
+ container: container)
end
end
diff --git a/app/models/serverless/domain_cluster.rb b/app/models/serverless/domain_cluster.rb
index 1effabf1c22..561bfc65b2b 100644
--- a/app/models/serverless/domain_cluster.rb
+++ b/app/models/serverless/domain_cluster.rb
@@ -19,7 +19,7 @@ module Serverless
validates :uuid, presence: true, uniqueness: true, length: { is: ::Serverless::Domain::UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
- default_value_for(:uuid, allows_nil: false) { ::Serverless::Domain.generate_uuid }
+ after_initialize :set_uuid, if: :new_record?
delegate :domain, to: :pages_domain
delegate :cluster, to: :knative
@@ -29,5 +29,11 @@ module Serverless
.includes(:pages_domain, :knative)
.find_by(uuid: uuid)
end
+
+ private
+
+ def set_uuid
+ self.uuid = ::Serverless::Domain.generate_uuid
+ end
end
end
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index e5c8f4ab32a..8a207c891e2 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -28,7 +28,7 @@ module Terraform
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
- default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
+ attribute :uuid, default: -> { SecureRandom.hex(UUID_LENGTH / 2) }
def latest_file
latest_version&.file
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index c50eaa66860..d6a16ad5b99 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -13,7 +13,7 @@ module Terraform
scope :with_files_stored_locally, -> { where(file_store: Terraform::StateUploader::Store::LOCAL) }
scope :preload_state, -> { includes(:terraform_state) }
- default_value_for(:file_store) { StateUploader.default_store }
+ attribute :file_store, default: -> { StateUploader.default_store }
mount_file_store_uploader StateUploader
diff --git a/app/models/time_tracking/timelog_category.rb b/app/models/time_tracking/timelog_category.rb
index 26614f6fc44..246e78f31cb 100644
--- a/app/models/time_tracking/timelog_category.rb
+++ b/app/models/time_tracking/timelog_category.rb
@@ -24,8 +24,7 @@ module TimeTracking
DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
- attribute :color, ::Gitlab::Database::Type::Color.new
- default_value_for :color, DEFAULT_COLOR
+ attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR
def self.find_by_name(namespace_id, name)
where(namespace: namespace_id)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 634fa9e7eda..f2fa0df852a 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -94,7 +94,7 @@ class Todo < ApplicationRecord
#
# Returns an `ActiveRecord::Relation`.
def for_group_ids_and_descendants(group_ids)
- groups = Group.groups_including_descendants_by(group_ids)
+ groups = Group.where(id: group_ids).self_and_descendants
from_union(
[
diff --git a/app/models/user.rb b/app/models/user.rb
index 6d198fc755b..24f947183a2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -83,7 +83,10 @@ class User < ApplicationRecord
serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable,
- :validatable, :omniauthable, :confirmable, :registerable, :pbkdf2_encryptable
+ :validatable, :omniauthable, :confirmable, :registerable
+
+ # Must be included after `devise`
+ include EncryptedUserPassword
include AdminChangedPasswordNotifier
@@ -185,7 +188,7 @@ class User < ApplicationRecord
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, through: :project_members
- has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
+ has_many :created_projects, foreign_key: :creator_id, class_name: 'Project', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :projects_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :project_members, source: :project
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :starred_projects, through: :users_star_projects, source: :project
@@ -258,6 +261,8 @@ class User < ApplicationRecord
has_many :resource_state_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :authored_events, class_name: 'Event', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :namespace_commit_emails
+
#
# Validations
#
@@ -420,10 +425,6 @@ class User < ApplicationRecord
end
# rubocop: disable CodeReuse/ServiceClass
- # Ideally we should not call a service object here but user.block
- # is also called by Users::MigrateToGhostUserService which references
- # this state transition object in order to do a rollback.
- # For this reason the tradeoff is to disable this cop.
after_transition any => :blocked do |user|
user.run_after_commit do
Ci::DropPipelineService.new.execute_async_for_all(user.pipelines, :user_blocked, user)
@@ -447,6 +448,14 @@ class User < ApplicationRecord
after_transition banned: :active do |user|
user.banned_user&.destroy
end
+
+ after_transition any => :active do |user|
+ user.starred_projects.update_counters(star_count: 1)
+ end
+
+ after_transition active: any do |user|
+ user.starred_projects.update_counters(star_count: -1)
+ end
end
# Scopes
@@ -693,6 +702,8 @@ class User < ApplicationRecord
#
# Returns an ActiveRecord::Relation.
def search(query, **options)
+ return none unless query.is_a?(String)
+
query = query&.delete_prefix('@')
return none if query.blank?
@@ -937,26 +948,14 @@ class User < ApplicationRecord
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
- def authenticatable_salt
- return encrypted_password[0, 29] unless Feature.enabled?(:pbkdf2_password_encryption)
- return super if password_strategy == :pbkdf2_sha512
-
- encrypted_password[0, 29]
- end
-
# Overwrites valid_password? from Devise::Models::DatabaseAuthenticatable
# In constant-time, check both that the password isn't on a denylist AND
# that the password is the user's password
def valid_password?(password)
return false unless password_allowed?(password)
return false if password_automatically_set?
- return super if Feature.enabled?(:pbkdf2_password_encryption)
- Devise::Encryptor.compare(self.class, encrypted_password, password)
- rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash
- validate_and_migrate_bcrypt_password(password)
- rescue ::BCrypt::Errors::InvalidHash
- false
+ super
end
def generate_otp_backup_codes!
@@ -975,27 +974,6 @@ class User < ApplicationRecord
end
end
- # This method should be removed once the :pbkdf2_password_encryption feature flag is removed.
- def password=(new_password)
- if Feature.enabled?(:pbkdf2_password_encryption) && Feature.enabled?(:pbkdf2_password_encryption_write, self)
- super
- else
- # Copied from Devise DatabaseAuthenticatable.
- @password = new_password
- self.encrypted_password = Devise::Encryptor.digest(self.class, new_password) if new_password.present?
- end
- end
-
- def password_strategy
- super
- rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash
- begin
- return :bcrypt if BCrypt::Password.new(encrypted_password)
- rescue BCrypt::Errors::InvalidHash
- :unknown
- end
- end
-
# See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638
DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze
@@ -1224,6 +1202,10 @@ class User < ApplicationRecord
authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
+ def preloaded_member_roles_for_projects(projects)
+ # overridden in EE
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def require_ssh_key?
count = Users::KeysCountService.new(self).count
@@ -1786,7 +1768,7 @@ class User < ApplicationRecord
end
def owns_runner?(runner)
- ci_owned_runners.exists?(runner.id)
+ ci_owned_runners.include?(runner)
end
def notification_email_for(notification_group)
@@ -2440,15 +2422,6 @@ class User < ApplicationRecord
Ci::NamespaceMirror.contains_traversal_ids(traversal_ids)
end
-
- def validate_and_migrate_bcrypt_password(password)
- return false unless Devise::Encryptor.compare(self.class, encrypted_password, password)
- return true unless Feature.enabled?(:pbkdf2_password_encryption_write, self)
-
- update_attribute(:password, password)
- rescue ::BCrypt::Errors::InvalidHash
- false
- end
end
User.prepend_mod_with('User')
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index ae6950d800c..b037d07658d 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -62,7 +62,8 @@ module Users
namespace_storage_limit_banner_error_threshold: 58, # EE-only
project_quality_summary_feedback: 59, # EE-only
merge_request_settings_moved_callout: 60,
- new_top_level_group_alert: 61
+ new_top_level_group_alert: 61,
+ artifacts_management_page_feedback_banner: 62
}
validates :feature_name,
diff --git a/app/models/users/ghost_user_migration.rb b/app/models/users/ghost_user_migration.rb
index 1d93498e88b..4578e0503c3 100644
--- a/app/models/users/ghost_user_migration.rb
+++ b/app/models/users/ghost_user_migration.rb
@@ -8,5 +8,7 @@ module Users
belongs_to :initiator_user, class_name: 'User'
validates :user_id, presence: true
+
+ scope :consume_order, -> { order(:consume_after, :id) }
end
end
diff --git a/app/models/users/namespace_commit_email.rb b/app/models/users/namespace_commit_email.rb
new file mode 100644
index 00000000000..4ec02f12717
--- /dev/null
+++ b/app/models/users/namespace_commit_email.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Users
+ class NamespaceCommitEmail < ApplicationRecord
+ belongs_to :user
+ belongs_to :namespace
+ belongs_to :email
+
+ validates :user, presence: true
+ validates :namespace, presence: true
+ validates :email, presence: true
+ validates :user_id, uniqueness: { scope: [:namespace_id] }
+ end
+end
diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb
index 9a514b82506..6cffc97822d 100644
--- a/app/models/users_star_project.rb
+++ b/app/models/users_star_project.rb
@@ -3,7 +3,7 @@
class UsersStarProject < ApplicationRecord
include Sortable
- belongs_to :project, counter_cache: :star_count
+ belongs_to :project
belongs_to :user
validates :user, presence: true
@@ -12,6 +12,10 @@ class UsersStarProject < ApplicationRecord
alias_attribute :starred_since, :created_at
+ after_create :increment_project_star_count
+ after_destroy :decrement_project_star_count
+
+ scope :with_active_user, -> { joins(:user).merge(User.with_state(:active)) }
scope :order_user_name_asc, -> { joins(:user).merge(User.order_name_asc) }
scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) }
scope :by_project, -> (project) { where(project_id: project.id) }
@@ -35,4 +39,14 @@ class UsersStarProject < ApplicationRecord
joins(:user).merge(User.search(query, use_minimum_char_limit: false))
end
end
+
+ private
+
+ def increment_project_star_count
+ Project.update_counters(project, star_count: 1) if user.active?
+ end
+
+ def decrement_project_star_count
+ Project.update_counters(project, star_count: -1) if user.active?
+ end
end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index b718c3a096f..57488749b76 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -190,7 +190,7 @@ class Wiki
end
def empty?
- !repository_exists? || list_page_paths.empty?
+ !repository_exists? || list_page_paths(limit: 1).empty?
end
def exists?
@@ -207,9 +207,29 @@ class Wiki
#
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def list_pages(limit: 0, direction: DIRECTION_ASC, load_content: false)
+ def list_pages(direction: DIRECTION_ASC, load_content: false, limit: 0, offset: 0)
create_wiki_repository unless repository_exists?
- list_pages_with_repository_rpcs(limit: limit, direction: direction, load_content: load_content)
+
+ paths = list_page_paths(limit: limit, offset: offset)
+ return [] if paths.empty?
+
+ pages = paths.map do |path|
+ page = Gitlab::Git::WikiPage.new(
+ url_path: sluggified_title(strip_extension(path)),
+ title: canonicalize_filename(path),
+ format: find_page_format(path),
+ path: sluggified_title(path),
+ raw_data: '',
+ name: canonicalize_filename(path),
+ historical: false
+ )
+ WikiPage.new(self, page)
+ end
+ sort_pages!(pages, direction)
+ pages = pages.take(limit) if limit > 0
+ fetch_pages_content!(pages) if load_content
+
+ pages
end
def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
@@ -229,7 +249,27 @@ class Wiki
# Returns an initialized WikiPage instance or nil
def find_page(title, version = nil, load_content: true)
create_wiki_repository unless repository_exists?
- find_page_with_repository_rpcs(title, version, load_content: load_content)
+
+ version = version.presence || default_branch
+ path = find_matched_file(title, version)
+ return if path.blank?
+
+ blob_options = load_content ? {} : { limit: 0 }
+ blob = repository.blob_at(version, path, **blob_options)
+ commit = repository.commit(blob.commit_id)
+ format = find_page_format(path)
+
+ page = Gitlab::Git::WikiPage.new(
+ url_path: sluggified_title(strip_extension(path)),
+ title: canonicalize_filename(path),
+ format: format,
+ path: sluggified_title(path),
+ raw_data: blob.data,
+ name: canonicalize_filename(path),
+ historical: version == default_branch ? false : check_page_historical(path, commit),
+ version: Gitlab::Git::WikiPageVersion.new(commit, format)
+ )
+ WikiPage.new(self, page)
end
def find_sidebar(version = nil)
@@ -315,12 +355,6 @@ class Wiki
[title, title_array.join("/")]
end
- # TODO: This method is redundant. Should be replaced by create_wiki_repository
- def ensure_repository
- create_wiki_repository
- raise CouldNotCreateWikiError unless repository_exists?
- end
-
def hook_attrs
{
web_url: web_url,
@@ -457,7 +491,7 @@ class Wiki
escaped_path = RE2::Regexp.escape(sluggified_title(title))
path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{file_extension_regexp})$")
- matched_files = repository.search_files_by_regexp(path_regexp, version)
+ matched_files = repository.search_files_by_regexp(path_regexp, version, limit: 1)
return if matched_files.blank?
Gitlab::EncodingHelper.encode_utf8_no_detect(matched_files.first)
@@ -472,29 +506,6 @@ class Wiki
repository.last_commit_for_path(default_branch, path)&.id != commit&.id
end
- def find_page_with_repository_rpcs(title, version, load_content: true)
- version = version.presence || default_branch
- path = find_matched_file(title, version)
- return if path.blank?
-
- blob_options = load_content ? {} : { limit: 0 }
- blob = repository.blob_at(version, path, **blob_options)
- commit = repository.commit(blob.commit_id)
- format = find_page_format(path)
-
- page = Gitlab::Git::WikiPage.new(
- url_path: sluggified_title(strip_extension(path)),
- title: canonicalize_filename(path),
- format: format,
- path: sluggified_title(path),
- raw_data: blob.data,
- name: canonicalize_filename(path),
- historical: version == default_branch ? false : check_page_historical(path, commit),
- version: Gitlab::Git::WikiPageVersion.new(commit, format)
- )
- WikiPage.new(self, page)
- end
-
def file_extension_regexp
# We could not use ALLOWED_EXTENSIONS_REGEX constant or similar regexp with
# Regexp.union. The result combination complicated modifiers:
@@ -509,34 +520,11 @@ class Wiki
path.sub(/\.[^.]+\z/, "")
end
- def list_page_paths
+ def list_page_paths(limit: 0, offset: 0)
return [] if repository.empty?
path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)\\.(#{file_extension_regexp})$")
- repository.search_files_by_regexp(path_regexp, default_branch)
- end
-
- def list_pages_with_repository_rpcs(limit:, direction:, load_content:)
- paths = list_page_paths
- return [] if paths.empty?
-
- pages = paths.map do |path|
- page = Gitlab::Git::WikiPage.new(
- url_path: sluggified_title(strip_extension(path)),
- title: canonicalize_filename(path),
- format: find_page_format(path),
- path: sluggified_title(path),
- raw_data: '',
- name: canonicalize_filename(path),
- historical: false
- )
- WikiPage.new(self, page)
- end
- sort_pages!(pages, direction)
- pages = pages.take(limit) if limit > 0
- fetch_pages_content!(pages) if load_content
-
- pages
+ repository.search_files_by_regexp(path_regexp, default_branch, limit: limit, offset: offset)
end
# After migrating to normal repository RPCs, it's very expensive to sort the
@@ -552,7 +540,7 @@ class Wiki
def fetch_pages_content!(pages)
blobs =
repository
- .blobs_at(pages.map { |page| [default_branch, page.path] } )
+ .blobs_at(pages.map { |page| [default_branch, page.path] })
.to_h { |blob| [blob.path, blob.data] }
pages.each do |page|
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 05e45fa5b29..ed6f9d161a6 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -16,8 +16,14 @@ class WorkItem < Issue
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
- def self.assignee_association_name
- 'issue'
+ class << self
+ def assignee_association_name
+ 'issue'
+ end
+
+ def test_reports_join_column
+ 'issues.id'
+ end
end
def noteable_target_type_name
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 753fcbcb8f9..dc30899d24f 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -12,20 +12,27 @@ module WorkItems
# Base types need to exist on the DB on app startup
# This constant is used by the DB seeder
+ # TODO - where to add new icon names created?
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
- task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }
+ task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 },
+ objective: { name: 'Objective', icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only
+ key_result: { name: 'Key Result', icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
}.freeze
WIDGETS_FOR_TYPE = {
- issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate],
+ issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
+ Widgets::Milestone],
incident: [Widgets::Description, Widgets::Hierarchy],
test_case: [Widgets::Description],
requirement: [Widgets::Description],
- task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate]
+ task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
+ Widgets::Milestone],
+ objective: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::Milestone],
+ key_result: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::StartAndDueDate]
}.freeze
WI_TYPES_WITH_CREATED_HEADER = %w[issue incident].freeze
@@ -67,7 +74,7 @@ module WorkItems
end
def self.allowed_types_for_issues
- base_types.keys.excluding('task')
+ base_types.keys.excluding('task', 'objective', 'key_result')
end
def default?
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
index 930aced8ace..d0819076efd 100644
--- a/app/models/work_items/widgets/hierarchy.rb
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -4,14 +4,10 @@ module WorkItems
module Widgets
class Hierarchy < Base
def parent
- return unless work_item.project.work_items_feature_flag_enabled?
-
work_item.work_item_parent
end
def children
- return WorkItem.none unless work_item.project.work_items_feature_flag_enabled?
-
work_item.work_item_children
end
end
diff --git a/app/models/work_items/widgets/milestone.rb b/app/models/work_items/widgets/milestone.rb
new file mode 100644
index 00000000000..a3e610110f1
--- /dev/null
+++ b/app/models/work_items/widgets/milestone.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Milestone < Base
+ delegate :milestone, to: :work_item
+ end
+ end
+end