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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/abuse/trust_score.rb15
-rw-r--r--app/models/abuse/user_trust_score.rb53
-rw-r--r--app/models/ai/service_access_token.rb25
-rw-r--r--app/models/alert_management/http_integration.rb21
-rw-r--r--app/models/analytics/cycle_analytics/stage.rb13
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb10
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/audit_event.rb34
-rw-r--r--app/models/award_emoji.rb11
-rw-r--r--app/models/broadcast_message.rb3
-rw-r--r--app/models/bulk_import.rb8
-rw-r--r--app/models/bulk_imports/batch_tracker.rb4
-rw-r--r--app/models/bulk_imports/entity.rb23
-rw-r--r--app/models/bulk_imports/export.rb11
-rw-r--r--app/models/bulk_imports/export_status.rb40
-rw-r--r--app/models/bulk_imports/tracker.rb5
-rw-r--r--app/models/ci/artifact_blob.rb25
-rw-r--r--app/models/ci/bridge.rb107
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/build_need.rb6
-rw-r--r--app/models/ci/build_pending_state.rb3
-rw-r--r--app/models/ci/build_report_result.rb3
-rw-r--r--app/models/ci/build_runner_session.rb3
-rw-r--r--app/models/ci/build_trace_chunk.rb7
-rw-r--r--app/models/ci/build_trace_metadata.rb3
-rw-r--r--app/models/ci/catalog/resource.rb2
-rw-r--r--app/models/ci/external_pull_request.rb106
-rw-r--r--app/models/ci/group_variable.rb16
-rw-r--r--app/models/ci/job_artifact.rb3
-rw-r--r--app/models/ci/job_variable.rb3
-rw-r--r--app/models/ci/pending_build.rb3
-rw-r--r--app/models/ci/persistent_ref.rb7
-rw-r--r--app/models/ci/pipeline.rb12
-rw-r--r--app/models/ci/pipeline_variable.rb5
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_manager.rb13
-rw-r--r--app/models/ci/running_build.rb3
-rw-r--r--app/models/ci/sources/pipeline.rb3
-rw-r--r--app/models/ci/stage.rb5
-rw-r--r--app/models/ci/unit_test_failure.rb3
-rw-r--r--app/models/ci/variable.rb1
-rw-r--r--app/models/clusters/agent.rb2
-rw-r--r--app/models/clusters/concerns/prometheus_client.rb2
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/commit_range.rb2
-rw-r--r--app/models/commit_status.rb4
-rw-r--r--app/models/concerns/commit_signature.rb3
-rw-r--r--app/models/concerns/database_event_tracking.rb15
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb3
-rw-r--r--app/models/concerns/enums/vulnerability.rb8
-rw-r--r--app/models/concerns/expirable.rb6
-rw-r--r--app/models/concerns/has_user_type.rb2
-rw-r--r--app/models/concerns/ignorable_columns.rb2
-rw-r--r--app/models/concerns/issue_available_features.rb2
-rw-r--r--app/models/concerns/issues/forbid_issue_type_column_usage.rb59
-rw-r--r--app/models/concerns/milestoneish.rb5
-rw-r--r--app/models/concerns/packages/debian/component_file.rb2
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/protected_ref.rb7
-rw-r--r--app/models/concerns/protected_ref_access.rb22
-rw-r--r--app/models/concerns/protected_ref_deploy_key_access.rb61
-rw-r--r--app/models/concerns/spammable.rb6
-rw-r--r--app/models/concerns/triggerable_hooks.rb3
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb1
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb9
-rw-r--r--app/models/container_repository.rb4
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/design_management/repository.rb2
-rw-r--r--app/models/environment.rb20
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/external_pull_request.rb106
-rw-r--r--app/models/group.rb100
-rw-r--r--app/models/hooks/project_hook.rb3
-rw-r--r--app/models/hooks/web_hook_log.rb2
-rw-r--r--app/models/integration.rb9
-rw-r--r--app/models/integrations/base_chat_notification.rb4
-rw-r--r--app/models/integrations/base_slack_notification.rb11
-rw-r--r--app/models/integrations/chat_message/group_mention_message.rb102
-rw-r--r--app/models/integrations/hangouts_chat.rb37
-rw-r--r--app/models/integrations/microsoft_teams.rb37
-rw-r--r--app/models/integrations/prometheus.rb6
-rw-r--r--app/models/integrations/unify_circuit.rb34
-rw-r--r--app/models/integrations/webex_teams.rb37
-rw-r--r--app/models/issue.rb103
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/group_member.rb1
-rw-r--r--app/models/merge_request.rb28
-rw-r--r--app/models/merge_request/diff_llm_summary.rb14
-rw-r--r--app/models/merge_request/metrics.rb2
-rw-r--r--app/models/milestone.rb8
-rw-r--r--app/models/ml/experiment.rb12
-rw-r--r--app/models/ml/model.rb25
-rw-r--r--app/models/ml/model_version.rb38
-rw-r--r--app/models/namespace.rb9
-rw-r--r--app/models/namespaces/traversal/linear.rb33
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb16
-rw-r--r--app/models/note.rb12
-rw-r--r--app/models/organizations/organization.rb8
-rw-r--r--app/models/organizations/organization_setting.rb20
-rw-r--r--app/models/organizations/organization_user.rb8
-rw-r--r--app/models/packages/npm/metadatum.rb7
-rw-r--r--app/models/packages/package.rb9
-rw-r--r--app/models/pages/lookup_path.rb11
-rw-r--r--app/models/personal_access_token.rb11
-rw-r--r--app/models/plan_limits.rb36
-rw-r--r--app/models/project.rb80
-rw-r--r--app/models/project_ci_cd_setting.rb3
-rw-r--r--app/models/project_statistics.rb1
-rw-r--r--app/models/projects/topic.rb2
-rw-r--r--app/models/projects/triggered_hooks.rb2
-rw-r--r--app/models/protected_branch/push_access_level.rb44
-rw-r--r--app/models/protected_tag/create_access_level.rb45
-rw-r--r--app/models/release.rb4
-rw-r--r--app/models/remote_mirror.rb1
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/service_desk_setting.rb9
-rw-r--r--app/models/system_access.rb7
-rw-r--r--app/models/todo.rb13
-rw-r--r--app/models/user.rb67
-rw-r--r--app/models/user_custom_attribute.rb2
-rw-r--r--app/models/user_preference.rb4
-rw-r--r--app/models/users/callout.rb19
-rw-r--r--app/models/users/group_callout.rb16
-rw-r--r--app/models/webauthn_registration.rb4
-rw-r--r--app/models/work_item.rb10
-rw-r--r--app/models/work_items/widgets/base.rb4
-rw-r--r--app/models/work_items/widgets/current_user_todos.rb13
127 files changed, 1228 insertions, 931 deletions
diff --git a/app/models/abuse/trust_score.rb b/app/models/abuse/trust_score.rb
index b7ed504a0ba..2e8b7ed6686 100644
--- a/app/models/abuse/trust_score.rb
+++ b/app/models/abuse/trust_score.rb
@@ -2,9 +2,6 @@
module Abuse
class TrustScore < ApplicationRecord
- MAX_EVENTS = 100
- SPAMCHECK_HAM_THRESHOLD = 0.5
-
self.table_name = 'abuse_trust_scores'
enum source: Enums::Abuse::Source.sources
@@ -15,6 +12,9 @@ module Abuse
validates :score, presence: true
validates :source, presence: true
+ scope :order_created_at_asc, -> { order(created_at: :asc) }
+ scope :order_created_at_desc, -> { order(created_at: :desc) }
+
before_create :assign_correlation_id
after_commit :remove_old_scores
@@ -25,14 +25,7 @@ module Abuse
end
def remove_old_scores
- count = user.trust_scores_for_source(source).count
- return unless count > MAX_EVENTS
-
- TrustScore.delete(
- user.trust_scores_for_source(source)
- .order(created_at: :asc)
- .limit(count - MAX_EVENTS)
- )
+ Abuse::UserTrustScore.new(user).remove_old_scores(source)
end
end
end
diff --git a/app/models/abuse/user_trust_score.rb b/app/models/abuse/user_trust_score.rb
new file mode 100644
index 00000000000..3a935e230ae
--- /dev/null
+++ b/app/models/abuse/user_trust_score.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Abuse
+ class UserTrustScore
+ MAX_EVENTS = 100
+ SPAMCHECK_HAM_THRESHOLD = 0.5
+
+ def initialize(user)
+ @user = user
+ end
+
+ def spammer?
+ spam_score > SPAMCHECK_HAM_THRESHOLD
+ end
+
+ def spam_score
+ user_scores.spamcheck.average(:score) || 0.0
+ end
+
+ def telesign_score
+ user_scores.telesign.order_created_at_desc.first&.score || 0.0
+ end
+
+ def arkose_global_score
+ user_scores.arkose_global_score.order_created_at_desc.first&.score || 0.0
+ end
+
+ def arkose_custom_score
+ user_scores.arkose_custom_score.order_created_at_desc.first&.score || 0.0
+ end
+
+ def trust_scores_for_source(source)
+ user_scores.where(source: source)
+ end
+
+ def remove_old_scores(source)
+ count = trust_scores_for_source(source).count
+ return unless count > MAX_EVENTS
+
+ Abuse::TrustScore.delete(
+ trust_scores_for_source(source)
+ .order_created_at_asc
+ .limit(count - MAX_EVENTS)
+ )
+ end
+
+ private
+
+ def user_scores
+ Abuse::TrustScore.where(user_id: @user.id)
+ end
+ end
+end
diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb
new file mode 100644
index 00000000000..863bdfc7899
--- /dev/null
+++ b/app/models/ai/service_access_token.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ai
+ class ServiceAccessToken < ApplicationRecord
+ self.table_name = 'service_access_tokens'
+
+ scope :expired, -> { where('expires_at < :now', now: Time.current) }
+ scope :for_category, ->(category) { where(category: category) }
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+
+ validates :token, :expires_at, presence: true
+
+ enum category: {
+ code_suggestions: 1
+ }
+
+ validates :category, presence: true
+ end
+end
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index d5162865a79..a70168dc0d8 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -4,7 +4,7 @@ module AlertManagement
class HttpIntegration < ApplicationRecord
include ::Gitlab::Routing
- LEGACY_IDENTIFIER = 'legacy'
+ LEGACY_IDENTIFIERS = %w[legacy legacy-prometheus].freeze
belongs_to :project, inverse_of: :alert_management_http_integrations
@@ -20,8 +20,8 @@ module AlertManagement
validates :token, presence: true, format: { with: /\A\h{32}\z/ }
validates :name, presence: true, length: { maximum: 255 }
validates :type_identifier, presence: true
- validates :endpoint_identifier, presence: true, length: { maximum: 255 }, format: { with: /\A[A-Za-z0-9]+\z/ }
- validates :endpoint_identifier, uniqueness: { scope: [:project_id, :active] }, if: :active?
+ validates :endpoint_identifier, presence: true, length: { maximum: 255 }, format: { with: /\A[A-Za-z0-9-]+\z/ }
+ validates :endpoint_identifier, uniqueness: { scope: [:project_id] }
validates :payload_attribute_mapping, json_schema: { filename: 'http_integration_payload_attribute_mapping' }
before_validation :prevent_token_assignment
@@ -33,7 +33,6 @@ module AlertManagement
scope :for_type, ->(type) { where(type_identifier: type) }
scope :for_project, ->(project_ids) { where(project: project_ids) }
scope :active, -> { where(active: true) }
- scope :legacy, -> { for_endpoint_identifier(LEGACY_IDENTIFIER) }
scope :ordered_by_type_and_id, -> { order(:type_identifier, :id) }
enum type_identifier: {
@@ -42,16 +41,18 @@ module AlertManagement
}
def url
- if legacy?
- return project_alerts_notify_url(project, format: :json) if http?
- return notify_project_prometheus_alerts_url(project, format: :json) if prometheus?
+ case endpoint_identifier
+ when 'legacy'
+ project_alerts_notify_url(project, format: :json)
+ when 'legacy-prometheus'
+ notify_project_prometheus_alerts_url(project, format: :json)
+ else
+ project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json)
end
-
- project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json)
end
def legacy?
- endpoint_identifier == LEGACY_IDENTIFIER
+ LEGACY_IDENTIFIERS.include?(endpoint_identifier)
end
private
diff --git a/app/models/analytics/cycle_analytics/stage.rb b/app/models/analytics/cycle_analytics/stage.rb
index c7bff7c8d7f..6f152e7749e 100644
--- a/app/models/analytics/cycle_analytics/stage.rb
+++ b/app/models/analytics/cycle_analytics/stage.rb
@@ -3,6 +3,8 @@
module Analytics
module CycleAnalytics
class Stage < ApplicationRecord
+ MAX_STAGES_PER_VALUE_STREAM = 15
+
self.table_name = :analytics_cycle_analytics_group_stages
include DatabaseEventTracking
@@ -10,6 +12,8 @@ module Analytics
include Analytics::CycleAnalytics::Parentable
validates :name, uniqueness: { scope: [:group_id, :group_value_stream_id] }
+ validate :max_stages_count, on: :create
+
belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ValueStream',
foreign_key: :group_value_stream_id, inverse_of: :stages
@@ -49,6 +53,15 @@ module Analytics
name
group_value_stream_id
].freeze
+
+ private
+
+ def max_stages_count
+ return unless value_stream
+ return unless value_stream.stages.count >= MAX_STAGES_PER_VALUE_STREAM
+
+ errors.add(:value_stream, _('Maximum number of stages per value stream exceeded'))
+ end
end
end
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 31e06075bcb..16446a5b463 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -3,6 +3,8 @@
module Analytics
module CycleAnalytics
class ValueStream < ApplicationRecord
+ MAX_VALUE_STREAMS_PER_NAMESPACE = 50
+
self.table_name = :analytics_cycle_analytics_group_value_streams
include Analytics::CycleAnalytics::Parentable
@@ -15,6 +17,7 @@ module Analytics
validates :name, presence: true
validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :group_id }
+ validate :max_value_streams_count, on: :create
accepts_nested_attributes_for :stages, allow_destroy: true
@@ -35,6 +38,13 @@ module Analytics
private
+ def max_value_streams_count
+ return unless namespace
+ return unless namespace.value_streams.count >= MAX_VALUE_STREAMS_PER_NAMESPACE
+
+ errors.add(:namespace, _('Maximum number of value streams per namespace exceeded'))
+ end
+
def ensure_aggregation_record_presence
Analytics::CycleAnalytics::Aggregation.safe_create_for_namespace(namespace)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a71b47e88d8..827f8bc93be 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -38,7 +38,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
encrypted_tofa_url
encrypted_tofa_url_iv
vertex_project
- ], remove_with: '16.2', remove_after: '2023-06-22'
+ ], remove_with: '16.3', remove_after: '2023-07-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -596,6 +596,13 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
allow_blank: true,
public_url: ADDRESSABLE_URL_VALIDATION_OPTIONS
+ with_options(presence: true, if: :slack_app_enabled?) do
+ validates :slack_app_id
+ validates :slack_app_secret
+ validates :slack_app_signing_secret
+ validates :slack_app_verification_token
+ end
+
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
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 9370982be47..163e741d990 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -100,40 +100,6 @@ class AuditEvent < ApplicationRecord
super || details[:target_details]
end
- def self.by_group(group)
- group_id = group.id
-
- # Bring entity_type and entity_id from projects and group into one query
- scope1 = Group.find(group_id).all_projects.select("'Project' as entity_type", 'id AS entity_id')
- scope2 = Project.from("(VALUES ('Group', #{group_id})) as projects(entity_type, entity_id)").select('entity_type',
- 'entity_id')
- array_scope = Project.from_union([scope1, scope2], remove_duplicates: false).select(:entity_type, :entity_id)
-
- # order by created_at (id is the tie breaker)
- scope = AuditEvent.order(:created_at, :id)
-
- array_mapping_scope = ->(entity_type_expression, entity_id_expression) do
- AuditEvent.where(AuditEvent.arel_table[:entity_id].eq(entity_id_expression))
- .where(AuditEvent.arel_table[:entity_type].eq(entity_type_expression))
- end
-
- finder_query = ->(created_at_expression, id_expression) do
- # we need to add created_at filter as well because that's the partitioning key
- AuditEvent.where(
- AuditEvent.arel_table[:id].eq(id_expression)
- ).where(
- AuditEvent.arel_table[:created_at].eq(created_at_expression)
- )
- end
-
- Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
- scope: scope,
- array_scope: array_scope,
- array_mapping_scope: array_mapping_scope,
- finder_query: finder_query
- ).execute
- end
-
private
def sanitize_message
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 31bee8db1b4..ebc43b04b1b 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -31,6 +31,7 @@ class AwardEmoji < ApplicationRecord
after_destroy :expire_cache
after_save :expire_cache
+ after_commit :broadcast_note_update, if: -> { !importing? && awardable.is_a?(Note) }
class << self
def votes_for_collection(ids, type)
@@ -73,11 +74,19 @@ class AwardEmoji < ApplicationRecord
def expire_cache
awardable.try(:bump_updated_at)
- awardable.expire_etag_cache if awardable.is_a?(Note)
awardable.try(:update_upvotes_count) if upvote?
end
+ def broadcast_note_update
+ awardable.expire_etag_cache
+ awardable.trigger_note_subscription_update
+ end
+
def to_ability_name
'emoji'
end
+
+ def hook_attrs
+ Gitlab::HookData::EmojiBuilder.new(self).build
+ end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index bf25ea7367c..ccc5ca7395d 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -3,7 +3,6 @@
class BroadcastMessage < MainClusterwide::ApplicationRecord
include CacheMarkdownField
include Sortable
- include IgnorableColumns
ALLOWED_TARGET_ACCESS_LEVELS = [
Gitlab::Access::GUEST,
@@ -13,8 +12,6 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord
Gitlab::Access::OWNER
].freeze
- ignore_column :namespace_id, remove_with: '16.0', remove_after: '2022-06-22'
-
cache_markdown_field :message, pipeline: :broadcast_message, whitelisted: true
validates :message, presence: true
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index c2d7529f468..fde528e3fa0 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -58,6 +58,10 @@ class BulkImport < ApplicationRecord
Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT)
end
+ def self.min_gl_version_for_migration_in_batches
+ Gitlab::VersionInfo.new(16, 2)
+ end
+
def self.all_human_statuses
state_machine.states.map(&:human_name)
end
@@ -68,4 +72,8 @@ class BulkImport < ApplicationRecord
update!(has_failures: true)
end
+
+ def supports_batched_export?
+ source_version_info >= self.class.min_gl_version_for_migration_in_batches
+ end
end
diff --git a/app/models/bulk_imports/batch_tracker.rb b/app/models/bulk_imports/batch_tracker.rb
index df1fab89ee6..2e79d41d46e 100644
--- a/app/models/bulk_imports/batch_tracker.rb
+++ b/app/models/bulk_imports/batch_tracker.rb
@@ -25,9 +25,7 @@ module BulkImports
end
event :finish do
- transition started: :finished
- transition failed: :failed
- transition skipped: :skipped
+ transition any => :finished
end
event :skip do
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 94e4a8165eb..4f50a112141 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -144,12 +144,27 @@ class BulkImports::Entity < ApplicationRecord
end
end
- def export_relations_url_path
- "#{base_resource_path}/export_relations"
+ def export_relations_url_path_base
+ File.join(base_resource_path, 'export_relations')
end
- def relation_download_url_path(relation)
- "#{export_relations_url_path}/download?relation=#{relation}"
+ def export_relations_url_path(batched: false)
+ if batched && bulk_import.supports_batched_export?
+ Gitlab::Utils.add_url_parameters(export_relations_url_path_base, batched: batched)
+ else
+ export_relations_url_path_base
+ end
+ end
+
+ def relation_download_url_path(relation, batch_number = nil)
+ url = File.join(export_relations_url_path_base, 'download')
+ params = { relation: relation }
+
+ if batch_number && bulk_import.supports_batched_export?
+ params.merge!(batched: true, batch_number: batch_number)
+ end
+
+ Gitlab::Utils.add_url_parameters(url, params)
end
def wikis_url_path
diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb
index 93cf047c690..5c3f8e4b8d4 100644
--- a/app/models/bulk_imports/export.rb
+++ b/app/models/bulk_imports/export.rb
@@ -32,9 +32,7 @@ module BulkImports
end
event :finish do
- transition started: :finished
- transition finished: :finished
- transition failed: :failed
+ transition any => :finished
end
event :fail_op do
@@ -63,5 +61,12 @@ module BulkImports
FileTransfer.config_for(portable)
end
end
+
+ def remove_existing_upload!
+ return unless upload&.export_file&.file
+
+ upload.remove_export_file!
+ upload.save!
+ end
end
end
diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb
index cbd7b189007..3d820e65d5b 100644
--- a/app/models/bulk_imports/export_status.rb
+++ b/app/models/bulk_imports/export_status.rb
@@ -13,28 +13,48 @@ module BulkImports
end
def started?
- !empty? && export_status['status'] == Export::STARTED
+ !empty? && status['status'] == Export::STARTED
end
def failed?
- !empty? && export_status['status'] == Export::FAILED
+ !empty? && status['status'] == Export::FAILED
end
def empty?
- export_status.nil?
+ status.nil?
end
def error
- export_status['error']
+ status['error']
+ end
+
+ def batched?
+ status['batched'] == true
+ end
+
+ def batches_count
+ status['batches_count'].to_i
+ end
+
+ def batch(batch_number)
+ raise ArgumentError if batch_number < 1
+
+ return unless batched?
+
+ status['batches'].find { |item| item['batch_number'] == batch_number }
end
private
attr_reader :client, :entity, :relation, :pipeline_tracker
- def export_status
- strong_memoize(:export_status) do
- fetch_export_status&.find { |item| item['relation'] == relation }
+ def status
+ strong_memoize(:status) do
+ status = fetch_status
+
+ next status if status.is_a?(Hash) || status.nil?
+
+ status.find { |item| item['relation'] == relation }
rescue BulkImports::NetworkError => e
raise BulkImports::RetryPipelineError.new(e.message, 2.seconds) if e.retriable?(pipeline_tracker)
@@ -44,12 +64,12 @@ module BulkImports
end
end
- def fetch_export_status
- client.get(status_endpoint).parsed_response
+ def fetch_status
+ client.get(status_endpoint, relation: relation).parsed_response
end
def status_endpoint
- File.join(entity.export_relations_url_path, 'status')
+ File.join(entity.export_relations_url_path_base, 'status')
end
def default_error_response(message)
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 55502721a76..d1a6f3b9a80 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -24,6 +24,7 @@ class BulkImports::Tracker < ApplicationRecord
delegate :file_extraction_pipeline?, to: :pipeline_class
DEFAULT_PAGE_SIZE = 500
+ STALE_AFTER = 4.hours
scope :next_pipeline_trackers_for, -> (entity_id) {
entity_scope = where(bulk_import_entity_id: entity_id)
@@ -89,4 +90,8 @@ class BulkImports::Tracker < ApplicationRecord
transition [:created, :started] => :timeout
end
end
+
+ def stale?
+ created_at < STALE_AFTER.ago
+ end
end
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index f87b18d516f..1f6d218b015 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -4,8 +4,6 @@ module Ci
class ArtifactBlob
include BlobLike
- EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .xml .log].freeze
-
attr_reader :entry
def initialize(entry)
@@ -35,31 +33,18 @@ module Ci
:build_artifact
end
- def external_url(project, job)
- return unless external_link?(job)
-
- url_project_path = project.full_path.partition('/').last
-
- artifact_path = [
- '-', url_project_path, '-',
- 'jobs', job.id,
- 'artifacts', path
- ].join('/')
-
- "#{project.pages_namespace_url}/#{artifact_path}"
+ def external_url(job)
+ pages_url_builder(job.project).artifact_url(entry, job)
end
def external_link?(job)
- pages_config.enabled &&
- pages_config.artifacts_server &&
- EXTENSIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
- (pages_config.access_control || job.project.public?)
+ pages_url_builder(job.project).artifact_url_available?(entry, job)
end
private
- def pages_config
- Gitlab.config.pages
+ def pages_url_builder(project)
+ @pages_url_builder ||= Gitlab::Pages::UrlBuilder.new(project)
end
end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 7cdd0d56a98..5052d84378f 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -224,15 +224,46 @@ module Ci
end
end
+ def target_revision_ref
+ downstream_pipeline_params.dig(:target_revision, :ref)
+ end
+
def downstream_variables
- calculate_downstream_variables
- .reverse # variables priority
- .uniq { |var| var[:key] } # only one variable key to pass
- .reverse
+ Gitlab::Ci::Variables::Downstream::Generator.new(self).calculate
end
- def target_revision_ref
- downstream_pipeline_params.dig(:target_revision, :ref)
+ def variables
+ strong_memoize(:variables) do
+ Gitlab::Ci::Variables::Collection.new
+ .concat(scoped_variables)
+ .concat(pipeline.persisted_variables)
+ end
+ end
+
+ def pipeline_variables
+ pipeline.variables
+ end
+
+ def pipeline_schedule_variables
+ return [] unless pipeline.pipeline_schedule
+
+ pipeline.pipeline_schedule.variables.to_a
+ end
+
+ def forward_yaml_variables?
+ strong_memoize(:forward_yaml_variables) do
+ result = options&.dig(:trigger, :forward, :yaml_variables)
+
+ result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result
+ end
+ end
+
+ def forward_pipeline_variables?
+ strong_memoize(:forward_pipeline_variables) do
+ result = options&.dig(:trigger, :forward, :pipeline_variables)
+
+ result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result
+ end
end
private
@@ -273,70 +304,6 @@ module Ci
}
}
end
-
- def calculate_downstream_variables
- expand_variables = scoped_variables
- .concat(pipeline.persisted_variables)
- .to_runner_variables
-
- # The order of this list refers to the priority of the variables
- downstream_yaml_variables(expand_variables) +
- downstream_pipeline_variables(expand_variables) +
- downstream_pipeline_schedule_variables(expand_variables)
- end
-
- def downstream_yaml_variables(expand_variables)
- return [] unless forward_yaml_variables?
-
- yaml_variables.to_a.map do |hash|
- if hash[:raw]
- { key: hash[:key], value: hash[:value], raw: true }
- else
- { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) }
- end
- end
- end
-
- def downstream_pipeline_variables(expand_variables)
- return [] unless forward_pipeline_variables?
-
- pipeline.variables.to_a.map do |variable|
- if variable.raw?
- { key: variable.key, value: variable.value, raw: true }
- else
- { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
- end
- end
- end
-
- def downstream_pipeline_schedule_variables(expand_variables)
- return [] unless forward_pipeline_variables?
- return [] unless pipeline.pipeline_schedule
-
- pipeline.pipeline_schedule.variables.to_a.map do |variable|
- if variable.raw?
- { key: variable.key, value: variable.value, raw: true }
- else
- { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
- end
- end
- end
-
- def forward_yaml_variables?
- strong_memoize(:forward_yaml_variables) do
- result = options&.dig(:trigger, :forward, :yaml_variables)
-
- result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result
- end
- end
-
- def forward_pipeline_variables?
- strong_memoize(:forward_pipeline_variables) do
- result = options&.dig(:trigger, :forward, :pipeline_variables)
-
- result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result
- end
- end
end
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 382f861a802..4c723bb7c0c 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -10,11 +10,9 @@ module Ci
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
- include SafelyChangeColumnDefault
self.table_name = 'p_ci_builds_metadata'
self.primary_key = 'id'
- columns_changing_default :partition_id
partitionable scope: :build
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 940221619b3..317f2523f69 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -3,8 +3,12 @@
module Ci
class BuildNeed < Ci::ApplicationRecord
include Ci::Partitionable
- include BulkInsertSafe
include IgnorableColumns
+ include SafelyChangeColumnDefault
+ include BulkInsertSafe
+
+ columns_changing_default :partition_id
+ ignore_column :id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22'
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 966884ae158..0b88f745d78 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -2,6 +2,9 @@
class Ci::BuildPendingState < Ci::ApplicationRecord
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id, inverse_of: :pending_state
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index b2d99fab295..90b621b8da1 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -3,6 +3,9 @@
module Ci
class BuildReportResult < Ci::ApplicationRecord
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
self.primary_key = :build_id
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 5773b6132be..eaa2e1c428e 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -5,6 +5,9 @@ module Ci
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < Ci::ApplicationRecord
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 03b59b19ef1..0a0f401c9d5 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -8,6 +8,9 @@ module Ci
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::OptimisticLocking
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :trace_chunks
@@ -166,7 +169,7 @@ module Ci
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
self.class.with_read_consistency(build) do
- reset.then(&:unsafe_persist_data!)
+ reset.unsafe_persist_data!
end
end
rescue FailedToObtainLockError
@@ -242,7 +245,7 @@ module Ci
##
# We need to so persist data then save a new store identifier before we
# remove data from the previous store to make this operation
- # trasnaction-safe. `unsafe_set_data! calls `save!` because of this
+ # transaction-safe. `unsafe_set_data! calls `save!` because of this
# reason.
#
# TODO consider using callbacks and state machine to remove old data
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index 4c76089617f..c5ad3d19425 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -3,6 +3,9 @@
module Ci
class BuildTraceMetadata < Ci::ApplicationRecord
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
MAX_ATTEMPTS = 5
self.table_name = 'ci_build_trace_metadata'
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 77cfe91ddd6..38603ddfe59 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -19,6 +19,8 @@ module Ci
delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
+ enum state: { draft: 0, published: 1 }
+
def versions
project.releases.order_released_desc
end
diff --git a/app/models/ci/external_pull_request.rb b/app/models/ci/external_pull_request.rb
new file mode 100644
index 00000000000..bd37aa9f85a
--- /dev/null
+++ b/app/models/ci/external_pull_request.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+# This model stores pull requests coming from external providers, such as
+# GitHub, when GitLab project is set as CI/CD only and remote mirror.
+#
+# When setting up a remote mirror with GitHub we subscribe to push and
+# pull_request webhook events. When a pull request is opened on GitHub,
+# a webhook is sent out, we create or update the status of the pull
+# request locally.
+#
+# When the mirror is updated and changes are pushed to branches we check
+# if there are open pull requests for the source and target branch.
+# If so, we create pipelines for external pull requests.
+module Ci
+ class ExternalPullRequest < Ci::ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+ include ShaAttribute
+ include EachBatch
+
+ belongs_to :project
+
+ sha_attribute :source_sha
+ sha_attribute :target_sha
+
+ validates :source_branch, presence: true
+ validates :target_branch, presence: true
+ validates :source_sha, presence: true
+ validates :target_sha, presence: true
+ validates :source_repository, presence: true
+ validates :target_repository, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ open: 1,
+ closed: 2
+ }
+
+ # We currently don't support pull requests from fork, so
+ # we are going to return an error to the webhook
+ validate :not_from_fork
+
+ scope :by_source_branch, ->(branch) { where(source_branch: branch) }
+ scope :by_source_repository, ->(repository) { where(source_repository: repository) }
+
+ # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix
+ self.table_name = 'external_pull_requests'
+
+ def self.create_or_update_from_params(params)
+ find_params = params.slice(:project_id, :source_branch, :target_branch)
+
+ safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
+ yield(pull_request) if block_given?
+ end
+ end
+
+ def actual_branch_head?
+ actual_source_branch_sha == source_sha
+ end
+
+ def from_fork?
+ source_repository != target_repository
+ end
+
+ def source_ref
+ Gitlab::Git::BRANCH_REF_PREFIX + source_branch
+ end
+
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
+ end
+ end
+
+ def modified_paths
+ project.repository.diff_stats(target_sha, source_sha).paths
+ end
+
+ private
+
+ def actual_source_branch_sha
+ project.commit(source_ref)&.sha
+ end
+
+ def not_from_fork
+ return unless from_fork?
+
+ errors.add(:base, _('Pull requests from fork are not supported'))
+ end
+
+ def self.safe_find_or_initialize_and_update(find:, update:)
+ safe_ensure_unique(retries: 1) do
+ model = find_or_initialize_by(find)
+
+ yield(model) if model.update(update) && block_given?
+
+ model
+ end
+ end
+ end
+end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 5522a01758f..25d0228beb0 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -14,6 +14,7 @@ module Ci
alias_attribute :secret_value, :value
+ validates :description, length: { maximum: 255 }, allow_blank: true
validates :key, uniqueness: {
scope: [:group_id, :environment_scope],
message: "(%{value}) has already been taken"
@@ -36,6 +37,12 @@ module Ci
.pluck(:environment_scope)
end
+ # Sorting
+ scope :order_created_asc, -> { reorder(created_at: :asc) }
+ scope :order_created_desc, -> { reorder(created_at: :desc) }
+ scope :order_key_asc, -> { reorder(key: :asc) }
+ scope :order_key_desc, -> { reorder(key: :desc) }
+
self.limit_name = 'group_ci_variables'
self.limit_scope = :group
@@ -50,5 +57,14 @@ module Ci
def group_ci_cd_settings_path
Gitlab::Routing.url_helpers.group_settings_ci_cd_path(group)
end
+
+ def self.sort_by_attribute(method)
+ case method.to_s
+ when 'created_at_asc' then order_created_asc
+ when 'created_at_desc' then order_created_desc
+ when 'key_asc' then order_key_asc
+ when 'key_desc' then order_key_desc
+ end
+ end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 5cd7988837e..11d70e088e9 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -13,6 +13,9 @@ module Ci
include FileStoreMounter
include EachBatch
include Gitlab::Utils::StrongMemoize
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
enum accessibility: { public: 0, private: 1 }, _suffix: true
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index 573999995bc..21c9842399e 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -5,8 +5,11 @@ module Ci
include Ci::Partitionable
include Ci::NewHasVariable
include Ci::RawVariable
+ include SafelyChangeColumnDefault
include BulkInsertSafe
+ columns_changing_default :partition_id
+
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id, inverse_of: :job_variables
partitionable scope: :job
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 14050a1e78e..dc9a8b7a1bf 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -4,6 +4,9 @@ module Ci
class PendingBuild < Ci::ApplicationRecord
include EachBatch
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb
index 57aa1962bd2..f713d5952bc 100644
--- a/app/models/ci/persistent_ref.rb
+++ b/app/models/ci/persistent_ref.rb
@@ -19,6 +19,11 @@ module Ci
false
end
+ # This needs to be kept in sync with `Ci::Pipeline#after_transition` calling `pipeline.persistent_ref.delete`
+ def should_delete?
+ pipeline.status.to_sym.in?(::Ci::Pipeline.stopped_statuses)
+ end
+
def create
create_ref(sha, path)
rescue StandardError => e
@@ -27,6 +32,8 @@ module Ci
end
def delete
+ return unless should_delete?
+
delete_refs(path)
rescue Gitlab::Git::Repository::NoRepository
# no-op
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 6f2939583e0..bd327cfbe7b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -17,6 +17,9 @@ module Ci
include UpdatedAtFilterable
include EachBatch
include FastDestroyAll::Helpers
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
include IgnorableColumns
ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22'
@@ -51,7 +54,7 @@ module Ci
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline', inverse_of: :auto_canceled_pipelines
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
- belongs_to :external_pull_request
+ belongs_to :external_pull_request, class_name: 'Ci::ExternalPullRequest'
belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines
has_internal_id :iid, scope: :project, presence: false,
@@ -335,9 +338,14 @@ module Ci
end
end
+ # This needs to be kept in sync with `Ci::PipelineRef#should_delete?`
after_transition any => ::Ci::Pipeline.stopped_statuses do |pipeline|
pipeline.run_after_commit do
- pipeline.persistent_ref.delete
+ if Feature.enabled?(:pipeline_cleanup_ref_worker_async, pipeline.project)
+ ::Ci::PipelineCleanupRefWorker.perform_async(pipeline.id)
+ else
+ pipeline.persistent_ref.delete
+ end
end
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index f2457af0074..9747f9ef527 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -5,9 +5,12 @@ module Ci
include Ci::Partitionable
include Ci::HasVariable
include Ci::RawVariable
-
include IgnorableColumns
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22'
+ ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.5', remove_after: '2023-10-22'
belongs_to :pipeline
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 6319163b0d7..4eb5c3c9ed2 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -72,6 +72,7 @@ module Ci
has_many :runner_managers, inverse_of: :runner
has_many :builds
+ has_many :running_builds, inverse_of: :runner
has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects, disable_joins: true
has_many :runner_namespaces, inverse_of: :runner, autosave: true
@@ -198,6 +199,7 @@ module Ci
scope :order_created_at_desc, -> { order(created_at: :desc) }
scope :order_token_expires_at_asc, -> { order(token_expires_at: :asc) }
scope :order_token_expires_at_desc, -> { order(token_expires_at: :desc) }
+
scope :with_tags, -> { preload(:tags) }
scope :with_creator, -> { preload(:creator) }
@@ -456,7 +458,7 @@ module Ci
end
new_version = values[:version]
- schedule_runner_version_update(new_version) if new_version && values[:version] != version
+ schedule_runner_version_update(new_version) if new_version && new_version != version
merge_cache_attributes(values)
diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb
index e36024d9f5b..3a3f95a8c69 100644
--- a/app/models/ci/runner_manager.rb
+++ b/app/models/ci/runner_manager.rb
@@ -44,6 +44,10 @@ module Ci
remove_duplicates: false).where(created_some_time_ago)
end
+ scope :for_runner, ->(runner_id) do
+ where(runner_id: runner_id)
+ end
+
def self.online_contact_time_deadline
Ci::Runner.online_contact_time_deadline
end
@@ -52,6 +56,13 @@ module Ci
STALE_TIMEOUT.ago
end
+ def self.aggregate_upgrade_status_by_runner_id
+ joins(:runner_version)
+ .group(:runner_id)
+ .maximum(:status)
+ .transform_values { |s| Ci::RunnerVersion.statuses.key(s).to_sym }
+ end
+
def heartbeat(values, update_contacted_at: true)
##
# We can safely ignore writes performed by a runner heartbeat. We do
@@ -66,7 +77,7 @@ module Ci
end
new_version = values[:version]
- schedule_runner_version_update(new_version) if new_version && values[:version] != version
+ schedule_runner_version_update(new_version) if new_version && new_version != version
merge_cache_attributes(values)
diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb
index e6f80658f5d..cfdc47de531 100644
--- a/app/models/ci/running_build.rb
+++ b/app/models/ci/running_build.rb
@@ -10,6 +10,9 @@ module Ci
# of the running builds there is worth the additional pressure.
class RunningBuild < Ci::ApplicationRecord
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
partitionable scope: :build
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 719d19f4169..4853c57d41f 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -5,6 +5,9 @@ module Ci
class Pipeline < Ci::ApplicationRecord
include Ci::Partitionable
include Ci::NamespacedModelName
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
self.table_name = "ci_sources_pipelines"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d61760bd0fc..4f9a2e44562 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -7,6 +7,9 @@ module Ci
include Ci::HasStatus
include Gitlab::OptimisticLocking
include Presentable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
partitionable scope: :pipeline
@@ -148,7 +151,7 @@ module Ci
end
def manual_playable?
- blocked? || skipped?
+ blocked?
end
# This will be removed with ci_remove_ensure_stage_service
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index cfef1249164..37893f6cdae 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -3,6 +3,9 @@
module Ci
class UnitTestFailure < Ci::ApplicationRecord
include Ci::Partitionable
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
REPORT_WINDOW = 14.days
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 23fe89c38df..6f5972ebefa 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -14,6 +14,7 @@ module Ci
alias_attribute :secret_value, :value
+ validates :description, length: { maximum: 255 }, allow_blank: true
validates :key, uniqueness: {
scope: [:project_id, :environment_scope],
message: "(%{value}) has already been taken"
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index 372fdfda1ea..8dc866929f3 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -66,7 +66,6 @@ module Clusters
def ci_access_authorized_for?(user)
return false unless user
- return false unless ::Feature.enabled?(:expose_authorized_cluster_agents, project)
all_ci_access_authorized_projects_for(user).exists? ||
all_ci_access_authorized_namespaces_for(user).exists?
@@ -74,7 +73,6 @@ module Clusters
def user_access_authorized_for?(user)
return false unless user
- return false unless ::Feature.enabled?(:expose_authorized_cluster_agents, project)
Clusters::Agents::Authorizations::UserAccess::Finder
.new(user, agent: self, preload: false, limit: 1).execute.any?
diff --git a/app/models/clusters/concerns/prometheus_client.rb b/app/models/clusters/concerns/prometheus_client.rb
index 10cb307addd..d2f69b813aa 100644
--- a/app/models/clusters/concerns/prometheus_client.rb
+++ b/app/models/clusters/concerns/prometheus_client.rb
@@ -29,7 +29,7 @@ module Clusters
rescue Kubeclient::HttpError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENETUNREACH
# If users have mistakenly set parameters or removed the depended clusters,
# `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
- # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab,
+ # Since `PrometheusAdapter#can_query?` is eargely loaded on environment pages in gitlab,
# we need to silence the exceptions
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 26412205899..ded4b06a028 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -149,6 +149,10 @@ class Commit
from_hash(hash, project)
end
+
+ def underscore
+ 'commit'
+ end
end
attr_accessor :raw
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 90cdd267cbd..c6e507e4b6c 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -64,7 +64,7 @@ class CommitRange
range_string = range_string.strip
- unless range_string =~ /\A#{PATTERN}\z/o
+ unless /\A#{PATTERN}\z/o.match?(range_string)
raise ArgumentError, "invalid CommitRange string format: #{range_string}"
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f26831c1049..3f631f583b6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -8,13 +8,11 @@ class CommitStatus < Ci::ApplicationRecord
include Presentable
include BulkInsertableAssociations
include TaggableQueries
- include SafelyChangeColumnDefault
self.table_name = 'ci_builds'
self.sequence_name = 'ci_builds_id_seq'
self.primary_key = :id
partitionable scope: :pipeline
- columns_changing_default :partition_id
belongs_to :user
belongs_to :project
@@ -290,7 +288,7 @@ class CommitStatus < Ci::ApplicationRecord
def sortable_name
name.to_s.split(/(\d+)/).map do |v|
- v =~ /\d+/ ? v.to_i : v
+ /\d+/.match?(v) ? v.to_i : v
end
end
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5dac3c7833a..5bdf6bb31bf 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -16,7 +16,8 @@ module CommitSignature
unverified_key: 4,
unknown_key: 5,
multiple_signatures: 6,
- revoked_key: 7
+ revoked_key: 7,
+ verified_system: 8
}
belongs_to :project, class_name: 'Project', foreign_key: 'project_id', optional: false
diff --git a/app/models/concerns/database_event_tracking.rb b/app/models/concerns/database_event_tracking.rb
index 26e184c202f..7e2f445189e 100644
--- a/app/models/concerns/database_event_tracking.rb
+++ b/app/models/concerns/database_event_tracking.rb
@@ -3,8 +3,6 @@
module DatabaseEventTracking
extend ActiveSupport::Concern
- FEATURE_FLAG_BATCH2_CLASSES = %w[Vulnerability MergeRequest::Metrics].freeze
-
included do
after_create_commit :publish_database_create_event
after_destroy_commit :publish_database_destroy_event
@@ -24,9 +22,6 @@ module DatabaseEventTracking
end
def publish_database_event(name)
- return unless database_events_for_class_enabled?
- return unless database_events_feature_flag_enabled?
-
# Gitlab::Tracking#event is triggering Snowplow event
# Snowplow events are sent with usage of
# https://snowplow.github.io/snowplow-ruby-tracker/SnowplowTracker/AsyncEmitter.html
@@ -54,14 +49,4 @@ module DatabaseEventTracking
.with_indifferent_access
.slice(*self.class::SNOWPLOW_ATTRIBUTES)
end
-
- def database_events_for_class_enabled?
- is_batch2 = FEATURE_FLAG_BATCH2_CLASSES.include?(self.class.to_s)
-
- !is_batch2 || Feature.enabled?(:product_intelligence_database_event_tracking_batch2)
- end
-
- def database_events_feature_flag_enabled?
- Feature.enabled?(:product_intelligence_database_event_tracking)
- end
end
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index d798a13741f..f5ffeb8c425 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -85,7 +85,8 @@ module Enums
external_project_source: 5,
bridge_source: 6,
parameter_source: 7,
- compliance_source: 8
+ compliance_source: 8,
+ security_policies_default_source: 9
}
end
end
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
index 4b325de61bc..dbf05dbc428 100644
--- a/app/models/concerns/enums/vulnerability.rb
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -50,6 +50,10 @@ module Enums
CONFIDENCE_LEVELS
end
+ def self.parse_confidence_level(input)
+ input&.downcase.then { |value| confidence_levels.key?(value) ? value : 'unknown' }
+ end
+
def self.report_types
REPORT_TYPES
end
@@ -58,6 +62,10 @@ module Enums
SEVERITY_LEVELS
end
+ def self.parse_severity_level(input)
+ input&.downcase.then { |value| severity_levels.key?(value) ? value : 'unknown' }
+ end
+
def self.detection_methods
DETECTION_METHODS
end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
index cc55315d6d7..af139e735af 100644
--- a/app/models/concerns/expirable.rb
+++ b/app/models/concerns/expirable.rb
@@ -6,10 +6,8 @@ module Expirable
DAYS_TO_EXPIRE = 7
included do
- scope :not, ->(scope) { where(scope.arel.constraints.reduce(:and).not) }
-
- scope :expired, -> { where.not(expires_at: nil).where(arel_table[:expires_at].lteq(Time.current)) }
- scope :not_expired, -> { self.not(expired) }
+ scope :expired, -> { where(arel_table[:expires_at].lteq(Time.current)) }
+ scope :not_expired, -> { where(arel_table[:expires_at].gt(Time.current)).or(where(expires_at: nil)) }
end
def expired?
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 9d4b8328e8d..2d0ff82e624 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -14,7 +14,7 @@ module HasUserType
migration_bot: 7,
security_bot: 8,
automation_bot: 9,
- security_policy_bot: 10, # Currently not in use. See https://gitlab.com/gitlab-org/gitlab/-/issues/384174
+ security_policy_bot: 10,
admin_bot: 11,
suggested_reviewers_bot: 12,
service_account: 13,
diff --git a/app/models/concerns/ignorable_columns.rb b/app/models/concerns/ignorable_columns.rb
index 4cbcb25406d..249d0b99494 100644
--- a/app/models/concerns/ignorable_columns.rb
+++ b/app/models/concerns/ignorable_columns.rb
@@ -18,7 +18,7 @@ module IgnorableColumns
#
# Indicate the earliest date and release we can stop ignoring the column with +remove_after+ (a date string) and +remove_with+ (a release)
def ignore_columns(*columns, remove_after:, remove_with:)
- raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_after =~ Gitlab::Regex.utc_date_regex
+ raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless Gitlab::Regex.utc_date_regex.match?(remove_after)
raise ArgumentError, 'Please indicate in which release we can stop ignoring columns with remove_with, example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_with
self.ignored_columns += columns.flatten # rubocop:disable Cop/IgnoredColumns
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 209456f8b67..3f65e701da7 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -19,7 +19,7 @@ module IssueAvailableFeatures
end
included do
- scope :with_feature, ->(feature) { where(issue_type: available_features_for_issue_types[feature]) }
+ scope :with_feature, ->(feature) { with_issue_type(available_features_for_issue_types[feature]) }
end
def issue_type_supports?(feature)
diff --git a/app/models/concerns/issues/forbid_issue_type_column_usage.rb b/app/models/concerns/issues/forbid_issue_type_column_usage.rb
deleted file mode 100644
index 46a8a0278d9..00000000000
--- a/app/models/concerns/issues/forbid_issue_type_column_usage.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699
-module Issues
- module ForbidIssueTypeColumnUsage
- extend ActiveSupport::Concern
-
- ForbiddenColumnUsed = Class.new(StandardError)
-
- included do
- WorkItems::Type.base_types.each do |base_type, _value|
- define_method "#{base_type}?".to_sym do
- error_message = <<~ERROR
- `#{model_name.element}.#{base_type}?` uses the `issue_type` column underneath. As we want to remove the column,
- its usage is forbidden. You should use the `work_item_types` table instead.
-
- # Before
-
- #{model_name.element}.#{base_type}? => true
-
- # After
-
- #{model_name.element}.work_item_type.#{base_type}? => true
-
- More details in https://gitlab.com/groups/gitlab-org/-/epics/10529
- ERROR
-
- raise ForbiddenColumnUsed, error_message
- end
-
- define_singleton_method base_type.to_sym do
- error = ForbiddenColumnUsed.new(
- <<~ERROR
- `#{name}.#{base_type}` uses the `issue_type` column underneath. As we want to remove the column,
- its usage is forbidden. You should use the `work_item_types` table instead.
-
- # Before
-
- #{name}.#{base_type}
-
- # After
-
- #{name}.with_issue_type(:#{base_type})
-
- More details in https://gitlab.com/groups/gitlab-org/-/epics/10529
- ERROR
- )
-
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
- error,
- method_name: "#{name}.#{base_type}"
- )
-
- with_issue_type(base_type.to_sym)
- end
- end
- end
- end
-end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 4f2ea58f36d..3d9e09acf44 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -51,6 +51,7 @@ module Milestoneish
def issue_participants_visible_by_user(user)
User.joins(:issue_assignees)
.where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id))
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417457")
.distinct
end
@@ -90,9 +91,9 @@ module Milestoneish
def expires_at
if due_date
if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
+ "expired on #{due_date.to_fs(:medium)}"
else
- "expires on #{due_date.to_s(:medium)}"
+ "expires on #{due_date.to_fs(:medium)}"
end
end
end
diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb
index cc7279d05f8..90d3abddbf1 100644
--- a/app/models/concerns/packages/debian/component_file.rb
+++ b/app/models/concerns/packages/debian/component_file.rb
@@ -10,8 +10,6 @@ module Packages
include FileStoreMounter
include IgnorableColumns
- ignore_column :file_md5, remove_with: '16.2', remove_after: '2023-06-22'
-
def self.container_foreign_key
"#{container_type}_id".to_sym
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 76c733b1c0b..c70100c03c8 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -4,7 +4,7 @@
#
# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
# fields to a new table "project_features", support for the old fields is still needed in the API.
-require 'gitlab/utils'
+require 'gitlab/utils/all'
module ProjectFeaturesCompatibility
extend ActiveSupport::Concern
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 7e1ebd1eba3..a87eadb9332 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -32,7 +32,12 @@ module ProtectedRef
# to fail.
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
- validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }, unless: -> { allow_multiple?(type) }
+ validates :"#{type}_access_levels",
+ length: {
+ is: 1,
+ message: "are restricted to a single instance per #{self.model_name.human}."
+ },
+ unless: -> { allow_multiple?(type) }
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
end
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index c1c670db543..f0bb1cc359b 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -29,14 +29,30 @@ module ProtectedRefAccess
def humanize(access_level)
human_access_levels[access_level]
end
+
+ def non_role_types
+ []
+ end
end
included do
scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
- scope :for_role, -> { where(user_id: nil, group_id: nil) }
-
- validates :access_level, presence: true, if: :role?, inclusion: { in: allowed_access_levels }
+ scope :for_role, -> {
+ if non_role_types.present?
+ where.missing(*non_role_types)
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417457")
+ else
+ all
+ end
+ }
+
+ protected_ref_fk = "#{module_parent.model_name.singular}_id"
+ validates :access_level,
+ presence: true,
+ inclusion: { in: allowed_access_levels },
+ uniqueness: { scope: protected_ref_fk, conditions: -> { for_role } },
+ if: :role?
end
def humanize
diff --git a/app/models/concerns/protected_ref_deploy_key_access.rb b/app/models/concerns/protected_ref_deploy_key_access.rb
new file mode 100644
index 00000000000..4275476a1ff
--- /dev/null
+++ b/app/models/concerns/protected_ref_deploy_key_access.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module ProtectedRefDeployKeyAccess
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :deploy_key
+
+ protected_ref_fk = "#{module_parent.model_name.singular}_id"
+ validates :deploy_key_id, uniqueness: { scope: protected_ref_fk, allow_nil: true }
+ validate :validate_deploy_key_membership
+ end
+
+ class_methods do
+ def non_role_types
+ super << :deploy_key
+ end
+ end
+
+ def type
+ return :deploy_key if deploy_key.present?
+
+ super
+ end
+
+ def humanize
+ return deploy_key.title if deploy_key?
+
+ super
+ end
+
+ def check_access(current_user)
+ super do
+ break enabled_deploy_key_for_user?(current_user) if deploy_key?
+
+ yield if block_given?
+ end
+ end
+
+ private
+
+ def deploy_key?
+ type == :deploy_key
+ end
+
+ def validate_deploy_key_membership
+ return if deploy_key.nil? || deploy_key_has_write_access_to_project?
+
+ errors.add(:deploy_key, 'is not enabled for this project')
+ end
+
+ def enabled_deploy_key_for_user?(current_user)
+ current_user.can?(:read_project, project) &&
+ deploy_key.user_id == current_user.id &&
+ deploy_key_has_write_access_to_project?
+ end
+
+ def deploy_key_has_write_access_to_project?
+ DeployKey.with_write_access_for_project(project, deploy_key: deploy_key).exists?
+ end
+end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 6550c5a94a0..5986f8f5b5f 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -138,7 +138,7 @@ module Spammable
result.reject(&:blank?).join("\n")
end
- # Override in Spammable if further checks are necessary
+ # Override in included class if further checks are necessary
def check_for_spam?(*)
spammable_attribute_changed?
end
@@ -153,8 +153,8 @@ module Spammable
end
end
- # Override in Spammable if differs
- def allow_possible_spam?
+ # Override in included class if you want to allow possible spam under specific circumstances
+ def allow_possible_spam?(*)
Gitlab::CurrentSettings.allow_possible_spam
end
end
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index e3800caa43f..0e72bd30a37 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -17,7 +17,8 @@ module TriggerableHooks
feature_flag_hooks: :feature_flag_events,
release_hooks: :releases_events,
member_hooks: :member_events,
- subgroup_hooks: :subgroup_events
+ subgroup_hooks: :subgroup_events,
+ emoji_hooks: :emoji_events
}.freeze
extend ActiveSupport::Concern
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index a5b69997900..e8a50497b20 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -59,6 +59,7 @@ module VulnerabilityFindingHelpers
evidence = Vulnerabilities::Finding::Evidence.new(data: report_finding.evidence.data) if report_finding.evidence
Vulnerabilities::Finding.new(finding_data).tap do |finding|
+ finding.uuid = security_finding.uuid
finding.location_fingerprint = report_finding.location.fingerprint
finding.vulnerability = vulnerability_for(security_finding.uuid)
finding.project = project
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
index 71a12b4077b..a225625815b 100644
--- a/app/models/concerns/vulnerability_finding_signature_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -2,12 +2,17 @@
module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
+
# If the location object describes a physical location within a file
# (filename + line numbers), the 'location' algorithm_type should be used
# If the location object describes arbitrary data, then the 'hash'
# algorithm_type should be used.
-
- ALGORITHM_TYPES = { hash: 1, location: 2, scope_offset: 3 }.with_indifferent_access.freeze
+ ALGORITHM_TYPES = {
+ hash: 1,
+ location: 2,
+ scope_offset: 3,
+ scope_offset_compressed: 4
+ }.with_indifferent_access.freeze
class_methods do
def priority(algorithm_type)
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 0f0abeae795..6a52f6a0112 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -403,7 +403,7 @@ class ContainerRepository < ApplicationRecord
end
def migrated?
- Gitlab.com?
+ Gitlab.com_except_jh?
end
def last_import_step_done_at
@@ -526,7 +526,7 @@ class ContainerRepository < ApplicationRecord
def size
strong_memoize(:size) do
- next unless Gitlab.com?
+ next unless Gitlab.com_except_jh?
next if self.created_at.before?(MIGRATION_PHASE_1_STARTED_AT) && self.migration_state != 'import_done'
next unless gitlab_api_client.supports_gitlab_api?
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1e3a80087c8..b59b22c10c4 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -351,7 +351,7 @@ class Deployment < ApplicationRecord
end
def formatted_deployment_time
- deployed_at&.to_time&.in_time_zone&.to_s(:medium)
+ deployed_at&.to_time&.in_time_zone&.to_fs(:medium)
end
def deployed_by
@@ -447,7 +447,7 @@ class Deployment < ApplicationRecord
# when refs_by_oid is passed an SHA, returns refs for that commit
def tags(limit: 100)
strong_memoize_with(:tag, limit) do
- project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || []
+ project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX])
end
end
diff --git a/app/models/design_management/repository.rb b/app/models/design_management/repository.rb
index 39077fdbcb1..7410944e174 100644
--- a/app/models/design_management/repository.rb
+++ b/app/models/design_management/repository.rb
@@ -8,7 +8,7 @@ module DesignManagement
belongs_to :project, inverse_of: :design_management_repository
validates :project, presence: true, uniqueness: true
- delegate :lfs_enabled?, :storage, :repository_storage, to: :project
+ delegate :lfs_enabled?, :storage, :repository_storage, :run_after_commit, to: :project
def repository
::DesignManagement::GitRepository.new(
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 8480272eced..241b454f5ce 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -18,7 +18,7 @@ class Environment < ApplicationRecord
belongs_to :cluster_agent, class_name: 'Clusters::Agent', optional: true, inverse_of: :environments
use_fast_destroy :all_deployments
- nullify_if_blank :external_url
+ nullify_if_blank :external_url, :kubernetes_namespace
has_many :all_deployments, class_name: 'Deployment'
has_many :deployments, -> { visible }
@@ -70,13 +70,15 @@ class Environment < ApplicationRecord
length: { maximum: 255 },
allow_nil: true
- # Currently, the tier presence is validaed for newly created environments.
- # After the `BackfillEnvironmentTiers` background migration has been completed, we should remove `on: :create`.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/385253.
- # Todo: Remove along with FF `validate_environment_tier_presence`.
- validates :tier, presence: true, on: :create, unless: :validate_environment_tier_present?
+ validates :kubernetes_namespace,
+ allow_nil: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
- validates :tier, presence: true, if: :validate_environment_tier_present?
+ validates :tier, presence: true
validate :safe_external_url
validate :merge_request_not_changed
@@ -602,10 +604,6 @@ class Environment < ApplicationRecord
self.class.tiers[:other]
end
end
-
- def validate_environment_tier_present?
- Feature.enabled?(:validate_environment_tier_presence, self.project)
- end
end
Environment.prepend_mod_with('Environment')
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 36030b80370..06dc9cad5f9 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -44,7 +44,7 @@ class ExternalIssue
end
def reference_link_text(from = nil)
- return "##{id}" if id =~ /^\d+$/
+ return "##{id}" if /^\d+$/.match?(id)
id
end
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
deleted file mode 100644
index 94c242782c1..00000000000
--- a/app/models/external_pull_request.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-# frozen_string_literal: true
-
-# This model stores pull requests coming from external providers, such as
-# GitHub, when GitLab project is set as CI/CD only and remote mirror.
-#
-# When setting up a remote mirror with GitHub we subscribe to push and
-# pull_request webhook events. When a pull request is opened on GitHub,
-# a webhook is sent out, we create or update the status of the pull
-# request locally.
-#
-# When the mirror is updated and changes are pushed to branches we check
-# if there are open pull requests for the source and target branch.
-# If so, we create pipelines for external pull requests.
-class ExternalPullRequest < Ci::ApplicationRecord
- include Gitlab::Utils::StrongMemoize
- include ShaAttribute
- include EachBatch
-
- belongs_to :project
-
- sha_attribute :source_sha
- sha_attribute :target_sha
-
- validates :source_branch, presence: true
- validates :target_branch, presence: true
- validates :source_sha, presence: true
- validates :target_sha, presence: true
- validates :source_repository, presence: true
- validates :target_repository, presence: true
- validates :status, presence: true
-
- enum status: {
- open: 1,
- closed: 2
- }
-
- # We currently don't support pull requests from fork, so
- # we are going to return an error to the webhook
- validate :not_from_fork
-
- scope :by_source_branch, ->(branch) { where(source_branch: branch) }
- scope :by_source_repository, -> (repository) { where(source_repository: repository) }
-
- # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix
- self.table_name = 'external_pull_requests'
-
- def self.create_or_update_from_params(params)
- find_params = params.slice(:project_id, :source_branch, :target_branch)
-
- safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
- yield(pull_request) if block_given?
- end
- end
-
- def actual_branch_head?
- actual_source_branch_sha == source_sha
- end
-
- def from_fork?
- source_repository != target_repository
- end
-
- def source_ref
- Gitlab::Git::BRANCH_REF_PREFIX + source_branch
- end
-
- def predefined_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
- variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
- end
- end
-
- def modified_paths
- project.repository.diff_stats(target_sha, source_sha).paths
- end
-
- private
-
- def actual_source_branch_sha
- project.commit(source_ref)&.sha
- end
-
- def not_from_fork
- if from_fork?
- errors.add(:base, _('Pull requests from fork are not supported'))
- end
- end
-
- def self.safe_find_or_initialize_and_update(find:, update:)
- safe_ensure_unique(retries: 1) do
- model = find_or_initialize_by(find)
-
- if model.update(update)
- yield(model) if block_given?
- end
-
- model
- end
- end
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index 85971c48567..2b5a392e02c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -510,7 +510,9 @@ class Group < Namespace
members_with_parents(only_active_users: false)
end
- members_from_hiearchy.all_owners.left_outer_joins(:user).merge(User.without_project_bot)
+ members_from_hiearchy.all_owners.left_outer_joins(:user)
+ .merge(User.without_project_bot)
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
end
def ldap_synced?
@@ -663,13 +665,24 @@ class Group < Namespace
# 2. They belong to a project that belongs to the group
# 3. They belong to a sub-group or project in such sub-group
# 4. They belong to an ancestor group
- def direct_and_indirect_users
+ # 5. They belong to a group that is shared with this group, if share_with_groups is true
+ def direct_and_indirect_users(share_with_groups: false)
+ members = if share_with_groups
+ # We only need :user_id column, but
+ # `members_from_self_and_ancestor_group_shares` needs more
+ # columns to make the CTE query work.
+ GroupMember.from_union([
+ direct_and_indirect_members.select(:user_id, :source_type, :type),
+ members_from_self_and_ancestor_group_shares.reselect(:user_id, :source_type, :type)
+ ])
+ else
+ direct_and_indirect_members
+ end
+
User.from_union([
- User
- .where(id: direct_and_indirect_members.select(:user_id))
- .reorder(nil),
- project_users_with_descendants
- ])
+ User.where(id: members.select(:user_id)).reorder(nil),
+ project_users_with_descendants
+ ])
end
# Returns all users (also inactive) that are members of the group because:
@@ -683,7 +696,7 @@ class Group < Namespace
.where(id: direct_and_indirect_members_with_inactive.select(:user_id))
.reorder(nil),
project_users_with_descendants
- ])
+ ]).allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") # failed in spec/tasks/gitlab/user_management_rake_spec.rb
end
def users_count
@@ -696,6 +709,7 @@ class Group < Namespace
User
.joins(projects: :group)
.where(namespaces: { id: self_and_descendants.select(:id) })
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
end
# Return the highest access level for a user
@@ -802,8 +816,11 @@ class Group < Namespace
end
def execute_integrations(data, hooks_scope)
- # NOOP
- # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ return unless Feature.enabled?(:group_mentions, self)
+
+ integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
+ integration.async_execute(data)
+ end
end
def preload_shared_group_links
@@ -813,16 +830,6 @@ class Group < Namespace
).call
end
- def update_shared_runners_setting!(state)
- raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state)
-
- case state
- when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override
- when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE then disable_shared_runners_and_allow_override!
- when SR_ENABLED then enable_shared_runners! # set both to true
- end
- end
-
def first_owner
owners.first || parent&.first_owner || owner
end
@@ -969,12 +976,14 @@ class Group < Namespace
end
def max_member_access(user_ids)
- Gitlab::SafeRequestLoader.execute(
- resource_key: max_member_access_for_resource_key(User),
- resource_ids: user_ids,
- default_value: Gitlab::Access::NO_ACCESS
- ) do |user_ids|
- members_with_parents.where(user_id: user_ids).group(:user_id).maximum(:access_level)
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") do
+ Gitlab::SafeRequestLoader.execute(
+ resource_key: max_member_access_for_resource_key(User),
+ resource_ids: user_ids,
+ default_value: Gitlab::Access::NO_ACCESS
+ ) do |user_ids|
+ members_with_parents.where(user_id: user_ids).group(:user_id).maximum(:access_level)
+ end
end
end
@@ -1057,45 +1066,6 @@ class Group < Namespace
Arel::Nodes::SqlLiteral.new(column_alias))
end
- def disable_shared_runners!
- update!(
- shared_runners_enabled: false,
- allow_descendants_override_disabled_shared_runners: false)
-
- group_ids = descendants
- unless group_ids.empty?
- Group.by_id(group_ids).update_all(
- shared_runners_enabled: false,
- allow_descendants_override_disabled_shared_runners: false)
- end
-
- all_projects.update_all(shared_runners_enabled: false)
- end
-
- def disable_shared_runners_and_allow_override!
- # enabled -> disabled_and_overridable
- if shared_runners_enabled?
- update!(
- shared_runners_enabled: false,
- allow_descendants_override_disabled_shared_runners: true)
-
- group_ids = descendants
- unless group_ids.empty?
- Group.by_id(group_ids).update_all(shared_runners_enabled: false)
- end
-
- all_projects.update_all(shared_runners_enabled: false)
-
- # disabled_and_unoverridable -> disabled_and_overridable
- else
- update!(allow_descendants_override_disabled_shared_runners: true)
- end
- end
-
- def enable_shared_runners!
- update!(shared_runners_enabled: true)
- end
-
def runners_token_prefix
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 695041f0247..05c5ad22218 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -21,7 +21,8 @@ class ProjectHook < WebHook
:wiki_page_hooks,
:deployment_hooks,
:feature_flag_hooks,
- :release_hooks
+ :release_hooks,
+ :emoji_hooks
]
belongs_to :project
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index e08294058e4..4c35f699468 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -66,7 +66,7 @@ class WebHookLog < ApplicationRecord
def redact_user_emails
self.request_data.deep_transform_values! do |value|
- value.to_s =~ URI::MailTo::EMAIL_REGEXP ? _('[REDACTED]') : value
+ URI::MailTo::EMAIL_REGEXP.match?(value.to_s) ? _('[REDACTED]') : value
end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index f2f242136ab..f823a385022 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -90,6 +90,8 @@ class Integration < ApplicationRecord
attribute :push_events, default: true
attribute :tag_push_events, default: true
attribute :wiki_page_events, default: true
+ attribute :group_mention_events, default: false
+ attribute :group_confidential_mention_events, default: false
after_initialize :initialize_properties
@@ -137,6 +139,8 @@ class Integration < ApplicationRecord
scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :incident_hooks, -> { where(incident_events: true, active: true) }
scope :deployment, -> { where(category: 'deployment') }
+ scope :group_mention_hooks, -> { where(group_mention_events: true, active: true) }
+ scope :group_confidential_mention_hooks, -> { where(group_confidential_mention_events: true, active: true) }
class << self
private
@@ -586,6 +590,7 @@ class Integration < ApplicationRecord
end
def async_execute(data)
+ return if ::Gitlab::SilentMode.enabled?
return unless supported_events.include?(data[:object_kind])
Integrations::ExecuteWorker.perform_async(id, data)
@@ -600,6 +605,10 @@ class Integration < ApplicationRecord
category == :chat
end
+ def ci?
+ category == :ci
+ end
+
private
# Ancestors sorted by hierarchy depth in bottom-top order.
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 4477f3d207f..c9de4d2b3bb 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -262,11 +262,11 @@ module Integrations
end
def project_name
- project.full_name
+ project.try(:full_name)
end
def project_url
- project.web_url
+ project.try(:web_url)
end
def update?(data)
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index c83a559e0da..29a20419809 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -7,6 +7,8 @@ module Integrations
].freeze
prop_accessor EVENT_CHANNEL['alert']
+ prop_accessor EVENT_CHANNEL['group_mention']
+ prop_accessor EVENT_CHANNEL['group_confidential_mention']
override :default_channel_placeholder
def default_channel_placeholder
@@ -16,15 +18,20 @@ module Integrations
override :get_message
def get_message(object_kind, data)
return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+ return Integrations::ChatMessage::GroupMentionMessage.new(data) if object_kind == 'group_mention'
super
end
override :supported_events
def supported_events
- additional = ['alert']
+ additional = %w[alert]
- super + additional
+ if group_level? && Feature.enabled?(:group_mentions, group)
+ additional += %w[group_mention group_confidential_mention]
+ end
+
+ (super + additional).freeze
end
override :configurable_channels?
diff --git a/app/models/integrations/chat_message/group_mention_message.rb b/app/models/integrations/chat_message/group_mention_message.rb
new file mode 100644
index 00000000000..a2bc00ddbd9
--- /dev/null
+++ b/app/models/integrations/chat_message/group_mention_message.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class GroupMentionMessage < BaseMessage
+ ISSUE_KIND = 'issue'
+ MR_KIND = 'merge_request'
+ NOTE_KIND = 'note'
+
+ KNOWN_KINDS = [ISSUE_KIND, MR_KIND, NOTE_KIND].freeze
+
+ def initialize(params)
+ super
+ params = HashWithIndifferentAccess.new(params)
+
+ @group_name, @group_url = params[:mentioned].values_at(:name, :url)
+ @detail = nil
+
+ obj_attr = params[:object_attributes]
+ obj_kind = obj_attr[:object_kind]
+ raise NotImplementedError unless KNOWN_KINDS.include?(obj_kind)
+
+ case obj_kind
+ when 'issue'
+ @source_name, @title = get_source_for_issue(obj_attr)
+ @detail = obj_attr[:description]
+ when 'merge_request'
+ @source_name, @title = get_source_for_merge_request(obj_attr)
+ @detail = obj_attr[:description]
+ when 'note'
+ if params[:commit]
+ @source_name, @title = get_source_for_commit(params[:commit])
+ elsif params[:issue]
+ @source_name, @title = get_source_for_issue(params[:issue])
+ elsif params[:merge_request]
+ @source_name, @title = get_source_for_merge_request(params[:merge_request])
+ else
+ raise NotImplementedError
+ end
+
+ @detail = obj_attr[:note]
+ end
+
+ @source_url = obj_attr[:url]
+ end
+
+ def attachments
+ if markdown
+ detail
+ else
+ [{ text: format(detail), color: attachment_color }]
+ end
+ end
+
+ def activity
+ {
+ title: "Group #{group_link} was mentioned in #{source_link}",
+ subtitle: "of #{project_link}",
+ text: strip_markup(formatted_title),
+ image: user_avatar
+ }
+ end
+
+ private
+
+ attr_reader :group_name, :group_url, :source_name, :source_url, :title, :detail
+
+ def get_source_for_commit(params)
+ commit_sha = Commit.truncate_sha(params[:id])
+ ["commit #{commit_sha}", params[:title]]
+ end
+
+ def get_source_for_issue(params)
+ ["issue ##{params[:iid]}", params[:title]]
+ end
+
+ def get_source_for_merge_request(params)
+ ["merge request !#{params[:iid]}", params[:title]]
+ end
+
+ def message
+ "Group #{group_link} was mentioned in #{source_link} of #{project_link}: *#{formatted_title}*"
+ end
+
+ def formatted_title
+ strip_markup(title.lines.first.chomp)
+ end
+
+ def group_link
+ link(group_name, group_url)
+ end
+
+ def source_link
+ link(source_name, source_url)
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index ad82f1b916f..7ba9bbc38e6 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -2,6 +2,23 @@
module Integrations
class HangoutsChat < BaseChatNotification
+ undef :notify_only_broken_pipelines
+
+ field :webhook,
+ section: SECTION_TYPE_CONNECTION,
+ help: 'https://chat.googleapis.com/v1/spaces…',
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION
+
+ field :branches_to_be_notified,
+ type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
def title
'Google Chat'
end
@@ -19,25 +36,15 @@ module Integrations
s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
- def default_channel_placeholder
+ def fields
+ self.class.fields + build_event_channels
end
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
+ def default_channel_placeholder
end
- def default_fields
- [
- { type: 'text', name: 'webhook', help: 'https://chat.googleapis.com/v1/spaces…' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- {
- type: 'select',
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }
- ]
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
private
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index d6cbe5760e8..a9ed0bd3da1 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -2,6 +2,24 @@
module Integrations
class MicrosoftTeams < BaseChatNotification
+ undef :notify_only_broken_pipelines
+
+ field :webhook,
+ section: SECTION_TYPE_CONNECTION,
+ help: 'https://outlook.office.com/webhook/…',
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION,
+ help: 'If selected, successful pipelines do not trigger a notification event.'
+
+ field :branches_to_be_notified,
+ type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
def title
'Microsoft Teams notifications'
end
@@ -26,23 +44,8 @@ module Integrations
pipeline wiki_page]
end
- def default_fields
- [
- { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', help: 'https://outlook.office.com/webhook/…', required: true },
- {
- type: 'checkbox',
- section: SECTION_TYPE_CONFIGURATION,
- name: 'notify_only_broken_pipelines',
- help: 'If selected, successful pipelines do not trigger a notification event.'
- },
- {
- type: 'select',
- section: SECTION_TYPE_CONFIGURATION,
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }
- ]
+ def fields
+ self.class.fields + build_event_channels
end
def sections
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 2dc0fd7d011..8969c6c13b2 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -15,7 +15,7 @@ module Integrations
title: 'API URL',
placeholder: -> { s_('PrometheusService|https://prometheus.example.com/') },
help: -> { s_('PrometheusService|The Prometheus API base URL.') },
- required: true
+ required: false
field :google_iap_audience_client_id,
title: 'Google IAP Audience Client ID',
@@ -34,8 +34,8 @@ module Integrations
# to allow localhost URLs when the following conditions are true:
# 1. api_url is the internal Prometheus URL.
with_options presence: true do
- validates :api_url, public_url: true, if: ->(object) { object.manual_configuration? && !object.allow_local_api_url? }
- validates :api_url, url: true, if: ->(object) { object.manual_configuration? && object.allow_local_api_url? }
+ validates :api_url, public_url: true, if: ->(object) { object.api_url.present? && object.manual_configuration? && !object.allow_local_api_url? }
+ validates :api_url, url: true, if: ->(object) { object.api_url.present? && object.manual_configuration? && object.allow_local_api_url? }
end
before_save :synchronize_service_state
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index aa19133b8c2..6c447c8f4e4 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -2,6 +2,23 @@
module Integrations
class UnifyCircuit < BaseChatNotification
+ undef :notify_only_broken_pipelines
+
+ field :webhook,
+ section: SECTION_TYPE_CONNECTION,
+ help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…',
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION
+
+ field :branches_to_be_notified,
+ type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
def title
'Unify Circuit'
end
@@ -14,6 +31,10 @@ module Integrations
'unify_circuit'
end
+ def fields
+ self.class.fields + build_event_channels
+ end
+
def help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
@@ -27,19 +48,6 @@ module Integrations
pipeline wiki_page]
end
- def default_fields
- [
- { type: 'text', name: 'webhook', help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…', required: true },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- {
- type: 'select',
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }
- ]
- end
-
private
def notify(message, opts)
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 8e6f5ca6d17..ef1bc81ea58 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -2,6 +2,23 @@
module Integrations
class WebexTeams < BaseChatNotification
+ undef :notify_only_broken_pipelines
+
+ field :webhook,
+ section: SECTION_TYPE_CONNECTION,
+ help: 'https://api.ciscospark.com/v1/webhooks/incoming/...',
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION
+
+ field :branches_to_be_notified,
+ type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
def title
s_("WebexTeamsService|Webex Teams")
end
@@ -14,6 +31,10 @@ module Integrations
'webex_teams'
end
+ def fields
+ self.class.fields + build_event_channels
+ end
+
def help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
@@ -23,21 +44,7 @@ module Integrations
end
def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', help: 'https://api.ciscospark.com/v1/webhooks/incoming/...', required: true },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- {
- type: 'select',
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }
- ]
+ %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
private
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 890af8a27a0..6e48dcab9ed 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -56,6 +56,8 @@ class Issue < ApplicationRecord
# This default came from the enum `issue_type` column. Defined as default in the DB
DEFAULT_ISSUE_TYPE = :issue
+ ignore_column :issue_type, remove_with: '16.4', remove_after: '2023-08-22'
+
belongs_to :project
belongs_to :namespace, inverse_of: :issues
@@ -133,12 +135,6 @@ class Issue < ApplicationRecord
validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed?
validate :due_date_after_start_date
validate :parent_link_confidentiality
- # using a custom validation since we are overwriting the `issue_type` method to use the work_item_types table
- validate :issue_type_attribute_present
-
- enum issue_type: WorkItems::Type.base_types
- # TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699
- include ::Issues::ForbidIssueTypeColumnUsage
alias_method :issuing_parent, :project
alias_attribute :issuing_parent_id, :project_id
@@ -187,7 +183,10 @@ class Issue < ApplicationRecord
scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
- scope :with_web_entity_associations, -> { preload(:author, :namespace, project: [:project_feature, :route, namespace: :route]) }
+ scope :with_web_entity_associations, -> do
+ preload(:author, :namespace, :labels, project: [:project_feature, :route, namespace: :route])
+ end
+
scope :preload_awardable, -> { preload(:award_emoji) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
@@ -201,24 +200,17 @@ class Issue < ApplicationRecord
scope :with_issue_type, ->(types) {
types = Array(types)
- if Feature.enabled?(:issue_type_uses_work_item_types_table)
- # Using != 1 since we also want the guard clause to handle empty arrays
- return joins(:work_item_type).where(work_item_types: { base_type: types }) if types.size != 1
+ # Using != 1 since we also want the guard clause to handle empty arrays
+ return joins(:work_item_type).where(work_item_types: { base_type: types }) if types.size != 1
- where(
- '"issues"."work_item_type_id" = (?)',
- WorkItems::Type.by_type(types.first).select(:id).limit(1)
- )
- else
- where(issue_type: types)
- end
+ # This optimization helps the planer use the correct indexes when filtering by a single type
+ where(
+ '"issues"."work_item_type_id" = (?)',
+ WorkItems::Type.by_type(types.first).select(:id).limit(1)
+ )
}
scope :without_issue_type, ->(types) {
- if Feature.enabled?(:issue_type_uses_work_item_types_table)
- joins(:work_item_type).where.not(work_item_types: { base_type: types })
- else
- where.not(issue_type: types)
- end
+ joins(:work_item_type).where.not(work_item_types: { base_type: types })
}
scope :public_only, -> { where(confidential: false) }
@@ -258,7 +250,6 @@ class Issue < ApplicationRecord
scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') }
before_validation :ensure_namespace_id, :ensure_work_item_type
- before_save :check_issue_type_in_sync!
after_save :ensure_metrics!, unless: :importing?
after_commit :expire_etag_cache, unless: :importing?
@@ -588,16 +579,12 @@ class Issue < ApplicationRecord
user, project.external_authorization_classification_label)
end
- def check_for_spam?(user:)
- # content created via support bots is always checked for spam, EVEN if
- # the issue is not publicly visible and/or confidential
- return true if user.support_bot? && spammable_attribute_changed?
-
- # Only check for spam on issues which are publicly visible (and thus indexed in search engines)
- return false unless publicly_visible?
+ # Always enforce spam check for support bot but allow for other users when issue is not publicly visible
+ def allow_possible_spam?(user)
+ return true if Gitlab::CurrentSettings.allow_possible_spam
+ return false if user.support_bot?
- # Only check for spam if certain attributes have changed
- spammable_attribute_changed?
+ !publicly_visible?
end
def supports_recaptcha?
@@ -753,11 +740,7 @@ class Issue < ApplicationRecord
end
def issue_type
- if ::Feature.enabled?(:issue_type_uses_work_item_types_table)
- work_item_type_with_default.base_type
- else
- super
- end
+ work_item_type_with_default.base_type
end
def unsubscribe_email_participant(email)
@@ -766,41 +749,11 @@ class Issue < ApplicationRecord
issue_email_participants.find_by_email(email)&.destroy
end
- private
-
- def check_issue_type_in_sync!
- # We might have existing records out of sync, so we need to skip this check unless the value is changed
- # so those records can still be updated until we fix them and remove the issue_type column
- # https://gitlab.com/gitlab-org/gitlab/-/work_items/403158
- return unless (changes.keys & %w[issue_type work_item_type_id]).any?
-
- # Do not replace the use of attributes with `issue_type` here
- if attributes['issue_type'] != work_item_type.base_type
- error = IssueTypeOutOfSyncError.new(
- <<~ERROR
- Issue `issue_type` out of sync with `work_item_type_id` column.
- `issue_type` must be equal to `work_item.base_type`.
- You can assign the correct work_item_type like this for example:
-
- Issue.new(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident))
-
- More details in https://gitlab.com/gitlab-org/gitlab/-/issues/338005
- ERROR
- )
-
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
- error,
- issue_type: attributes['issue_type'],
- work_item_type_id: work_item_type_id
- )
- end
+ def hook_attrs
+ Gitlab::HookData::IssueBuilder.new(self).build
end
- def issue_type_attribute_present
- return if attributes['issue_type'].present?
-
- errors.add(:issue_type, 'Must be present')
- end
+ private
def due_date_after_start_date
return unless start_date.present? && due_date.present?
@@ -834,12 +787,6 @@ class Issue < ApplicationRecord
Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
end
- def spammable_attribute_changed?
- # NOTE: We need to check them for spam when issues are made non-confidential, because spam
- # may have been added while they were confidential and thus not being checked for spam.
- super || confidential_changed?(from: true, to: false)
- end
-
def ensure_metrics!
Issue::Metrics.record!(self)
end
@@ -868,9 +815,7 @@ class Issue < ApplicationRecord
def ensure_work_item_type
return if work_item_type_id.present? || work_item_type_id_change&.last.present?
- # TODO: We should switch to DEFAULT_ISSUE_TYPE here when the issue_type column is dropped
- # https://gitlab.com/gitlab-org/gitlab/-/work_items/402700
- self.work_item_type = WorkItems::Type.default_by_type(attributes['issue_type'])
+ self.work_item_type = WorkItems::Type.default_by_type(DEFAULT_ISSUE_TYPE)
end
def allowed_work_item_type_change
diff --git a/app/models/member.rb b/app/models/member.rb
index 0700b1a8448..f164ea244b4 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -66,6 +66,7 @@ class Member < ApplicationRecord
scope :with_invited_user_state, -> do
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
.select('members.*', 'invited_user.state as invited_user_state')
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
end
scope :in_hierarchy, ->(source) do
@@ -174,7 +175,10 @@ class Member < ApplicationRecord
scope :by_access_level, -> (access_level) { active.where(access_level: access_level) }
scope :all_by_access_level, -> (access_level) { where(access_level: access_level) }
- scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
+ scope :preload_user_and_notification_settings, -> do
+ preload(user: :notification_settings)
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
+ end
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
scope :including_source, -> { includes(:source) }
@@ -288,7 +292,9 @@ class Member < ApplicationRecord
class << self
def search(query)
- scope = joins(:user).merge(User.search(query, use_minimum_char_limit: false))
+ scope = joins(:user)
+ .merge(User.search(query, use_minimum_char_limit: false))
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
return scope unless Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
@@ -347,6 +353,7 @@ class Member < ApplicationRecord
def left_join_users
left_outer_joins(:user)
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
end
def access_for_user_ids(user_ids)
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 237054587bc..ada89345a7f 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -20,7 +20,6 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups&.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> { group(:source_id).count }
- scope :with_user, -> (user) { where(user: user) }
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 116108ceaf9..2773569161d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -421,7 +421,9 @@ class MergeRequest < ApplicationRecord
scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: { merge_request_diff_commits: [:commit_author, :committer] }) }
scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) }
- scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) }
+ scope :with_web_entity_associations, -> do
+ preload(:author, :labels, target_project: [:project_feature, group: [:route, :parent], namespace: :route])
+ end
scope :with_auto_merge_enabled, -> do
with_state(:opened).where(auto_merge_enabled: true)
@@ -1199,10 +1201,17 @@ class MergeRequest < ApplicationRecord
end
alias_method :wip_title, :draft_title
- def mergeable?(skip_ci_check: false, skip_discussions_check: false)
+ def skipped_mergeable_checks(options = {})
+ {
+ skip_ci_check: options.fetch(:auto_merge_requested, false)
+ }
+ end
+
+ def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false)
return false unless mergeable_state?(
skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check
+ skip_discussions_check: skip_discussions_check,
+ skip_approved_check: skip_approved_check
)
check_mergeability
@@ -1223,11 +1232,12 @@ class MergeRequest < ApplicationRecord
]
end
- def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
+ def mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false)
additional_checks = execute_merge_checks(
params: {
skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check
+ skip_discussions_check: skip_discussions_check,
+ skip_approved_check: skip_approved_check
}
)
additional_checks.success?
@@ -1526,6 +1536,14 @@ class MergeRequest < ApplicationRecord
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/train"
end
+ def schedule_cleanup_refs(only: :all)
+ if Feature.enabled?(:merge_request_cleanup_ref_worker_async, target_project)
+ MergeRequests::CleanupRefWorker.perform_async(id, only.to_s)
+ else
+ cleanup_refs(only: only)
+ end
+ end
+
def cleanup_refs(only: :all)
target_refs = []
target_refs << ref_path if %i[all head].include?(only)
diff --git a/app/models/merge_request/diff_llm_summary.rb b/app/models/merge_request/diff_llm_summary.rb
deleted file mode 100644
index e13fe5e1f50..00000000000
--- a/app/models/merge_request/diff_llm_summary.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# rubocop:disable Style/ClassAndModuleChildren
-# frozen_string_literal: true
-
-class MergeRequest::DiffLlmSummary < ApplicationRecord
- belongs_to :merge_request_diff
- belongs_to :user, optional: true
-
- validates :merge_request_diff_id, uniqueness: true
- validates :provider, presence: true
- validates :content, presence: true, length: { maximum: 2056 }
-
- enum provider: { openai: 0 }
-end
-# rubocop:enable Style/ClassAndModuleChildren
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 70216144035..a13cb353c7b 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -13,7 +13,7 @@ class MergeRequest::Metrics < ApplicationRecord
before_save :ensure_target_project_id
scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) }
- scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) }
+ scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date.is_a?(Time) ? date.end_of_day : date)) }
scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) }
scope :by_target_project, ->(project) { where(target_project_id: project) }
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d300b938fc0..8de717fb61d 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -10,6 +10,7 @@ class Milestone < ApplicationRecord
include IidRoutes
include UpdatedAtFilterable
include EachBatch
+ include Spammable
prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
@@ -62,6 +63,9 @@ class Milestone < ApplicationRecord
validate :parent_type_check
validate :uniqueness_of_title, if: :title_changed?
+ attr_spammable :title, spam_title: true
+ attr_spammable :description, spam_description: true
+
state_machine :state, initial: :active do
event :close do
transition active: :closed
@@ -255,6 +259,10 @@ class Milestone < ApplicationRecord
end
end
+ def check_for_spam?(*)
+ spammable_attribute_changed? && parent.public?
+ end
+
private
def timebox_format_reference(format = :iid)
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index d1277efac7b..5c5f8d3b2db 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -11,6 +11,7 @@ module Ml
belongs_to :project
belongs_to :user
+ belongs_to :model, optional: true, inverse_of: :default_experiment
has_many :candidates, class_name: 'Ml::Candidate'
has_many :metadata, class_name: 'Ml::ExperimentMetadata'
@@ -22,10 +23,21 @@ module Ml
has_internal_id :iid, scope: :project
+ before_destroy :stop_destroy
+
def package_name
"#{PACKAGE_PREFIX}#{iid}"
end
+ def stop_destroy
+ return unless model_id
+
+ errors[:base] << "Cannot delete an experiment associated to a model"
+ # According to docs, throw is the correct way to stop on a callback
+ # https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks
+ throw :abort # rubocop:disable Cop/BanCatchThrow
+ end
+
class << self
def by_project_id_and_iid(project_id, iid)
find_by(project_id: project_id, iid: iid)
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
new file mode 100644
index 00000000000..684b8e1983b
--- /dev/null
+++ b/app/models/ml/model.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ml
+ class Model < ApplicationRecord
+ validates :project, :default_experiment, presence: true
+ validates :name,
+ format: Gitlab::Regex.ml_model_name_regex,
+ uniqueness: { scope: :project },
+ presence: true,
+ length: { maximum: 255 }
+
+ validate :valid_default_experiment?
+
+ has_one :default_experiment, class_name: 'Ml::Experiment'
+ belongs_to :project
+ has_many :versions, class_name: 'Ml::ModelVersion'
+
+ def valid_default_experiment?
+ return unless default_experiment
+
+ errors.add(:default_experiment) unless default_experiment.name == name
+ errors.add(:default_experiment) unless default_experiment.project_id == project_id
+ end
+ end
+end
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
new file mode 100644
index 00000000000..540fe6018a1
--- /dev/null
+++ b/app/models/ml/model_version.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelVersion < ApplicationRecord
+ validates :project, :model, presence: true
+
+ validates :version,
+ format: Gitlab::Regex.ml_model_version_regex,
+ uniqueness: { scope: [:project, :model_id] },
+ presence: true,
+ length: { maximum: 255 }
+
+ validate :valid_model?, :valid_package?
+
+ belongs_to :model, class_name: 'Ml::Model'
+ belongs_to :project
+ belongs_to :package, class_name: 'Packages::Package', optional: true
+
+ delegate :name, to: :model
+
+ private
+
+ def valid_model?
+ return unless model
+
+ errors.add(:model, 'model project must be the same') unless model.project_id == project_id
+ end
+
+ def valid_package?
+ return unless package
+
+ errors.add(:package, 'package must be ml_model') unless package.ml_model?
+ errors.add(:package, 'package name must be the same') unless package.name == name
+ errors.add(:package, 'package version must be the same') unless package.version == version
+ errors.add(:package, 'package project must be the same') unless package.project_id == project_id
+ end
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7b3bb04da5b..5449f006a2e 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -57,6 +57,7 @@ class Namespace < ApplicationRecord
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: 'User'
+ belongs_to :organization, class_name: 'Organizations::Organization'
belongs_to :parent, class_name: "Namespace"
has_many :children, -> { where(type: Group.sti_name) }, class_name: "Namespace", foreign_key: :parent_id
@@ -305,7 +306,7 @@ class Namespace < ApplicationRecord
end
def first_project_with_container_registry_tags
- if ContainerRegistry::GitlabApiClient.supports_gitlab_api?
+ if Gitlab.com_except_jh? && ContainerRegistry::GitlabApiClient.supports_gitlab_api?
ContainerRegistry::GitlabApiClient.one_project_with_container_registry_tag(full_path)
else
all_projects.includes(:container_repositories).find(&:has_container_registry_tags?)
@@ -423,6 +424,10 @@ class Namespace < ApplicationRecord
false
end
+ def all_project_ids
+ all_projects.pluck(:id)
+ end
+
def all_project_ids_except(ids)
all_projects.where.not(id: ids).pluck(:id)
end
@@ -478,7 +483,7 @@ class Namespace < ApplicationRecord
def container_repositories_size
strong_memoize(:container_repositories_size) do
- next unless Gitlab.com?
+ next unless Gitlab.com_except_jh?
next unless root?
next unless ContainerRegistry::GitlabApiClient.supports_gitlab_api?
next 0 if all_container_repositories.empty?
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 9006f104c64..1ca3c8e85f3 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -96,27 +96,6 @@ module Namespaces
traversal_ids.present?
end
- def use_traversal_ids_for_self_and_hierarchy?
- return false unless use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy, root_ancestor)
-
- traversal_ids.present?
- end
-
- def use_traversal_ids_for_ancestors?
- return false unless use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor)
-
- traversal_ids.present?
- end
-
- def use_traversal_ids_for_ancestors_upto?
- return false unless use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids_for_ancestors_upto, root_ancestor)
-
- traversal_ids.present?
- end
-
def root_ancestor
strong_memoize(:root_ancestor) do
if association(:parent).loaded? && parent.present?
@@ -150,13 +129,13 @@ module Namespaces
end
def self_and_hierarchy
- return super unless use_traversal_ids_for_self_and_hierarchy?
+ return super unless use_traversal_ids?
self_and_descendants.or(ancestors)
end
def ancestors(hierarchy_order: nil)
- return super unless use_traversal_ids_for_ancestors?
+ return super unless use_traversal_ids?
return self.class.none if parent_id.blank?
@@ -164,7 +143,7 @@ module Namespaces
end
def ancestor_ids(hierarchy_order: nil)
- return super unless use_traversal_ids_for_ancestors?
+ return super unless use_traversal_ids?
hierarchy_order == :desc ? traversal_ids[0..-2] : traversal_ids[0..-2].reverse
end
@@ -176,7 +155,7 @@ module Namespaces
# This copies the behavior of the recursive method. We will deprecate
# this behavior soon.
def ancestors_upto(top = nil, hierarchy_order: nil)
- return super unless use_traversal_ids_for_ancestors_upto?
+ return super unless use_traversal_ids?
# We can't use a default value in the method definition above because
# we need to preserve those specific parameters for super.
@@ -198,7 +177,7 @@ module Namespaces
end
def self_and_ancestors(hierarchy_order: nil)
- return super unless use_traversal_ids_for_ancestors?
+ return super unless use_traversal_ids?
return self.class.where(id: id) if parent_id.blank?
@@ -206,7 +185,7 @@ module Namespaces
end
def self_and_ancestor_ids(hierarchy_order: nil)
- return super unless use_traversal_ids_for_ancestors?
+ return super unless use_traversal_ids?
hierarchy_order == :desc ? traversal_ids : traversal_ids.reverse
end
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index c50d3dd1de6..6e79e3ac9a1 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -18,7 +18,7 @@ module Namespaces
end
def roots
- return super unless use_traversal_ids_roots?
+ return super unless use_traversal_ids?
root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct
unscoped.where(id: root_ids)
@@ -37,13 +37,13 @@ module Namespaces
end
def self_and_descendants(include_self: true)
- return super unless use_traversal_ids_for_descendants_scopes?
+ return super unless use_traversal_ids?
self_and_descendants_with_comparison_operators(include_self: include_self)
end
def self_and_descendant_ids(include_self: true)
- return super unless use_traversal_ids_for_descendants_scopes?
+ return super unless use_traversal_ids?
self_and_descendants(include_self: include_self).as_ids
end
@@ -78,16 +78,6 @@ module Namespaces
Feature.enabled?(:use_traversal_ids)
end
- def use_traversal_ids_roots?
- Feature.enabled?(:use_traversal_ids_roots) &&
- use_traversal_ids?
- end
-
- def use_traversal_ids_for_descendants_scopes?
- Feature.enabled?(:use_traversal_ids_for_descendants_scopes) &&
- use_traversal_ids?
- end
-
def use_traversal_ids_for_self_and_hierarchy_scopes?
Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes) &&
use_traversal_ids?
diff --git a/app/models/note.rb b/app/models/note.rb
index 09ff7ad3979..2df643c46aa 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -26,7 +26,7 @@ class Note < ApplicationRecord
include IgnorableColumns
include Spammable
- ignore_column :id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22'
+ ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22'
ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/.freeze
@@ -756,7 +756,7 @@ class Note < ApplicationRecord
Ability.users_that_can_read_internal_notes(users, resource_parent).pluck(:id)
end
- # Method necesary while we transition into the new format for task system notes
+ # Method necessary while we transition into the new format for task system notes
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/369923
def note
return super unless system? && for_issue? && super&.match?(ISSUE_TASK_SYSTEM_NOTE_PATTERN)
@@ -792,6 +792,14 @@ class Note < ApplicationRecord
true
end
+ # Use attributes.keys instead of attribute_names to filter out the fields that are skipped during export:
+ #
+ # - note_html
+ # - cached_markdown_version
+ def attribute_names_for_serialization
+ attributes.keys
+ end
+
private
def trigger_note_subscription?
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index ce89f57a73b..8aeca2eb137 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -8,6 +8,14 @@ module Organizations
before_destroy :check_if_default_organization
+ has_many :namespaces
+ has_many :groups
+
+ has_one :settings, class_name: "OrganizationSetting"
+
+ has_many :organization_users, inverse_of: :organization
+ has_many :users, through: :organization_users, inverse_of: :organizations
+
validates :name,
presence: true,
length: { maximum: 255 }
diff --git a/app/models/organizations/organization_setting.rb b/app/models/organizations/organization_setting.rb
new file mode 100644
index 00000000000..108531e6701
--- /dev/null
+++ b/app/models/organizations/organization_setting.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Organizations
+ class OrganizationSetting < ApplicationRecord
+ belongs_to :organization
+
+ validates :settings, json_schema: { filename: "organization_settings" }
+
+ jsonb_accessor :settings,
+ restricted_visibility_levels: [:integer, { array: true }]
+
+ validates_each :restricted_visibility_levels do |record, attr, value|
+ value&.each do |level|
+ unless Gitlab::VisibilityLevel.options.value?(level)
+ record.errors.add(attr, format(_("'%{level}' is not a valid visibility level"), level: level))
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/organizations/organization_user.rb b/app/models/organizations/organization_user.rb
new file mode 100644
index 00000000000..5aa1133b017
--- /dev/null
+++ b/app/models/organizations/organization_user.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Organizations
+ class OrganizationUser < ApplicationRecord
+ belongs_to :organization, inverse_of: :organization_users, optional: false
+ belongs_to :user, inverse_of: :organization_users, optional: false
+ end
+end
diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb
index ccbf056ec7b..2fc1c05cd48 100644
--- a/app/models/packages/npm/metadatum.rb
+++ b/app/models/packages/npm/metadatum.rb
@@ -26,6 +26,11 @@ class Packages::Npm::Metadatum < ApplicationRecord
def ensure_package_json_size
return if package_json.to_s.size < MAX_PACKAGE_JSON_SIZE
- errors.add(:package_json, _('structure is too large'))
+ errors.add(:package_json, :too_large,
+ message: format(
+ _('structure is too large. Maximum size is %{max_size} characters'),
+ max_size: MAX_PACKAGE_JSON_SIZE
+ )
+ )
end
end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 58305b45457..b618c7c20c4 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -83,7 +83,7 @@ class Packages::Package < ApplicationRecord
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
- validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm?
+ validates :name, format: { with: Gitlab::Regex.npm_package_name_regex, message: Gitlab::Regex.npm_package_name_regex_message }, if: :npm?
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module?
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package?
@@ -94,7 +94,8 @@ class Packages::Package < ApplicationRecord
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang?
validates :version, format: { with: Gitlab::Regex.helm_version_regex }, if: :helm?
- validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? || terraform_module? }
+ validates :version, format: { with: Gitlab::Regex.semver_regex, message: Gitlab::Regex.semver_regex_message },
+ if: -> { composer_tag_version? || npm? || terraform_module? }
validates :version,
presence: true,
@@ -166,16 +167,16 @@ class Packages::Package < ApplicationRecord
scope :preload_files, -> { preload(:installable_package_files) }
scope :preload_nuget_files, -> { preload(:installable_nuget_package_files) }
scope :preload_pipelines, -> { preload(pipelines: :user) }
- scope :last_of_each_version, -> { where(id: all.last_of_each_version_ids) }
- scope :last_of_each_version_ids, -> { select('MAX(id) AS id').unscope(where: :id).group(:version) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
scope :select_distinct_name, -> { select(:name).distinct }
+ scope :select_only_first_by_name, -> { select('DISTINCT ON (name) *') }
# Sorting
scope :order_created, -> { reorder(created_at: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_name, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :order_name_desc_version_desc, -> { reorder(name: :desc, version: :desc) }
scope :order_version, -> { reorder(version: :asc) }
scope :order_version_desc, -> { reorder(version: :desc) }
scope :order_type, -> { reorder(package_type: :asc) }
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 864ea04c019..2ffb2e84cbf 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -46,7 +46,7 @@ module Pages
strong_memoize_attr :source
def prefix
- if project.pages_namespace_url == project.pages_url
+ if url_builder.namespace_pages?
'/'
else
"#{project.full_path.delete_prefix(trim_prefix)}/"
@@ -55,9 +55,7 @@ module Pages
strong_memoize_attr :prefix
def unique_host
- return unless project.project_setting.pages_unique_domain_enabled?
-
- project.pages_unique_host
+ url_builder.unique_host
end
strong_memoize_attr :unique_host
@@ -76,5 +74,10 @@ module Pages
project.pages_metadatum.pages_deployment
end
strong_memoize_attr :deployment
+
+ def url_builder
+ Gitlab::Pages::UrlBuilder.new(project)
+ end
+ strong_memoize_attr :url_builder
end
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 2749404b7b5..08f725de980 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -20,6 +20,7 @@ class PersonalAccessToken < ApplicationRecord
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user
+ belongs_to :previous_personal_access_token, class_name: 'PersonalAccessToken'
after_initialize :set_default_scopes, if: :persisted?
before_save :ensure_token
@@ -99,9 +100,13 @@ class PersonalAccessToken < ApplicationRecord
def expires_at_before_instance_max_expiry_date
return unless expires_at
- if expires_at > MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now
- errors.add(:expires_at, _('must expire in 365 days'))
- end
+ max_expiry_date = Date.current.advance(days: MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS)
+ return unless expires_at > max_expiry_date
+
+ errors.add(
+ :expires_at,
+ format(_("must be before %{expiry_date}"), expiry_date: max_expiry_date)
+ )
end
end
diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb
index 6795e7a3049..245c0719439 100644
--- a/app/models/plan_limits.rb
+++ b/app/models/plan_limits.rb
@@ -2,6 +2,9 @@
class PlanLimits < ApplicationRecord
include IgnorableColumns
+ ALLOWED_LIMITS_HISTORY_ATTRIBUTES = %i[notification_limit enforcement_limit storage_size_limit
+ dashboard_limit_enabled_at].freeze
+
ignore_column :ci_max_artifact_size_running_container_scanning, remove_with: '14.3', remove_after: '2021-08-22'
ignore_column :web_hook_calls_high, remove_with: '15.10', remove_after: '2022-02-22'
ignore_column :ci_active_pipelines, remove_with: '16.3', remove_after: '2022-07-22'
@@ -50,32 +53,23 @@ class PlanLimits < ApplicationRecord
false
end
- def log_limits_changes(user, new_limits)
- new_limits.each do |attribute, value|
+ def format_limits_history(user, new_limits)
+ allowed_limits = new_limits.slice(*ALLOWED_LIMITS_HISTORY_ATTRIBUTES)
+ return {} if allowed_limits.empty?
+
+ allowed_limits.each do |attribute, value|
+ next if value == self[attribute]
+
limits_history[attribute] ||= []
limits_history[attribute] << {
- user_id: user&.id,
- username: user&.username,
- timestamp: Time.current.utc.to_i,
- value: value
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => Time.current.utc.to_i,
+ "value" => value
}
end
- update(limits_history: limits_history)
- end
-
- def limit_attribute_changes(attribute)
- limit_history = limits_history[attribute]
- return [] unless limit_history
-
- limit_history.map do |entry|
- {
- timestamp: entry[:timestamp],
- value: entry[:value],
- username: entry[:username],
- user_id: entry[:user_id]
- }
- end
+ limits_history
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 452a5c8973c..931f4db3a54 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -415,7 +415,7 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
- has_many :external_pull_requests, inverse_of: :project
+ has_many :external_pull_requests, inverse_of: :project, class_name: 'Ci::ExternalPullRequest'
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id
@@ -692,6 +692,10 @@ class Project < ApplicationRecord
scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) }
scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) }
scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) }
+ # .with_slack_integration can generate poorly performing queries. It is intended only for UsagePing.
+ scope :with_slack_integration, -> { joins(:slack_integration) }
+ # .with_slack_slash_commands_integration can generate poorly performing queries. It is intended only for UsagePing.
+ scope :with_slack_slash_commands_integration, -> { joins(:slack_slash_commands_integration) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
# includes(:route) which we use in ProjectsFinder.
@@ -775,6 +779,7 @@ class Project < ApplicationRecord
scope :pending_data_repair_analysis, -> do
left_outer_joins(:container_registry_data_repair_detail)
.where(container_registry_data_repair_details: { project_id: nil })
+ .order(id: :desc)
end
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -904,6 +909,16 @@ class Project < ApplicationRecord
scope :for_group_and_its_ancestor_groups, ->(group) { where(namespace_id: group.self_and_ancestors.select(:id)) }
scope :is_importing, -> { with_import_state.where(import_state: { status: %w[started scheduled] }) }
+ scope :without_created_and_owned_by_banned_user, -> do
+ where_not_exists(
+ Users::BannedUser.joins(
+ 'INNER JOIN project_authorizations ON project_authorizations.user_id = banned_users.user_id'
+ ).where('projects.creator_id = banned_users.user_id')
+ .where('project_authorizations.project_id = projects.id')
+ .where(project_authorizations: { access_level: Gitlab::Access::OWNER })
+ )
+ end
+
class << self
# Searches for a list of projects based on the query given in `query`.
#
@@ -1840,10 +1855,12 @@ class Project < ApplicationRecord
triggered.add_hooks(hooks)
end
- def execute_integrations(data, hooks_scope = :push_hooks)
+ def execute_integrations(data, hooks_scope = :push_hooks, skip_ci: false)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
association("#{hooks_scope}_integrations").reader.each do |integration|
+ next if skip_ci && integration.ci?
+
integration.async_execute(data)
end
end
@@ -2201,42 +2218,6 @@ class Project < ApplicationRecord
pages_metadatum&.deployed?
end
- def pages_url(with_unique_domain: false)
- return pages_unique_url if with_unique_domain && pages_unique_domain_enabled?
-
- url = pages_namespace_url
- url_path = full_path.partition('/').last
- namespace_url = "#{Settings.pages.protocol}://#{url_path}".downcase
-
- if Rails.env.development?
- url_without_port = URI.parse(url)
- url_without_port.port = nil
-
- return url if url_without_port.to_s == namespace_url
- end
-
- # If the project path is the same as host, we serve it as group page
- return url if url == namespace_url
-
- "#{url}/#{url_path}"
- end
-
- def pages_unique_url
- pages_url_for(project_setting.pages_unique_domain)
- end
-
- def pages_unique_host
- URI(pages_unique_url).host
- end
-
- def pages_namespace_url
- pages_url_for(pages_subdomain)
- end
-
- def pages_subdomain
- full_path.partition('/').first
- end
-
def pages_path
# TODO: when we migrate Pages to work with new storage types, change here to use disk_path
File.join(Settings.pages.path, full_path)
@@ -2483,7 +2464,7 @@ class Project < ApplicationRecord
break unless pages_enabled?
variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host)
- variables.append(key: 'CI_PAGES_URL', value: pages_url)
+ variables.append(key: 'CI_PAGES_URL', value: Gitlab::Pages::UrlBuilder.new(self).pages_url)
end
end
@@ -3167,6 +3148,10 @@ class Project < ApplicationRecord
pending_delete? || hidden?
end
+ def created_and_owned_by_banned_user?
+ creator.banned? && team.max_member_access(creator.id) == Gitlab::Access::OWNER
+ end
+
def content_editor_on_issues_feature_flag_enabled?
group&.content_editor_on_issues_feature_flag_enabled? || Feature.enabled?(:content_editor_on_issues, self)
end
@@ -3236,25 +3221,8 @@ class Project < ApplicationRecord
group.crm_enabled?
end
- def frozen_outbound_job_token_scopes?
- Feature.enabled?(:frozen_outbound_job_token_scopes, self) && Feature.disabled?(:frozen_outbound_job_token_scopes_override, self)
- end
- strong_memoize_attr :frozen_outbound_job_token_scopes?
-
private
- def pages_unique_domain_enabled?
- Feature.enabled?(:pages_unique_domain, self) &&
- project_setting.pages_unique_domain_enabled?
- end
-
- def pages_url_for(domain)
- # The host in URL always needs to be downcased
- Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
- "#{prefix}#{domain}."
- end.downcase
- end
-
# overridden in EE
def project_group_links_with_preload
project_group_links
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index aa65f27870d..cc9003423be 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -2,7 +2,6 @@
class ProjectCiCdSetting < ApplicationRecord
include ChronicDurationAttribute
- include IgnorableColumns
belongs_to :project, inverse_of: :ci_cd_settings
@@ -23,8 +22,6 @@ class ProjectCiCdSetting < ApplicationRecord
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
- ignore_column :opt_in_jwt, remove_with: '16.2', remove_after: '2023-07-01'
-
def keep_latest_artifacts_available?
# The project level feature can only be enabled when the feature is enabled instance wide
Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact?
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 14f6a90e5ed..365bb5237c3 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -34,7 +34,6 @@ class ProjectStatistics < ApplicationRecord
:build_artifacts_size,
:packages_size,
:snippets_size,
- :pipeline_artifacts_size,
:uploads_size
].freeze
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index ed1795b43e0..347d65841ed 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -71,7 +71,7 @@ module Projects
# /\R/ - A linebreak: \n, \v, \f, \r \u0085 (NEXT LINE),
# \u2028 (LINE SEPARATOR), \u2029 (PARAGRAPH SEPARATOR) or \r\n.
- return unless name =~ /\R/
+ return unless /\R/.match?(name)
errors.add(:name, 'has characters that are not allowed')
end
diff --git a/app/models/projects/triggered_hooks.rb b/app/models/projects/triggered_hooks.rb
index e3aa3d106b7..1f51ced5b57 100644
--- a/app/models/projects/triggered_hooks.rb
+++ b/app/models/projects/triggered_hooks.rb
@@ -17,6 +17,8 @@ module Projects
# Assumes that the relations implement TriggerableHooks
@relations.each do |hooks|
hooks.hooks_for(@scope).select_active(@scope, @data).each do |hook|
+ next if @scope == :emoji_hooks && Feature.disabled?(:emoji_webhooks, hook.parent)
+
hook.async_execute(@data, @scope.to_s)
end
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index c86ca5723fa..53cec0c5511 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -3,49 +3,7 @@
class ProtectedBranch::PushAccessLevel < ApplicationRecord
include Importable
include ProtectedBranchAccess
+ include ProtectedRefDeployKeyAccess
# default value for the access_level column
GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER
-
- belongs_to :deploy_key
-
- validates :access_level, uniqueness: { scope: :protected_branch_id, if: :role?,
- conditions: -> { where(user_id: nil, group_id: nil, deploy_key_id: nil) } }
- validates :deploy_key_id, uniqueness: { scope: :protected_branch_id, allow_nil: true }
- validate :validate_deploy_key_membership
-
- def type
- if self.deploy_key.present?
- :deploy_key
- else
- super
- end
- end
-
- def humanize
- return "Deploy key" if deploy_key.present?
-
- super
- end
-
- def check_access(user)
- if user && deploy_key.present?
- return user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
- end
-
- super
- end
-
- private
-
- def validate_deploy_key_membership
- return unless deploy_key
-
- unless project.deploy_keys_projects.where(deploy_key: deploy_key).exists?
- self.errors.add(:deploy_key, 'is not enabled for this project')
- end
- end
-
- def enabled_deploy_key_for_user?(deploy_key, user)
- deploy_key.user_id == user.id && DeployKey.with_write_access_for_project(protected_branch.project, deploy_key: deploy_key).any?
- end
end
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
index 5837f3a5afb..0eff9924153 100644
--- a/app/models/protected_tag/create_access_level.rb
+++ b/app/models/protected_tag/create_access_level.rb
@@ -3,48 +3,5 @@
class ProtectedTag::CreateAccessLevel < ApplicationRecord
include Importable
include ProtectedTagAccess
-
- belongs_to :deploy_key
-
- validates :access_level, uniqueness: { scope: :protected_tag_id, if: :role?,
- conditions: -> { where(user_id: nil, group_id: nil, deploy_key_id: nil) } }
- validates :deploy_key_id, uniqueness: { scope: :protected_tag_id, allow_nil: true }
- validate :validate_deploy_key_membership
-
- def type
- return :deploy_key if deploy_key.present?
-
- super
- end
-
- def humanize
- return "Deploy key" if deploy_key.present?
-
- super
- end
-
- def check_access(current_user)
- super do
- break enabled_deploy_key_for_user?(current_user) if deploy_key?
- end
- end
-
- private
-
- def deploy_key?
- type == :deploy_key
- end
-
- def validate_deploy_key_membership
- return unless deploy_key
- return if project.deploy_keys_projects.where(deploy_key: deploy_key).exists?
-
- errors.add(:deploy_key, 'is not enabled for this project')
- end
-
- def enabled_deploy_key_for_user?(current_user)
- current_user.can?(:read_project, project) &&
- deploy_key.user_id == current_user.id &&
- DeployKey.with_write_access_for_project(protected_tag.project, deploy_key: deploy_key).any?
- end
+ include ProtectedRefDeployKeyAccess
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 7f74872cf67..f0ba56390ab 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -64,10 +64,10 @@ class Release < ApplicationRecord
end
# This query uses LATERAL JOIN to find the latest release for each project. To avoid
- # joining the `releases` table, we build an in-memory table using the project ids.
+ # joining the `projects` table, we build an in-memory table using the project ids.
# Example:
# SELECT ...
- # FROM (VALUES (PROJECT_ID_1),(PROJECT_ID_2)) project_ids (id)
+ # FROM (VALUES (PROJECT_ID_1),(PROJECT_ID_2)) projects (id)
# INNER JOIN LATERAL (...)
def latest_for_projects(projects, order_by: 'released_at')
return Release.none if projects.empty?
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 8b2f3bdcedf..934053cb92d 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -137,6 +137,7 @@ class RemoteMirror < ApplicationRecord
return false unless project.remote_mirror_available?
return false unless project.repository_exists?
return false if project.pending_delete?
+ return false if Gitlab::SilentMode.enabled?
true
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index b21df6baf0e..1321c9da780 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -838,7 +838,7 @@ class Repository
files = ls_files(options[:branch_name])
options[:actions] = files.each_with_object([]) do |item, list|
- next unless item =~ regex
+ next unless regex.match?(item)
list.push(
action: :move,
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index 4216ad7e70f..6560b25b39c 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -21,6 +21,7 @@ class ServiceDeskSetting < ApplicationRecord
validates :project_id, presence: true
validate :valid_issue_template
validate :valid_project_key
+ validate :custom_email_enabled_state
validates :outgoing_name, length: { maximum: 255 }, allow_blank: true
validates :project_key,
length: { maximum: 255 },
@@ -86,6 +87,14 @@ class ServiceDeskSetting < ApplicationRecord
end
end
+ def custom_email_enabled_state
+ return unless custom_email_enabled?
+
+ if custom_email_verification.blank? || !custom_email_verification.finished?
+ errors.add(:custom_email_enabled, 'cannot be enabled until verification process has finished.')
+ end
+ end
+
private
def source_template_project
diff --git a/app/models/system_access.rb b/app/models/system_access.rb
new file mode 100644
index 00000000000..9ffc63c5ca8
--- /dev/null
+++ b/app/models/system_access.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module SystemAccess
+ def self.table_name_prefix
+ 'system_access_'
+ end
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 724f97c4812..f202e1a266d 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -117,7 +117,18 @@ class Todo < ApplicationRecord
# target - The value of the `target_type` column, such as `Issue`.
# state - The value of the `state` column, such as `pending` or `done`.
def any_for_target?(target, state = nil)
- state.nil? ? exists?(target: target) : exists?(target: target, state: state)
+ conditions = {}
+
+ if target.respond_to?(:todoable_target_type_name)
+ conditions[:target_type] = target.todoable_target_type_name
+ conditions[:target_id] = target.id
+ else
+ conditions[:target] = target
+ end
+
+ conditions[:state] = state unless state.nil?
+
+ exists?(conditions)
end
# Updates attributes of a relation of todos to the new state.
diff --git a/app/models/user.rb b/app/models/user.rb
index 96cdbb192bc..4a57cc2e2e2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -60,7 +60,7 @@ class User < ApplicationRecord
INCOMING_MAIL_TOKEN_PREFIX = 'glimt-'
FEED_TOKEN_PREFIX = 'glft-'
- columns_changing_default :notified_of_own_activity
+ columns_changing_default :project_view
# lib/tasks/tokens.rake needs to be updated when changing mail and feed tokens
add_authentication_token_field :incoming_email_token, token_generator: -> { self.generate_incoming_mail_token }
@@ -170,8 +170,11 @@ class User < ApplicationRecord
has_many :following_users, foreign_key: :followee_id, class_name: 'Users::UserFollowUser'
has_many :followers, through: :following_users
- # Groups
+ # Namespaces
has_many :members
+ has_many :member_namespaces, through: :members
+
+ # Groups
has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, class_name: 'GroupMember'
has_many :groups, through: :group_members
has_many :groups_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :group_members, source: :group
@@ -256,6 +259,9 @@ class User < ApplicationRecord
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
+ has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user
+ has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users
+
has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :user
has_one :status, class_name: 'UserStatus'
@@ -1541,7 +1547,7 @@ class User < ApplicationRecord
end
def full_website_url
- return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
+ return "http://#{website_url}" unless %r{\Ahttps?://}.match?(website_url)
website_url
end
@@ -1827,8 +1833,12 @@ class User < ApplicationRecord
Project.where(id: events).not_aimed_for_deletion
end
+ # Returns true if the user can be removed, false otherwise.
+ # A user can be removed if they do not own any groups where they are the sole owner
+ # Method `none?` is used to ensure faster retrieval, See https://gitlab.com/gitlab-org/gitlab/-/issues/417105
+
def can_be_removed?
- !solo_owned_groups.present?
+ solo_owned_groups.none?
end
def can_remove_self?
@@ -2063,9 +2073,17 @@ class User < ApplicationRecord
# override, from Devise
def lock_access!(opts = {})
Gitlab::AppLogger.info("Account Locked: username=#{username}")
+ audit_lock_access(reason: opts.delete(:reason))
super
end
+ # override, from Devise
+ def unlock_access!(unlocked_by: self)
+ audit_unlock_access(author: unlocked_by)
+
+ super()
+ end
+
# Determine the maximum access level for a group of projects in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
@@ -2103,7 +2121,7 @@ class User < ApplicationRecord
end
def terms_accepted?
- return true if project_bot?
+ return true if project_bot? || service_account? || security_policy_bot?
accepted_term_id.present?
end
@@ -2279,30 +2297,6 @@ class User < ApplicationRecord
namespace_commit_emails.find_by(namespace: project.root_namespace)
end
- def spammer?
- spam_score > Abuse::TrustScore::SPAMCHECK_HAM_THRESHOLD
- end
-
- def spam_score
- abuse_trust_scores.spamcheck.average(:score) || 0.0
- end
-
- def telesign_score
- abuse_trust_scores.telesign.order(created_at: :desc).first&.score || 0.0
- end
-
- def arkose_global_score
- abuse_trust_scores.arkose_global_score.order(created_at: :desc).first&.score || 0.0
- end
-
- def arkose_custom_score
- abuse_trust_scores.arkose_custom_score.order(created_at: :desc).first&.score || 0.0
- end
-
- def trust_scores_for_source(source)
- abuse_trust_scores.where(source: source)
- end
-
def abuse_metadata
{
account_age: account_age_in_days,
@@ -2310,6 +2304,10 @@ class User < ApplicationRecord
}
end
+ def allow_possible_spam?
+ custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists?
+ end
+
def namespace_commit_email_for_namespace(namespace)
return if namespace.nil?
@@ -2330,7 +2328,7 @@ class User < ApplicationRecord
return super if ::Gitlab::CurrentSettings.email_confirmation_setting_soft?
# Following devise logic for method, we want to return `true`
- # See: https://github.com/heartcombo/devise/blob/main/lib/devise/models/confirmable.rb#L191-L218
+ # See: https://github.com/heartcombo/devise/blob/ec0674523e7909579a5a008f16fb9fe0c3a71712/lib/devise/models/confirmable.rb#L191-L218
true
end
alias_method :in_confirmation_period?, :confirmation_period_valid?
@@ -2355,7 +2353,8 @@ class User < ApplicationRecord
private
def block_or_ban
- if spammer? && account_age_in_days < 7
+ user_scores = Abuse::UserTrustScore.new(self)
+ if user_scores.spammer? && account_age_in_days < 7
ban_and_report
else
block
@@ -2608,6 +2607,12 @@ class User < ApplicationRecord
def prefix_for_feed_token
FEED_TOKEN_PREFIX
end
+
+ # method overriden in EE
+ def audit_lock_access(reason: nil); end
+
+ # method overriden in EE
+ def audit_unlock_access(author: self); end
end
User.prepend_mod_with('User')
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 63a5ee9770f..425f2cc062b 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -15,6 +15,8 @@ class UserCustomAttribute < ApplicationRecord
UNBLOCKED_BY = 'unblocked_by'
ARKOSE_RISK_BAND = 'arkose_risk_band'
AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id'
+ ALLOW_POSSIBLE_SPAM = 'allow_possible_spam'
+ IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
class << self
def upsert_custom_attributes(custom_attributes)
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 4d517408154..c263d552d40 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -2,15 +2,12 @@
class UserPreference < ApplicationRecord
include IgnorableColumns
- include SafelyChangeColumnDefault
# We could use enums, but Rails 4 doesn't support multiple
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
- columns_changing_default :tab_width, :time_display_relative, :render_whitespace_in_code
-
belongs_to :user
scope :with_user, -> { joins(:user) }
@@ -31,7 +28,6 @@ class UserPreference < ApplicationRecord
validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' }
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
- ignore_columns :time_format_in_24h, remove_with: '16.2', remove_after: '2023-07-22'
# 2023-06-22 is after 16.1 release and during 16.2 release https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#ignoring-the-column-release-m
ignore_columns :use_legacy_web_ide, remove_with: '16.2', remove_after: '2023-06-22'
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 38e518b6d3e..0d02a3b99aa 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -55,10 +55,10 @@ module Users
submit_license_usage_data_banner: 52, # EE-only
personal_project_limitations_banner: 53, # EE-only
mr_experience_survey: 54,
- namespace_storage_limit_banner_info_threshold: 55, # EE-only
- namespace_storage_limit_banner_warning_threshold: 56, # EE-only
- namespace_storage_limit_banner_alert_threshold: 57, # EE-only
- namespace_storage_limit_banner_error_threshold: 58, # EE-only
+ # 55 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
+ namespace_storage_limit_alert_warning_threshold: 56, # EE-only
+ namespace_storage_limit_alert_alert_threshold: 57, # EE-only
+ namespace_storage_limit_alert_error_threshold: 58, # EE-only
project_quality_summary_feedback: 59, # EE-only
merge_request_settings_moved_callout: 60,
new_top_level_group_alert: 61,
@@ -66,13 +66,14 @@ module Users
# 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233
branch_rules_info_callout: 65,
create_runner_workflow_banner: 66,
- repository_storage_limit_banner_info_threshold: 67, # EE-only
- repository_storage_limit_banner_warning_threshold: 68, # EE-only
- repository_storage_limit_banner_alert_threshold: 69, # EE-only
- repository_storage_limit_banner_error_threshold: 70, # EE-only
+ # 67 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
+ project_repository_limit_alert_warning_threshold: 68, # EE-only
+ project_repository_limit_alert_alert_threshold: 69, # EE-only
+ project_repository_limit_alert_error_threshold: 70, # EE-only
new_navigation_callout: 71,
code_suggestions_third_party_callout: 72, # EE-only
- namespace_over_storage_users_combined_alert: 73 # EE-only
+ namespace_over_storage_users_combined_alert: 73, # EE-only
+ rich_text_editor: 74
}
validates :feature_name,
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index c5946197b6f..74b653b5777 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -17,19 +17,19 @@ module Users
preview_user_over_limit_free_plan_alert: 7, # EE-only
user_reached_limit_free_plan_alert: 8, # EE-only
free_group_limited_alert: 9, # EE-only
- namespace_storage_limit_banner_info_threshold: 10, # EE-only
- namespace_storage_limit_banner_warning_threshold: 11, # EE-only
- namespace_storage_limit_banner_alert_threshold: 12, # EE-only
- namespace_storage_limit_banner_error_threshold: 13, # EE-only
+ # 10 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
+ namespace_storage_limit_alert_warning_threshold: 11, # EE-only
+ namespace_storage_limit_alert_alert_threshold: 12, # EE-only
+ namespace_storage_limit_alert_error_threshold: 13, # EE-only
usage_quota_trial_alert: 14, # EE-only
preview_usage_quota_free_plan_alert: 15, # EE-only
enforcement_at_limit_alert: 16, # EE-only
web_hook_disabled: 17, # EE-only
unlimited_members_during_trial_alert: 18, # EE-only
- repository_storage_limit_banner_info_threshold: 19, # EE-only
- repository_storage_limit_banner_warning_threshold: 20, # EE-only
- repository_storage_limit_banner_alert_threshold: 21, # EE-only
- repository_storage_limit_banner_error_threshold: 22, # EE-only
+ # 19 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
+ project_repository_limit_alert_warning_threshold: 20, # EE-only
+ project_repository_limit_alert_alert_threshold: 21, # EE-only
+ project_repository_limit_alert_error_threshold: 22, # EE-only
namespace_over_storage_users_combined_alert: 23 # EE-only
}
diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb
index c8b2513e702..5480b9e9c4a 100644
--- a/app/models/webauthn_registration.rb
+++ b/app/models/webauthn_registration.rb
@@ -3,10 +3,6 @@
# Registration information for WebAuthn credentials
class WebauthnRegistration < ApplicationRecord
- include IgnorableColumns
-
- ignore_column :u2f_registration_id, remove_with: '16.2', remove_after: '2023-06-22'
-
belongs_to :user
validates :credential_xid, :public_key, :counter, presence: true
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 9f28ffbf7b6..adf424a1d94 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -65,6 +65,12 @@ class WorkItem < Issue
'issue'
end
+ # Todo: remove method after target_type cleanup
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/416009
+ def todoable_target_type_name
+ %w[Issue WorkItem]
+ end
+
def widgets
strong_memoize(:widgets) do
work_item_type.widgets.map do |widget_class|
@@ -114,7 +120,9 @@ class WorkItem < Issue
.filter { |param_name| common_params.key?(param_name) }
.each do |param_name|
widget_params[widget.api_symbol] ||= {}
- widget_params[widget.api_symbol][param_name] = common_params.delete(param_name)
+ param_value = common_params.delete(param_name)
+
+ widget_params[widget.api_symbol].merge!(widget.process_quick_action_param(param_name, param_value))
end
end
diff --git a/app/models/work_items/widgets/base.rb b/app/models/work_items/widgets/base.rb
index a8b1b3f9a59..c4e87decdbf 100644
--- a/app/models/work_items/widgets/base.rb
+++ b/app/models/work_items/widgets/base.rb
@@ -15,6 +15,10 @@ module WorkItems
[]
end
+ def self.process_quick_action_param(param_name, value)
+ { param_name => value }
+ end
+
def self.callback_class
WorkItems::Callbacks.const_get(name.demodulize, false)
rescue NameError
diff --git a/app/models/work_items/widgets/current_user_todos.rb b/app/models/work_items/widgets/current_user_todos.rb
index 61c4fcb453b..64297b433dd 100644
--- a/app/models/work_items/widgets/current_user_todos.rb
+++ b/app/models/work_items/widgets/current_user_todos.rb
@@ -3,6 +3,19 @@
module WorkItems
module Widgets
class CurrentUserTodos < Base
+ def self.quick_action_commands
+ [:todo, :done]
+ end
+
+ def self.quick_action_params
+ [:todo_event]
+ end
+
+ def self.process_quick_action_param(param_name, value)
+ return super unless param_name == :todo_event
+
+ { action: value == 'done' ? 'mark_as_done' : 'add' }
+ end
end
end
end