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/ability.rb2
-rw-r--r--app/models/abuse/reports/user_mention.rb12
-rw-r--r--app/models/abuse_report.rb11
-rw-r--r--app/models/achievements/user_achievement.rb17
-rw-r--r--app/models/analytics/cycle_analytics/issue_stage_event.rb41
-rw-r--r--app/models/analytics/cycle_analytics/merge_request_stage_event.rb4
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb1
-rw-r--r--app/models/application_record.rb2
-rw-r--r--app/models/application_setting.rb39
-rw-r--r--app/models/application_setting_implementation.rb9
-rw-r--r--app/models/approval.rb3
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/badges/group_badge.rb2
-rw-r--r--app/models/bulk_import.rb4
-rw-r--r--app/models/bulk_imports/tracker.rb8
-rw-r--r--app/models/chat_name.rb2
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/build_need.rb2
-rw-r--r--app/models/ci/catalog/components_project.rb94
-rw-r--r--app/models/ci/catalog/listing.rb2
-rw-r--r--app/models/ci/catalog/resource.rb2
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/ref.rb8
-rw-r--r--app/models/ci/unlock_pipeline_request.rb53
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/concerns/prometheus_client.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb2
-rw-r--r--app/models/commit_user_mention.rb2
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb78
-rw-r--r--app/models/concerns/awardable.rb18
-rw-r--r--app/models/concerns/bulk_users_by_email_load.rb2
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb2
-rw-r--r--app/models/concerns/ci/deployable.rb39
-rw-r--r--app/models/concerns/enums/issuable_link.rb12
-rw-r--r--app/models/concerns/import_state/sidekiq_job_tracker.rb2
-rw-r--r--app/models/concerns/integrations/enable_ssl_verification.rb21
-rw-r--r--app/models/concerns/issuable_link.rb28
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/protected_ref_access.rb2
-rw-r--r--app/models/concerns/repository_storage_movable.rb1
-rw-r--r--app/models/concerns/reset_on_column_errors.rb50
-rw-r--r--app/models/concerns/reset_on_union_error.rb37
-rw-r--r--app/models/concerns/routable.rb74
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb112
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb1
-rw-r--r--app/models/container_expiration_policy.rb2
-rw-r--r--app/models/container_registry/protection.rb9
-rw-r--r--app/models/container_registry/protection/rule.rb20
-rw-r--r--app/models/design_user_mention.rb2
-rw-r--r--app/models/discussion_note.rb2
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb50
-rw-r--r--app/models/identity.rb1
-rw-r--r--app/models/integration.rb41
-rw-r--r--app/models/integrations/asana.rb18
-rw-r--r--app/models/integrations/bamboo.rb37
-rw-r--r--app/models/integrations/base_chat_notification.rb20
-rw-r--r--app/models/integrations/base_slack_notification.rb3
-rw-r--r--app/models/integrations/chat_message/alert_message.rb4
-rw-r--r--app/models/integrations/chat_message/deployment_message.rb24
-rw-r--r--app/models/integrations/chat_message/issue_message.rb6
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb18
-rw-r--r--app/models/integrations/chat_message/push_message.rb8
-rw-r--r--app/models/integrations/discord.rb38
-rw-r--r--app/models/integrations/hangouts_chat.rb23
-rw-r--r--app/models/integrations/integration_list.rb29
-rw-r--r--app/models/integrations/jira.rb92
-rw-r--r--app/models/integrations/pipelines_email.rb4
-rw-r--r--app/models/integrations/pivotaltracker.rb4
-rw-r--r--app/models/integrations/prometheus.rb2
-rw-r--r--app/models/integrations/pushover.rb4
-rw-r--r--app/models/integrations/telegram.rb10
-rw-r--r--app/models/issue.rb48
-rw-r--r--app/models/issue_user_mention.rb2
-rw-r--r--app/models/lfs_download_object.rb9
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb34
-rw-r--r--app/models/member.rb9
-rw-r--r--app/models/members/group_member.rb10
-rw-r--r--app/models/members/last_group_owner_assigner.rb4
-rw-r--r--app/models/members/member_task.rb44
-rw-r--r--app/models/merge_request.rb104
-rw-r--r--app/models/merge_request_diff.rb12
-rw-r--r--app/models/merge_request_user_mention.rb2
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/ml/model.rb9
-rw-r--r--app/models/namespace.rb21
-rw-r--r--app/models/namespace/detail.rb7
-rw-r--r--app/models/namespace_setting.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb4
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb19
-rw-r--r--app/models/note.rb16
-rw-r--r--app/models/note_diff_file.rb2
-rw-r--r--app/models/packages/protection/rule.rb37
-rw-r--r--app/models/pages/lookup_path.rb6
-rw-r--r--app/models/pages_deployment.rb8
-rw-r--r--app/models/plan_limits.rb1
-rw-r--r--app/models/preloaders/group_root_ancestor_preloader.rb2
-rw-r--r--app/models/preloaders/project_root_ancestor_preloader.rb1
-rw-r--r--app/models/preloaders/user_max_access_level_in_groups_preloader.rb18
-rw-r--r--app/models/project.rb52
-rw-r--r--app/models/project_authorization.rb1
-rw-r--r--app/models/project_import_data.rb5
-rw-r--r--app/models/project_pages_metadatum.rb1
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_team.rb6
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/models/resource_events/abuse_report_event.rb7
-rw-r--r--app/models/service_desk/custom_email_credential.rb2
-rw-r--r--app/models/service_list.rb27
-rw-r--r--app/models/snippet.rb8
-rw-r--r--app/models/snippet_user_mention.rb2
-rw-r--r--app/models/ssh_host_key.rb2
-rw-r--r--app/models/storage/hashed.rb4
-rw-r--r--app/models/storage/legacy_project.rb22
-rw-r--r--app/models/suggestion.rb2
-rw-r--r--app/models/system/broadcast_message.rb2
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/timelog.rb2
-rw-r--r--app/models/todo.rb7
-rw-r--r--app/models/tree.rb4
-rw-r--r--app/models/upload.rb1
-rw-r--r--app/models/user.rb18
-rw-r--r--app/models/user_custom_attribute.rb15
-rw-r--r--app/models/users/callout.rb5
-rw-r--r--app/models/users/credit_card_validation.rb16
-rw-r--r--app/models/users/in_product_marketing_email.rb38
-rw-r--r--app/models/users/phone_number_validation.rb4
-rw-r--r--app/models/vs_code/settings/vs_code_setting.rb15
-rw-r--r--app/models/vulnerability.rb3
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/models/work_item.rb6
-rw-r--r--app/models/work_items/parent_link.rb17
-rw-r--r--app/models/work_items/related_link_restriction.rb16
-rw-r--r--app/models/work_items/related_work_item_link.rb39
-rw-r--r--app/models/work_items/type.rb1
-rw-r--r--app/models/work_items/widgets/hierarchy.rb20
140 files changed, 1270 insertions, 862 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d8510524c1f..b8433191d84 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -166,3 +166,5 @@ class Ability
end
end
end
+
+Ability.prepend_mod_with('AbilityPrepend')
diff --git a/app/models/abuse/reports/user_mention.rb b/app/models/abuse/reports/user_mention.rb
new file mode 100644
index 00000000000..e8091089ede
--- /dev/null
+++ b/app/models/abuse/reports/user_mention.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Abuse
+ module Reports
+ class UserMention < UserMention
+ self.table_name = 'abuse_report_user_mentions'
+
+ belongs_to :abuse_report, optional: false
+ belongs_to :note, optional: false
+ end
+ end
+end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index bf25c539830..872dedf07b1 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -6,6 +6,8 @@ class AbuseReport < ApplicationRecord
include Gitlab::FileTypeDetection
include WithUploads
include Gitlab::Utils::StrongMemoize
+ include Mentionable
+ include Noteable
MAX_CHAR_LIMIT_URL = 512
MAX_FILE_SIZE = 1.megabyte
@@ -23,6 +25,9 @@ class AbuseReport < ApplicationRecord
has_many :abuse_events, class_name: 'Abuse::Event', inverse_of: :abuse_report
+ has_many :notes, as: :noteable
+ has_many :user_mentions, class_name: 'Abuse::Reports::UserMention'
+
validates :reporter, presence: true, on: :create
validates :user, presence: true, on: :create
validates :message, presence: true
@@ -158,6 +163,10 @@ class AbuseReport < ApplicationRecord
Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/'))
end
+ def group
+ Group.find_by_full_path(route_hash[:group_id])
+ end
+
def route_hash
match = Rails.application.routes.recognize_path(reported_from_url)
return {} if match[:unmatched_route].present?
@@ -200,7 +209,7 @@ class AbuseReport < ApplicationRecord
format(_('contains URLs that exceed the %{limit} character limit'), limit: MAX_CHAR_LIMIT_URL)
)
end
- rescue ::Gitlab::UrlBlocker::BlockedUrlError
+ rescue ::Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
errors.add(:links_to_spam, _('only supports valid HTTP(S) URLs'))
end
diff --git a/app/models/achievements/user_achievement.rb b/app/models/achievements/user_achievement.rb
index 08ebadaa6b0..8b15b25c183 100644
--- a/app/models/achievements/user_achievement.rb
+++ b/app/models/achievements/user_achievement.rb
@@ -15,6 +15,23 @@ module Achievements
optional: true
scope :not_revoked, -> { where(revoked_by_user_id: nil) }
+ scope :order_by_priority_asc, -> {
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'priority',
+ order_expression: ::Achievements::UserAchievement.arel_table[:priority].asc,
+ nullable: :nulls_last,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: ::Achievements::UserAchievement.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+ reorder(keyset_order)
+ }
scope :order_by_id_asc, -> { order(id: :asc) }
def revoked?
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
index 837eb35c839..1a8f1b7c84a 100644
--- a/app/models/analytics/cycle_analytics/issue_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -17,13 +17,44 @@ module Analytics
where(condition.arel.exists)
end
- def self.issuable_id_column
- :issue_id
- end
+ class << self
+ def project_column
+ :project_id
+ end
+
+ def issuable_id_column
+ :issue_id
+ end
+
+ def issuable_model
+ ::Issue
+ end
+
+ def select_columns
+ [
+ *super,
+ issuable_model.arel_table[:weight],
+ issuable_model.arel_table[:sprint_id]
+ ]
+ end
+
+ def column_list
+ [
+ *super,
+ :weight,
+ :sprint_id
+ ]
+ end
- def self.issuable_model
- ::Issue
+ def insert_column_list
+ [
+ *super,
+ :weight,
+ :sprint_id
+ ]
+ end
end
end
end
end
+Analytics::CycleAnalytics::IssueStageEvent.prepend_mod_with('Analytics::CycleAnalytics::IssueStageEvent')
diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
index 0dfa322b2c3..7f85d284034 100644
--- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -17,6 +17,10 @@ module Analytics
where(condition.arel.exists)
end
+ def self.project_column
+ :target_project_id
+ end
+
def self.issuable_id_column
:merge_request_id
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 16446a5b463..7f8c6eef704 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -51,3 +51,4 @@ module Analytics
end
end
end
+Analytics::CycleAnalytics::ValueStream.prepend_mod_with('Analytics::CycleAnalytics::ValueStream')
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 7058bfd5650..15e44296635 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -6,7 +6,7 @@ class ApplicationRecord < ActiveRecord::Base
include LegacyBulkInsert
include CrossDatabaseModification
include SensitiveSerializableHash
- include ResetOnUnionError
+ include ResetOnColumnErrors
self.abstract_class = true
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 153257636ba..824a2bd9fa4 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -16,12 +16,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
ignore_column :database_apdex_settings, remove_with: '16.4', remove_after: '2023-08-22'
- ignore_columns %i[
- dashboard_notification_limit
- dashboard_enforcement_limit
- dashboard_limit_new_namespace_creation_enforcement_date
- ], remove_with: '16.5', remove_after: '2023-08-22'
-
ignore_column %i[
relay_state_domain_allowlist
in_product_marketing_emails_enabled
@@ -36,7 +30,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
jitsu_project_xid
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
- ignore_columns %i[ai_access_token ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
+ ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -122,6 +116,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :default_branch_protection_defaults, json_schema: { filename: 'default_branch_protection_defaults' }
validates :default_branch_protection_defaults, bytesize: { maximum: -> { DEFAULT_BRANCH_PROTECTIONS_DEFAULT_MAX_SIZE } }
+ validates :failed_login_attempts_unlock_period_in_minutes,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :grafana_url,
system_hook_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({
blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}"
@@ -269,6 +267,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :max_login_attempts,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :max_pages_size,
presence: true,
numericality: {
@@ -311,7 +313,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
if: :auto_devops_enabled?
validates :enabled_git_access_protocol,
- inclusion: { in: %w[ssh http], allow_blank: true }
+ inclusion: { in: ->(_) { enabled_git_access_protocol_values }, allow_blank: true }
validates :domain_denylist,
presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' },
@@ -657,6 +659,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :throttle_authenticated_deprecated_api_period_in_seconds
validates :throttle_protected_paths_requests_per_period
validates :throttle_protected_paths_period_in_seconds
+ validates :project_jobs_api_rate_limit
end
with_options(numericality: { only_integer: true, greater_than_or_equal_to: 0 }) do
@@ -805,11 +808,12 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
attr_encrypted :openai_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :anthropic_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :vertex_ai_credentials, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
+ attr_encrypted :vertex_ai_access_token, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
# Restricting the validation to `on: :update` only to avoid cyclical dependencies with
# License <--> ApplicationSetting. This method calls a license check when we create
# ApplicationSetting from defaults which in turn depends on ApplicationSetting record.
- # The currect default is defined in the `defaults` method so we don't need to validate
+ # The correct default is defined in the `defaults` method so we don't need to validate
# it here.
validates :disable_feed_token,
inclusion: { in: [true, false], message: N_('must be a boolean value') }, on: :update
@@ -834,6 +838,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :math_rendering_limits_enabled,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
@@ -958,19 +965,31 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
false
end
+ def max_login_attempts_column_exists?
+ self.class.database.cached_column_exists?(:max_login_attempts)
+ end
+
+ def failed_login_attempts_unlock_period_in_minutes_column_exists?
+ self.class.database.cached_column_exists?(:failed_login_attempts_unlock_period_in_minutes)
+ end
+
private
def self.human_attribute_name(attribute, *options)
HUMANIZED_ATTRIBUTES[attribute.to_sym] || super
end
+ def self.enabled_git_access_protocol_values
+ %w[ssh http]
+ end
+
def parsed_grafana_url
@parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url)
end
def parsed_kroki_url
@parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w[http https], enforce_sanitization: true)[0]
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
self.errors.add(
:kroki_url,
"is not valid. #{e}"
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 5a90e246499..1bd15a56de5 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -51,6 +51,7 @@ module ApplicationSettingImplementation
container_registry_token_expire_delay: 5,
container_registry_vendor: '',
container_registry_version: '',
+ container_registry_db_enabled: false,
custom_http_clone_url_root: nil,
decompress_archive_file_timeout: 210,
default_artifacts_expire_in: '30 days',
@@ -87,6 +88,7 @@ module ApplicationSettingImplementation
external_pipeline_validation_service_timeout: nil,
external_pipeline_validation_service_token: nil,
external_pipeline_validation_service_url: nil,
+ failed_login_attempts_unlock_period_in_minutes: nil,
first_day_of_week: 0,
floc_enabled: false,
gitaly_timeout_default: 55,
@@ -117,12 +119,14 @@ module ApplicationSettingImplementation
login_recaptcha_protection_enabled: false,
mailgun_signing_key: nil,
mailgun_events_enabled: false,
+ math_rendering_limits_enabled: true,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
+ max_decompressed_archive_size: 25600,
max_export_size: 0,
max_import_size: 0,
max_import_remote_file_size: 10240,
- max_decompressed_archive_size: 25600,
+ max_login_attempts: nil,
max_terraform_state_size_bytes: 0,
max_yaml_size_bytes: 1.megabyte,
max_yaml_depth: 100,
@@ -267,7 +271,8 @@ module ApplicationSettingImplementation
gitlab_dedicated_instance: false,
ci_max_includes: 150,
allow_account_deletion: true,
- gitlab_shell_operation_limit: 600
+ gitlab_shell_operation_limit: 600,
+ project_jobs_api_rate_limit: 600
}.tap do |hsh|
hsh.merge!(non_production_defaults) unless Rails.env.production?
end
diff --git a/app/models/approval.rb b/app/models/approval.rb
index ecc15077c8d..c3992994dd3 100644
--- a/app/models/approval.rb
+++ b/app/models/approval.rb
@@ -14,4 +14,7 @@ class Approval < ApplicationRecord
validates :user_id, presence: true, uniqueness: { scope: [:merge_request_id] }
scope :with_user, -> { joins(:user) }
+ scope :with_invalid_patch_id_sha, ->(patch_id_sha) do
+ where.not(patch_id_sha: patch_id_sha).or(where(patch_id_sha: nil))
+ end
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 73e3fa709b0..e445d08a096 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -9,7 +9,7 @@ class AwardEmoji < ApplicationRecord
include Importable
include IgnorableColumns
- ignore_column :awardable_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :awardable_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb
index c0712f452df..f74c9f89e9f 100644
--- a/app/models/badges/group_badge.rb
+++ b/app/models/badges/group_badge.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GroupBadge < Badge
+ include EachBatch
+
belongs_to :group
validates :group, presence: true
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index fde528e3fa0..a7ace7429d7 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -76,4 +76,8 @@ class BulkImport < ApplicationRecord
def supports_batched_export?
source_version_info >= self.class.min_gl_version_for_migration_in_batches
end
+
+ def completed?
+ finished? || failed? || timeout?
+ end
end
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index d1a6f3b9a80..d9efd489af5 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -33,11 +33,9 @@ class BulkImports::Tracker < ApplicationRecord
entity_scope.where(stage: next_stage_scope).with_status(:created)
}
- def self.stage_running?(entity_id, stage)
- where(stage: stage, bulk_import_entity_id: entity_id)
- .with_status(:created, :enqueued, :started)
- .exists?
- end
+ scope :running_trackers, -> (entity_id) {
+ where(bulk_import_entity_id: entity_id).with_status(:enqueued, :started)
+ }
def pipeline_class
unless entity.pipeline_exists?(pipeline_name)
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index d3fbfe3aa55..38e6273bf20 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -27,6 +27,6 @@ class ChatName < ApplicationRecord
end
def update_last_used_at?
- last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago
+ last_used_at.nil? || last_used_at.before?(LAST_USED_AT_INTERVAL.ago)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 2abb8e4be48..d2cf9058976 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,6 +11,7 @@ module Ci
include Importable
include Ci::HasRef
include Ci::TrackEnvironmentUsage
+ include EachBatch
extend ::Gitlab::Utils::Override
@@ -414,7 +415,7 @@ module Ci
end
def options_scheduled_at
- ChronicDuration.parse(options[:start_in], use_complete_matcher: true)&.seconds&.from_now
+ ChronicDuration.parse(options[:start_in])&.seconds&.from_now
end
def action?
@@ -738,7 +739,7 @@ module Ci
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
- ChronicDuration.parse(value, use_complete_matcher: true)&.seconds&.from_now
+ ChronicDuration.parse(value)&.seconds&.from_now
end
end
@@ -1090,7 +1091,7 @@ module Ci
end
def has_expiring_artifacts?
- artifacts_expire_at.present? && artifacts_expire_at > Time.current
+ artifacts_expire_at.present? && artifacts_expire_at.future?
end
def job_jwt_variables
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 4c723bb7c0c..555565ff621 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -14,7 +14,7 @@ module Ci
self.table_name = 'p_ci_builds_metadata'
self.primary_key = 'id'
- partitionable scope: :build
+ partitionable scope: :build, partitioned: true
belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 00241908644..1831b7868f9 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -7,7 +7,7 @@ module Ci
include SafelyChangeColumnDefault
include BulkInsertSafe
- MAX_JOB_NAME_LENGTH = 128
+ MAX_JOB_NAME_LENGTH = 255
columns_changing_default :partition_id
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
new file mode 100644
index 00000000000..2bc33a6f050
--- /dev/null
+++ b/app/models/ci/catalog/components_project.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ class ComponentsProject
+ # ComponentsProject is a type of Catalog Resource which contains one or more
+ # CI/CD components.
+ # It is responsible for retrieving the data of a component file, including the content, name, and file path.
+
+ TEMPLATE_FILE = 'template.yml'
+ TEMPLATES_DIR = 'templates'
+ TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+
+ ComponentData = Struct.new(:content, :path, keyword_init: true)
+
+ def initialize(project, sha = project&.default_branch)
+ @project = project
+ @sha = sha
+ end
+
+ def fetch_component_paths(sha)
+ project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha)
+ end
+
+ def extract_component_name(path)
+ return unless path.match?(TEMPLATE_PATH_REGEX)
+
+ dirname = File.dirname(path)
+ filename = File.basename(path, '.*')
+
+ if dirname == TEMPLATES_DIR
+ filename
+ else
+ File.basename(dirname)
+ end
+ end
+
+ def extract_inputs(blob)
+ result = Gitlab::Ci::Config::Yaml::Loader.new(blob).load_uninterpolated_yaml
+
+ raise result.error_class, result.error unless result.valid?
+
+ result.inputs
+ end
+
+ def fetch_component(component_name)
+ path = simple_template_path(component_name)
+ content = fetch_content(path)
+
+ if content.nil?
+ path = complex_template_path(component_name)
+ content = fetch_content(path)
+ end
+
+ if content.nil?
+ path = legacy_template_path(component_name)
+ content = fetch_content(path)
+ end
+
+ ComponentData.new(content: content, path: path)
+ end
+
+ private
+
+ attr_reader :project, :sha
+
+ def fetch_content(component_path)
+ project.repository.blob_data_at(sha, component_path)
+ end
+
+ # A simple template consists of a single file
+ def simple_template_path(component_name)
+ # TODO: Extract this line and move to fetch_content once we remove legacy fetching
+ return unless component_name.index('/').nil?
+
+ File.join(TEMPLATES_DIR, "#{component_name}.yml")
+ end
+
+ # A complex template is directory-based and may consist of multiple files.
+ # Given a path like "my-org/sub-group/the-project/templates/component"
+ # returns the entry point path: "templates/component/template.yml".
+ def complex_template_path(component_name)
+ # TODO: Extract this line and move to fetch_content once we remove legacy fetching
+ return unless component_name.index('/').nil?
+
+ File.join(TEMPLATES_DIR, component_name, TEMPLATE_FILE)
+ end
+
+ def legacy_template_path(component_name)
+ File.join(component_name, TEMPLATE_FILE).delete_prefix('/')
+ end
+ end
+ end
+end
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index 1cb030c67c3..c3b18af8c3f 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -18,6 +18,8 @@ module Ci
case sort.to_s
when 'name_desc' then all_resources.order_by_name_desc
when 'name_asc' then all_resources.order_by_name_asc
+ when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc
+ when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc
else
all_resources.order_by_created_at_desc
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 799cdce4af7..8ffc0292a69 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -18,6 +18,8 @@ module Ci
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
+ scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
+ scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 3f9d8f07b06..2a346f97958 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -310,7 +310,7 @@ module Ci
end
def expiring?
- expire_at.present? && expire_at > Time.current
+ expire_at.present? && expire_at.future?
end
def expire_in
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5bf4e846304..0a876d26cc9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1366,6 +1366,11 @@ module Ci
merge_request.merge_request_diff_for(merge_request_diff_sha)
end
+ def reduced_build_attributes_list_for_rules?
+ ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project)
+ end
+ strong_memoize_attr :reduced_build_attributes_list_for_rules?
+
private
def add_message(severity, content)
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 199e1cd07e7..8655e8eb9b8 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -36,7 +36,7 @@ module Ci
next unless ci_ref.artifacts_locked?
ci_ref.run_after_commit do
- Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id)
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id)
end
end
end
@@ -52,7 +52,11 @@ module Ci
end
def last_finished_pipeline_id
- Ci::Pipeline.last_finished_for_ref_id(self.id)&.id
+ last_finished_pipeline&.id
+ end
+
+ def last_finished_pipeline
+ Ci::Pipeline.last_finished_for_ref_id(self.id)
end
def artifacts_locked?
diff --git a/app/models/ci/unlock_pipeline_request.rb b/app/models/ci/unlock_pipeline_request.rb
new file mode 100644
index 00000000000..c8fc82f3e55
--- /dev/null
+++ b/app/models/ci/unlock_pipeline_request.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Ci
+ class UnlockPipelineRequest
+ QUEUE_REDIS_KEY = 'ci_unlock_pipeline_requests:queue'
+
+ def self.enqueue(pipeline_id)
+ unix_timestamp = Time.current.utc.to_i
+ pipeline_ids = Array(pipeline_id).uniq
+ pipeline_ids_with_scores = pipeline_ids.map do |id|
+ # The order of values per pair is `[score, key]`, so in this case, the unix timestamp is the score.
+ # By default, the sort order of sorted sets is from lowest to highest, though this does not matter much
+ # because we use `ZPOPMIN` to make sure to return the lowest/oldest request in terms of unix timestamp score.
+ [unix_timestamp, id]
+ end
+
+ with_redis do |redis|
+ added = redis.zadd(QUEUE_REDIS_KEY, pipeline_ids_with_scores, nx: true)
+ log_event(:enqueued, pipeline_ids) if added > 0
+ added
+ end
+ end
+
+ def self.next!
+ with_redis do |redis|
+ pipeline_id, enqueue_timestamp = redis.zpopmin(QUEUE_REDIS_KEY)
+ break unless pipeline_id
+
+ pipeline_id = pipeline_id.to_i
+ log_event(:picked_next, pipeline_id)
+
+ [pipeline_id, enqueue_timestamp.to_i]
+ end
+ end
+
+ def self.total_pending
+ with_redis do |redis|
+ redis.zcard(QUEUE_REDIS_KEY)
+ end
+ end
+
+ def self.with_redis(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+
+ def self.log_event(event, pipeline_id)
+ Gitlab::AppLogger.info(
+ message: "Pipeline unlock - #{event}",
+ pipeline_id: pipeline_id
+ )
+ end
+ end
+end
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index f4c497a42cc..e2754db73b9 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -33,6 +33,10 @@ module Clusters
revoked: 1
}
+ def revoke!
+ update(status: :revoked)
+ end
+
def to_ability_name
:cluster
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index f9a34959675..5bd55fd6f4c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -24,7 +24,6 @@ module Clusters
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :deployment_clusters
has_many :deployments, inverse_of: :cluster, through: :deployment_clusters
- has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_many :environments, -> { distinct }, through: :deployments
has_many :cluster_groups, class_name: 'Clusters::Group'
diff --git a/app/models/clusters/concerns/prometheus_client.rb b/app/models/clusters/concerns/prometheus_client.rb
index d2f69b813aa..b4234e9cc0a 100644
--- a/app/models/clusters/concerns/prometheus_client.rb
+++ b/app/models/clusters/concerns/prometheus_client.rb
@@ -35,7 +35,7 @@ module Clusters
def configured?
kube_client.present? && available?
- rescue Gitlab::UrlBlocker::BlockedUrlError
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
false
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 5efbec45561..6ae0cd8e3fd 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -12,7 +12,7 @@ module Clusters
REQUIRED_K8S_MIN_VERSION = 23
IGNORED_CONNECTION_EXCEPTIONS = [
- Gitlab::UrlBlocker::BlockedUrlError,
+ Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError,
Kubeclient::HttpError,
Errno::ECONNREFUSED,
URI::InvalidURIError,
diff --git a/app/models/commit_user_mention.rb b/app/models/commit_user_mention.rb
index 9215e15f07d..fa7f065b6b4 100644
--- a/app/models/commit_user_mention.rb
+++ b/app/models/commit_user_mention.rb
@@ -3,7 +3,7 @@
class CommitUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :note
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index d268c32c088..1d9cf5729cd 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -16,6 +16,7 @@ module Analytics
scope :start_event_timestamp_before, -> (date) { where(arel_table[:start_event_timestamp].lteq(date)) }
scope :authored, ->(user) { where(author_id: user) }
scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
+ scope :without_milestone_id, -> (milestone_id) { where('milestone_id <> ? or milestone_id IS NULL', milestone_id) }
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
scope :order_by_end_event, -> (direction) do
# ORDER BY end_event_timestamp, merge_request_id/issue_id, start_event_timestamp
@@ -57,45 +58,19 @@ module Analytics
class_methods do
def upsert_data(data)
- upsert_values = data.map do |row|
- row.values_at(
- :stage_event_hash_id,
- :issuable_id,
- :group_id,
- :project_id,
- :milestone_id,
- :author_id,
- :state_id,
- :start_event_timestamp,
- :end_event_timestamp
- )
- end
+ upsert_values = data.map { |row| row.values_at(*column_list) }
value_list = Arel::Nodes::ValuesList.new(upsert_values).to_sql
query = <<~SQL
INSERT INTO #{quoted_table_name}
(
- stage_event_hash_id,
- #{connection.quote_column_name(issuable_id_column)},
- group_id,
- project_id,
- milestone_id,
- author_id,
- state_id,
- start_event_timestamp,
- end_event_timestamp
+ #{insert_column_list.join(",\n")}
)
#{value_list}
ON CONFLICT(stage_event_hash_id, #{issuable_id_column})
DO UPDATE SET
- group_id = excluded.group_id,
- project_id = excluded.project_id,
- milestone_id = excluded.milestone_id,
- author_id = excluded.author_id,
- state_id = excluded.state_id,
- start_event_timestamp = excluded.start_event_timestamp,
- end_event_timestamp = excluded.end_event_timestamp
+ #{column_updates.join(",\n")}
SQL
result = connection.execute(query)
@@ -113,6 +88,51 @@ module Analytics
def arel_order(arel_node, direction)
direction.to_sym == :desc ? arel_node.desc : arel_node.asc
end
+
+ def select_columns
+ [
+ issuable_model.arel_table[:id],
+ issuable_model.arel_table[project_column].as('project_id'),
+ issuable_model.arel_table[:milestone_id],
+ issuable_model.arel_table[:author_id],
+ issuable_model.arel_table[:state_id],
+ Project.arel_table[:parent_id].as('group_id')
+ ]
+ end
+
+ def column_list
+ [
+ :stage_event_hash_id,
+ :issuable_id,
+ :group_id,
+ :project_id,
+ :milestone_id,
+ :author_id,
+ :state_id,
+ :start_event_timestamp,
+ :end_event_timestamp
+ ]
+ end
+
+ def insert_column_list
+ [
+ :stage_event_hash_id,
+ connection.quote_column_name(issuable_id_column),
+ :group_id,
+ :project_id,
+ :milestone_id,
+ :author_id,
+ :state_id,
+ :start_event_timestamp,
+ :end_event_timestamp
+ ]
+ end
+
+ def column_updates
+ insert_column_list.map do |column|
+ "#{column} = excluded.#{column}"
+ end
+ end
end
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index e830594af11..22e71c4fa13 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -13,26 +13,26 @@ module Awardable
end
class_methods do
- def awarded(user, name = nil)
+ def awarded(user, name = nil, base_class_name = base_class.name, awardable_id_column = :id)
award_emoji_table = Arel::Table.new('award_emoji')
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
- .where(award_emoji_table[:awardable_type].eq(base_class.name))
- .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
+ .where(award_emoji_table[:awardable_type].eq(base_class_name))
+ .where(award_emoji_table[:awardable_id].eq(self.arel_table[awardable_id_column]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
where(inner_query.exists)
end
- def not_awarded(user, name = nil)
+ def not_awarded(user, name = nil, base_class_name = base_class.name, awardable_id_column = :id)
award_emoji_table = Arel::Table.new('award_emoji')
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
- .where(award_emoji_table[:awardable_type].eq(base_class.name))
- .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
+ .where(award_emoji_table[:awardable_type].eq(base_class_name))
+ .where(award_emoji_table[:awardable_id].eq(self.arel_table[awardable_id_column]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
@@ -52,14 +52,14 @@ module Awardable
end
# Order votes by emoji, optional sort order param `descending` defaults to true
- def order_votes(emoji_name, direction)
+ def order_votes(emoji_name, direction, base_class_name = base_class.name, awardable_id_column = :id)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table
.join(awards_table, Arel::Nodes::OuterJoin)
- .on(awards_table[:awardable_id].eq(awardable_table[:id])
- .and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name))))
+ .on(awards_table[:awardable_id].eq(awardable_table[awardable_id_column])
+ .and(awards_table[:awardable_type].eq(base_class_name).and(awards_table[:name].eq(emoji_name))))
.join_sources
joins(join_clause).group(awardable_table[:id]).reorder(
diff --git a/app/models/concerns/bulk_users_by_email_load.rb b/app/models/concerns/bulk_users_by_email_load.rb
index edbd3e21458..55143ead30a 100644
--- a/app/models/concerns/bulk_users_by_email_load.rb
+++ b/app/models/concerns/bulk_users_by_email_load.rb
@@ -7,7 +7,7 @@ module BulkUsersByEmailLoad
def users_by_emails(emails)
Gitlab::SafeRequestLoader.execute(resource_key: user_by_email_resource_key, resource_ids: emails) do |emails|
# have to consider all emails - even secondary, so use all_emails here
- grouped_users_by_email = User.by_any_email(emails).preload(:emails).group_by(&:all_emails)
+ grouped_users_by_email = User.by_any_email(emails, confirmed: true).preload(:emails).group_by(&:all_emails)
grouped_users_by_email.each_with_object({}) do |(found_emails, users), h|
found_emails.each { |e| h[e] = users.first if emails.include?(e) } # don't include all emails for an account, only the ones we want
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
index 7b7b61fdf06..44b34cf9b2f 100644
--- a/app/models/concerns/chronic_duration_attribute.rb
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -18,7 +18,7 @@ module ChronicDurationAttribute
begin
new_value = if value.present?
- ChronicDuration.parse(value, use_complete_matcher: true).to_i
+ ChronicDuration.parse(value).to_i
else
parameters[:default].presence
end
diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb
index d25151f9a34..844c8a1fa7d 100644
--- a/app/models/concerns/ci/deployable.rb
+++ b/app/models/concerns/ci/deployable.rb
@@ -4,6 +4,7 @@
module Ci
module Deployable
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
included do
prepend_mod_with('Ci::Deployable') # rubocop: disable Cop/InjectEnterpriseEditionModule
@@ -17,8 +18,16 @@ module Ci
end
end
+ after_transition any => [:failed] do |job|
+ next unless job.stops_environment?
+
+ job.run_after_commit do
+ Environments::StopJobFailedWorker.perform_async(id)
+ end
+ end
+
# Synchronize Deployment Status
- # Please note that the data integirty is not assured because we can't use
+ # Please note that the data integrity is not assured because we can't use
# a database transaction due to DB decomposition.
after_transition do |job, transition|
next if transition.loopback?
@@ -32,13 +41,12 @@ module Ci
end
def outdated_deployment?
- strong_memoize(:outdated_deployment) do
- deployment_job? &&
- project.ci_forward_deployment_enabled? &&
- (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
- deployment&.older_than_last_successful_deployment?
- end
+ deployment_job? &&
+ project.ci_forward_deployment_enabled? &&
+ (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
+ deployment&.older_than_last_successful_deployment?
end
+ strong_memoize_attr :outdated_deployment?
# Virtual deployment status depending on the environment status.
def deployment_status
@@ -106,10 +114,10 @@ module Ci
namespace = options.dig(:environment, :kubernetes, :namespace)
- if namespace.present? # rubocop:disable Style/GuardClause
- strong_memoize(:expanded_kubernetes_namespace) do
- ExpandVariables.expand(namespace, -> { simple_variables })
- end
+ return unless namespace.present?
+
+ strong_memoize(:expanded_kubernetes_namespace) do
+ ExpandVariables.expand(namespace, -> { simple_variables })
end
end
@@ -146,12 +154,11 @@ module Ci
end
def environment_status
- strong_memoize(:environment_status) do
- if has_environment_keyword? && merge_request
- EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
- end
- end
+ return unless has_environment_keyword? && merge_request
+
+ EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
end
+ strong_memoize_attr :environment_status
def on_stop
options&.dig(:environment, :on_stop)
diff --git a/app/models/concerns/enums/issuable_link.rb b/app/models/concerns/enums/issuable_link.rb
new file mode 100644
index 00000000000..ca5728c2600
--- /dev/null
+++ b/app/models/concerns/enums/issuable_link.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Enums
+ module IssuableLink
+ TYPE_RELATES_TO = 'relates_to'
+ TYPE_BLOCKS = 'blocks'
+
+ def self.link_types
+ { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+ end
+ end
+end
diff --git a/app/models/concerns/import_state/sidekiq_job_tracker.rb b/app/models/concerns/import_state/sidekiq_job_tracker.rb
index b7d0ed0f51b..9c892acb158 100644
--- a/app/models/concerns/import_state/sidekiq_job_tracker.rb
+++ b/app/models/concerns/import_state/sidekiq_job_tracker.rb
@@ -19,7 +19,7 @@ module ImportState
end
def self.jid_by(project_id:, status:)
- select(:jid).where(status: status).find_by(project_id: project_id)
+ select(:id, :jid).where(status: status).find_by(project_id: project_id)
end
end
end
diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb
index 9735a9bf5f6..cb20955488a 100644
--- a/app/models/concerns/integrations/enable_ssl_verification.rb
+++ b/app/models/concerns/integrations/enable_ssl_verification.rb
@@ -5,7 +5,11 @@ module Integrations
extend ActiveSupport::Concern
prepended do
- boolean_accessor :enable_ssl_verification
+ field :enable_ssl_verification,
+ type: :checkbox,
+ title: -> { s_('Integrations|SSL verification') },
+ checkbox_label: -> { s_('Integrations|Enable SSL verification') },
+ help: -> { s_('Integrations|Clear if using a self-signed certificate.') }
end
def initialize_properties
@@ -17,18 +21,11 @@ module Integrations
def fields
super.tap do |fields|
url_index = fields.index { |field| field[:name].ends_with?('_url') }
- insert_index = url_index ? url_index + 1 : -1
+ insert_index = url_index || -1
- fields.insert(insert_index,
- Field.new(
- name: 'enable_ssl_verification',
- integration_class: self,
- type: :checkbox,
- title: s_('Integrations|SSL verification'),
- checkbox_label: s_('Integrations|Enable SSL verification'),
- help: s_('Integrations|Clear if using a self-signed certificate.')
- )
- )
+ enable_ssl_verification_index = fields.index { |field| field[:name] == 'enable_ssl_verification' }
+
+ fields.insert(insert_index, fields.delete_at(enable_ssl_verification_index))
end
end
end
diff --git a/app/models/concerns/issuable_link.rb b/app/models/concerns/issuable_link.rb
index e884e5acecf..dcd2705185f 100644
--- a/app/models/concerns/issuable_link.rb
+++ b/app/models/concerns/issuable_link.rb
@@ -9,8 +9,8 @@
module IssuableLink
extend ActiveSupport::Concern
- TYPE_RELATES_TO = 'relates_to'
- TYPE_BLOCKS = 'blocks' ## EE-only. Kept here to be used on link_type enum.
+ MAX_LINKS_COUNT = 100
+ TYPE_RELATES_TO = Enums::IssuableLink::TYPE_RELATES_TO
class_methods do
def inverse_link_type(type)
@@ -38,10 +38,11 @@ module IssuableLink
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
validate :check_opposite_relation
+ validate :validate_max_number_of_links, on: :create
scope :for_source_or_target, ->(issuable) { where(source: issuable).or(where(target: issuable)) }
- enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+ enum link_type: Enums::IssuableLink.link_types
private
@@ -60,6 +61,27 @@ module IssuableLink
errors.add(:source, "is already related to this #{self.class.issuable_name}")
end
end
+
+ def validate_max_number_of_links
+ return unless source && target
+
+ validate_max_number_of_links_for(source, :source)
+ validate_max_number_of_links_for(target, :target)
+ end
+
+ def validate_max_number_of_links_for(item, attribute_name)
+ return unless item.linked_items_count >= MAX_LINKS_COUNT
+
+ errors.add(
+ attribute_name,
+ format(
+ s_('This %{issuable} would exceed the maximum number of linked %{issuables} (%{limit}).'),
+ issuable: self.class.issuable_name,
+ issuables: self.class.issuable_name.pluralize,
+ limit: MAX_LINKS_COUNT
+ )
+ )
+ end
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 06cee46645b..971089edc45 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -12,12 +12,12 @@ module Noteable
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
- %w[Issue MergeRequest]
+ %w[Issue MergeRequest AbuseReport]
end
# `Noteable` class names that support resolvable notes.
def resolvable_types
- %w[Issue MergeRequest DesignManagement::Design]
+ %w[Issue MergeRequest DesignManagement::Design AbuseReport]
end
# `Noteable` class names that support creating/forwarding individual notes.
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index f0bb1cc359b..a5994b538ce 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -71,6 +71,8 @@ module ProtectedRefAccess
return false if current_user.nil? || no_access?
return current_user.admin? if admin_access?
+ return false if Feature.enabled?(:check_membership_in_protected_ref_access) && !project.member?(current_user)
+
yield if block_given?
user_can_access?(current_user)
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 87ff413f2c1..77edabb9706 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -49,6 +49,7 @@ module RepositoryStorageMovable
begin
storage_move.container.set_repository_read_only!(skip_git_transfer_check: true)
rescue StandardError => e
+ storage_move.do_fail!
storage_move.add_error(e.message)
next false
end
diff --git a/app/models/concerns/reset_on_column_errors.rb b/app/models/concerns/reset_on_column_errors.rb
new file mode 100644
index 00000000000..8ace52ebff5
--- /dev/null
+++ b/app/models/concerns/reset_on_column_errors.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ResetOnColumnErrors
+ extend ActiveSupport::Concern
+
+ MAX_RESET_PERIOD = 10.minutes
+
+ included do |base|
+ base.rescue_from ActiveRecord::StatementInvalid, with: :reset_on_union_error
+ base.rescue_from ActiveModel::UnknownAttributeError, with: :reset_on_unknown_attribute_error
+
+ base.class_attribute :previous_reset_columns_from_error
+ end
+
+ class_methods do
+ def do_reset(exception)
+ class_to_be_reset = base_class
+
+ class_to_be_reset.reset_column_information
+ Gitlab::ErrorTracking.log_exception(exception, { reset_model_name: class_to_be_reset.name })
+
+ class_to_be_reset.previous_reset_columns_from_error = Time.current
+ end
+
+ def reset_on_union_error(exception)
+ if exception.message.include?("each UNION query must have the same number of columns") && should_reset?
+ do_reset(exception)
+ end
+
+ raise
+ end
+
+ def should_reset?
+ return false if base_class.previous_reset_columns_from_error? &&
+ base_class.previous_reset_columns_from_error > MAX_RESET_PERIOD.ago
+
+ Feature.enabled?(:reset_column_information_on_statement_invalid, type: :ops)
+ end
+ end
+
+ def reset_on_union_error(exception)
+ self.class.reset_on_union_error(exception)
+ end
+
+ def reset_on_unknown_attribute_error(exception)
+ self.class.do_reset(exception) if self.class.should_reset?
+
+ raise
+ end
+end
diff --git a/app/models/concerns/reset_on_union_error.rb b/app/models/concerns/reset_on_union_error.rb
deleted file mode 100644
index 42e350b0bed..00000000000
--- a/app/models/concerns/reset_on_union_error.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module ResetOnUnionError
- extend ActiveSupport::Concern
-
- MAX_RESET_PERIOD = 10.minutes
-
- included do |base|
- base.rescue_from ActiveRecord::StatementInvalid, with: :reset_on_union_error
-
- base.class_attribute :previous_reset_columns_from_error
- end
-
- class_methods do
- def reset_on_union_error(exception)
- if reset_on_statement_invalid?(exception)
- class_to_be_reset = base_class
-
- class_to_be_reset.reset_column_information
- Gitlab::ErrorTracking.log_exception(exception, { reset_model_name: class_to_be_reset.name })
-
- class_to_be_reset.previous_reset_columns_from_error = Time.current
- end
-
- raise
- end
-
- def reset_on_statement_invalid?(exception)
- return false unless exception.message.include?("each UNION query must have the same number of columns")
-
- return false if base_class.previous_reset_columns_from_error? &&
- base_class.previous_reset_columns_from_error > MAX_RESET_PERIOD.ago
-
- Feature.enabled?(:reset_column_information_on_statement_invalid, type: :ops)
- end
- end
-end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index ef14ff5fbe2..4c16ba18823 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -15,16 +15,7 @@ module Routable
#
# Returns a single object, or nil.
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def self.find_by_full_path(
- path,
- follow_redirects: false,
- route_scope: Route,
- redirect_route_scope: RedirectRoute,
- optimize_routable: Routable.optimize_routable_enabled?
- )
-
+ def self.find_by_full_path(path, follow_redirects: false, route_scope: nil)
return unless path.present?
# Convert path to string to prevent DB error: function lower(integer) does not exist
@@ -35,49 +26,22 @@ module Routable
#
# We need to qualify the columns with the table name, to support both direct lookups on
# Route/RedirectRoute, and scoped lookups through the Routable classes.
- if optimize_routable
- path_condition = { path: path }
-
- source_type_condition = if route_scope == Route
- {}
- else
- { source_type: route_scope.klass.base_class }
- end
+ path_condition = { path: path }
- route =
- Route.where(source_type_condition).find_by(path_condition) ||
- Route.where(source_type_condition).iwhere(path_condition).take
+ source_type_condition = route_scope ? { source_type: route_scope.klass.base_class } : {}
- if follow_redirects
- route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take
- end
+ route =
+ Route.where(source_type_condition).find_by(path_condition) ||
+ Route.where(source_type_condition).iwhere(path_condition).take
- return unless route
- return route.source if route_scope == Route
-
- route_scope.find_by(id: route.source_id)
- else
- Gitlab::Database.allow_cross_joins_across_databases(url:
- "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") do
- route =
- route_scope.find_by(routes: { path: path }) ||
- route_scope.iwhere(Route.arel_table[:path] => path).take
-
- if follow_redirects
- route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take
- end
-
- next unless route
-
- route.is_a?(Routable) ? route : route.source
- end
+ if follow_redirects
+ route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take
end
- end
- # rubocop:enable Metrics/PerceivedComplexity
- # rubocop:enable Metrics/CyclomaticComplexity
- def self.optimize_routable_enabled?
- Feature.enabled?(:optimize_routable)
+ return unless route
+ return route.source unless route_scope
+
+ route_scope.find_by(id: route.source_id)
end
included do
@@ -107,22 +71,12 @@ module Routable
#
# Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false)
- optimize_routable = Routable.optimize_routable_enabled?
-
- if optimize_routable
- route_scope = all
- redirect_route_scope = RedirectRoute
- else
- route_scope = includes(:route).references(:routes)
- redirect_route_scope = joins(:redirect_routes)
- end
+ route_scope = all
Routable.find_by_full_path(
path,
follow_redirects: follow_redirects,
- route_scope: route_scope,
- redirect_route_scope: redirect_route_scope,
- optimize_routable: optimize_routable
+ route_scope: route_scope
)
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
deleted file mode 100644
index 5455a2159cd..00000000000
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-module Storage
- module LegacyNamespace
- extend ActiveSupport::Concern
-
- include Gitlab::ShellAdapter
-
- def move_dir
- proj_with_tags = first_project_with_container_registry_tags
-
- if proj_with_tags
- raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry"
- end
-
- parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
- Namespace.find(parent_id_before_last_save) # raise NotFound early if needed
- end
-
- if saved_change_to_parent?
- former_parent_full_path = parent_was&.full_path
- parent_full_path = parent&.full_path
- Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
- else
- Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
- end
-
- # If repositories moved successfully we need to
- # send update instructions to users.
- # However we cannot allow rollback since we moved namespace dir
- # So we basically we mute exceptions in next actions
- begin
- send_update_instructions
- write_projects_repository_config
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
- full_path_before_last_save: full_path_before_last_save,
- full_path: full_path,
- action: 'move_dir')
- end
-
- true # false would cancel later callbacks but not rollback
- end
-
- # Hooks
-
- # Save the storages before the projects are destroyed to use them on after destroy
- def prepare_for_destroy
- old_repository_storages
- end
-
- private
-
- def move_repositories
- # Move the namespace directory in all storages used by member projects
- repository_storages(legacy_only: true).each do |repository_storage|
- # Ensure old directory exists before moving it
- Gitlab::GitalyClient::NamespaceService.allow do
- gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
-
- # Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
-
- unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
-
- Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}")
-
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError, 'namespace directory cannot be moved'
- end
- end
- end
- end
-
- def old_repository_storages
- @old_repository_storage_paths ||= repository_storages(legacy_only: true)
- end
-
- def repository_storages(legacy_only: false)
- # We need to get the storage paths for all the projects, even the ones that are
- # pending delete. Unscoping also get rids of the default order, which causes
- # problems with SELECT DISTINCT.
- Project.unscoped do
- namespace_projects = all_projects
- namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only
- namespace_projects.pluck(Arel.sql('distinct(repository_storage)'))
- end
- end
-
- def rm_dir
- # Remove the namespace directory in all storages paths used by member projects
- old_repository_storages.each do |repository_storage|
- # Move namespace directory into trash.
- # We will remove it later async
- new_path = "#{full_path}+#{id}+deleted"
-
- Gitlab::GitalyClient::NamespaceService.allow do
- if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
- Gitlab::AppLogger.info %(Namespace directory "#{full_path}" moved to "#{new_path}")
-
- # Remove namespace directory async with delay so
- # GitLab has time to remove all projects first
- run_after_commit do
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index e8a50497b20..94d091e8459 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -50,6 +50,7 @@ module VulnerabilityFindingHelpers
finding_data = report_finding.to_hash.except(
:compare_key, :identifiers, :location, :scanner, :links, :signatures, :flags, :evidence
)
+
identifiers = report_finding.identifiers.uniq(&:fingerprint).map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash.merge({ project: project }))
end
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index f643fa7730b..a7ed5e28695 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -80,7 +80,7 @@ class ContainerExpirationPolicy < ApplicationRecord
end
def set_next_run_at
- cadence_seconds = ChronicDuration.parse(cadence, use_complete_matcher: true).seconds
+ cadence_seconds = ChronicDuration.parse(cadence).seconds
self.next_run_at = Time.zone.now + cadence_seconds
end
diff --git a/app/models/container_registry/protection.rb b/app/models/container_registry/protection.rb
new file mode 100644
index 00000000000..33c94c0c893
--- /dev/null
+++ b/app/models/container_registry/protection.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ def self.table_name_prefix
+ 'container_registry_protection_'
+ end
+ end
+end
diff --git a/app/models/container_registry/protection/rule.rb b/app/models/container_registry/protection/rule.rb
new file mode 100644
index 00000000000..a91f3633d75
--- /dev/null
+++ b/app/models/container_registry/protection/rule.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class Rule < ApplicationRecord
+ enum delete_protected_up_to_access_level:
+ Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer),
+ _prefix: :delete_protected_up_to
+ enum push_protected_up_to_access_level:
+ Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer),
+ _prefix: :push_protected_up_to
+
+ belongs_to :project, inverse_of: :container_registry_protection_rules
+
+ validates :container_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 }
+ validates :delete_protected_up_to_access_level, presence: true
+ validates :push_protected_up_to_access_level, presence: true
+ end
+ end
+end
diff --git a/app/models/design_user_mention.rb b/app/models/design_user_mention.rb
index 7d0cd72e9eb..ba1ef1b5712 100644
--- a/app/models/design_user_mention.rb
+++ b/app/models/design_user_mention.rb
@@ -3,7 +3,7 @@
class DesignUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :design, class_name: 'DesignManagement::Design'
belongs_to :note
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index a1dfa0e72ec..fa830179022 100644
--- a/app/models/discussion_note.rb
+++ b/app/models/discussion_note.rb
@@ -9,7 +9,7 @@ class DiscussionNote < Note
# Names of all implementers of `Noteable` that support discussions.
def self.noteable_types
- %w[MergeRequest Issue Commit Snippet]
+ %w[MergeRequest Issue Commit Snippet AbuseReport]
end
validates :noteable_type, inclusion: { in: noteable_types }
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 29394c37e2c..efdcf7174aa 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -195,6 +195,10 @@ class Environment < ApplicationRecord
transition %i[available stopping] => :stopped
end
+ event :recover_stuck_stopping do
+ transition stopping: :available
+ end
+
state :available
state :stopping
state :stopped
diff --git a/app/models/event.rb b/app/models/event.rb
index 9e4a662aaa5..7de7ad8ccd6 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -12,7 +12,7 @@ class Event < ApplicationRecord
include IgnorableColumns
include EachBatch
- ignore_column :target_id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22'
+ ignore_column :target_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
ACTIONS = HashWithIndifferentAccess.new(
created: 1,
diff --git a/app/models/group.rb b/app/models/group.rb
index 9330ffef156..c83dd24e98e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -423,15 +423,13 @@ class Group < Namespace
owners.include?(user)
end
- def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
+ def add_members(users, access_level, current_user: nil, expires_at: nil)
Members::Groups::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
current_user: current_user,
- expires_at: expires_at,
- tasks_to_be_done: tasks_to_be_done,
- tasks_project_id: tasks_project_id
+ expires_at: expires_at
)
end
@@ -512,9 +510,15 @@ 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)
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
+ owners = []
+
+ members_from_hiearchy.all_owners.non_invite.each_batch do |relation|
+ owners += relation.preload(:user).load.reject do |member|
+ member.user.project_bot?
+ end
+ end
+
+ owners
end
def ldap_synced?
@@ -657,12 +661,6 @@ class Group < Namespace
.non_invite
end
- def users_with_parents
- User
- .where(id: members_with_parents.select(:user_id))
- .reorder(nil)
- end
-
def users_with_descendants
User
.where(id: members_with_descendants.select(:user_id))
@@ -694,7 +692,7 @@ class Group < Namespace
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership
- max_member_access([user.id])[user.id]
+ max_member_access(user)
end
def mattermost_team_params
@@ -879,10 +877,6 @@ class Group < Namespace
].compact.min
end
- def content_editor_on_issues_feature_flag_enabled?
- feature_flag_enabled_for_self_or_ancestor?(:content_editor_on_issues)
- end
-
def work_items_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end
@@ -953,16 +947,16 @@ class Group < Namespace
end
end
- def max_member_access(user_ids)
- ::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
+ def max_member_access(user)
+ Gitlab::SafeRequestLoader.execute(
+ resource_key: max_member_access_for_resource_key(User),
+ resource_ids: [user.id],
+ default_value: Gitlab::Access::NO_ACCESS
+ ) do |_|
+ next {} unless user.active?
+
+ members_with_parents(only_active_users: false).where(user_id: user.id).group(:user_id).maximum(:access_level)
+ end.fetch(user.id)
end
def update_two_factor_requirement
diff --git a/app/models/identity.rb b/app/models/identity.rb
index a4c59694050..1a3a9a300b6 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -14,6 +14,7 @@ class Identity < MainClusterwide::ApplicationRecord
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
scope :for_user, ->(user) { where(user: user) }
+ scope :for_user_ids, ->(user_ids) { where(user_id: user_ids) }
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
diff --git a/app/models/integration.rb b/app/models/integration.rb
index d4c76f743a3..b4408301c6d 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -47,6 +47,9 @@ class Integration < ApplicationRecord
Integrations::BaseThirdPartyWiki
].freeze
+ BASE_ATTRIBUTES = %w[id instance project_id group_id created_at updated_at
+ encrypted_properties encrypted_properties_iv properties].freeze
+
SECTION_TYPE_CONFIGURATION = 'configuration'
SECTION_TYPE_CONNECTION = 'connection'
SECTION_TYPE_TRIGGER = 'trigger'
@@ -111,18 +114,18 @@ class Integration < ApplicationRecord
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
- # TODO: Will be modified in 15.0
- # Details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74501#note_744393645
- scope :third_party_wikis, -> { where(type: %w[Integrations::Confluence Integrations::Shimo]).active }
+ scope :third_party_wikis, -> { where(category: 'third_party_wiki').active }
scope :by_name, ->(name) { by_type(integration_name_to_type(name)) }
scope :external_wikis, -> { by_name(:external_wiki).active }
scope :active, -> { where(active: true) }
scope :by_type, ->(type) { where(type: type) } # INTERNAL USE ONLY: use by_name instead
- scope :by_active_flag, -> (flag) { where(active: flag) }
- scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
+ scope :by_active_flag, ->(flag) { where(active: flag) }
+ scope :inherit_from_id, ->(id) { where(inherit_from_id: id) }
scope :with_default_settings, -> { where.not(inherit_from_id: nil) }
scope :with_custom_settings, -> { where(inherit_from_id: nil) }
- scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
+ scope :for_group, ->(group) {
+ where(group_id: group, type: available_integration_types(include_project_specific: false))
+ }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
scope :push_hooks, -> { where(push_events: true, active: true) }
@@ -216,13 +219,6 @@ class Integration < ApplicationRecord
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.boolean_accessor(*args)
args.each do |arg|
- # TODO: Allow legacy usage of `.boolean_accessor`, once all integrations
- # are converted to the field DSL we can remove this and only call
- # `.boolean_accessor` through `.field`.
- #
- # See https://gitlab.com/groups/gitlab-org/-/epics/7652
- prop_accessor(arg) unless method_defined?(arg)
-
class_eval <<~RUBY, __FILE__, __LINE__ + 1
# Make the original getter available as a private method.
alias_method :#{arg}_before_type_cast, :#{arg}
@@ -239,13 +235,14 @@ class Integration < ApplicationRecord
RUBY
end
end
+ private_class_method :boolean_accessor
def self.to_param
raise NotImplementedError
end
def self.event_names
- self.supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
+ supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
end
def self.supported_events
@@ -406,7 +403,7 @@ class Integration < ApplicationRecord
from_union([active.where(instance: true), active.where(group_id: group_ids, inherit_from_id: nil)])
.order(order)
.group_by(&:type)
- .count { |type, parents| build_from_integration(parents.first, association => owner.id).save }
+ .count { |_type, parents| build_from_integration(parents.first, association => owner.id).save }
end
def self.inherited_descendants_from_self_or_ancestors_from(integration)
@@ -415,9 +412,10 @@ class Integration < ApplicationRecord
.or(where(type: integration.type, instance: true)).select(:id)
from_union([
- where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
- where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
- ])
+ where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
+ where(type: integration.type, inherit_from_id: inherit_from_ids,
+ project: Project.in_namespace(integration.group.self_and_descendants))
+ ])
end
def activated?
@@ -490,10 +488,9 @@ class Integration < ApplicationRecord
def to_database_hash
column = self.class.attribute_aliases.fetch('type', 'type')
- as_json(
- except: %w[id instance project_id group_id created_at updated_at]
- ).merge(column => type)
- .merge(reencrypt_properties)
+ attributes_for_database.except(*BASE_ATTRIBUTES)
+ .merge(column => type)
+ .merge(reencrypt_properties)
end
def reencrypt_properties
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 859522670ef..77555996cd9 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-require 'asana'
-
module Integrations
class Asana < Integration
+ TASK_URL_TEMPLATE = 'https://app.asana.com/api/1.0/tasks/%{task_gid}'
+ STORY_URL_TEMPLATE = 'https://app.asana.com/api/1.0/tasks/%{task_gid}/stories'
+
validates :api_key, presence: true, if: :activated?
field :api_key,
@@ -40,12 +41,6 @@ module Integrations
%w[push]
end
- def client
- @_client ||= ::Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
- end
-
def execute(data)
return unless supported_events.include?(data[:object_kind])
@@ -78,11 +73,12 @@ module Integrations
taskid = tuple[2] || tuple[1]
begin
- task = ::Asana::Resources::Task.find_by_id(client, taskid)
- task.add_comment(text: "#{push_msg} #{message}")
+ story_on_task_url = format(STORY_URL_TEMPLATE, task_gid: taskid)
+ Gitlab::HTTP.post(story_on_task_url, headers: { "Authorization" => "Bearer #{api_key}" }, body: { text: "#{push_msg} #{message}" })
if tuple[0]
- task.update(completed: true)
+ task_url = format(TASK_URL_TEMPLATE, task_gid: taskid)
+ Gitlab::HTTP.put(task_url, headers: { "Authorization" => "Bearer #{api_key}" }, body: { completed: true })
end
rescue StandardError => e
log_error(e.message)
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 0b8432136dd..9f15532a0b0 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -28,14 +28,13 @@ module Integrations
non_empty_password_title: -> { s_('ProjectService|Enter new password') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') }
- validates :bamboo_url, presence: true, public_url: true, if: :activated?
- validates :build_key, presence: true, if: :activated?
- validates :username,
- presence: true,
- if: ->(service) { service.activated? && service.password }
- validates :password,
- presence: true,
- if: ->(service) { service.activated? && service.username }
+ with_options if: :activated? do
+ validates :bamboo_url, presence: true, public_url: true
+ validates :build_key, presence: true
+ end
+
+ validates :username, presence: true, if: ->(integration) { integration.activated? && integration.password }
+ validates :password, presence: true, if: ->(integration) { integration.activated? && integration.username }
attr_accessor :response
@@ -48,8 +47,16 @@ module Integrations
end
def help
- docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
- s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ docs_link = ActionController::Base.helpers.link_to(
+ _('Learn more.'),
+ Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+ format(
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and ' \
+ 'a repository trigger in Bamboo. %{docs_link}').html_safe,
+ docs_link: docs_link.html_safe)
end
def self.to_param
@@ -70,12 +77,18 @@ module Integrations
get_path("updateAndBuild.action", { buildKey: build_key })
end
- def calculate_reactive_cache(sha, ref)
+ def calculate_reactive_cache(sha, _ref)
response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
+ def avatar_url
+ ActionController::Base.helpers.image_path(
+ 'illustrations/third-party-logos/integrations-logos/atlassian-bamboo.svg'
+ )
+ end
+
private
def get_build_result(response)
@@ -112,7 +125,7 @@ module Integrations
if result.blank?
'Pending'
else
- result.dig('buildState')
+ result['buildState']
end
return :error unless status.present?
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 2c929dc2cb3..b75801335bd 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -13,6 +13,8 @@ module Integrations
tag_push pipeline wiki_page deployment incident
].freeze
+ GROUP_ONLY_SUPPORTED_EVENTS = %w[group_mention group_confidential_mention].freeze
+
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
@@ -26,12 +28,12 @@ module Integrations
attribute :category, default: 'chat'
- prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
+ prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified,
+ :labels_to_be_notified_behavior, :notify_only_default_branch
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
-
- boolean_accessor :notify_only_default_branch
+ prop_accessor(*GROUP_ONLY_SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
validates :webhook,
presence: true,
@@ -44,10 +46,10 @@ module Integrations
super
if properties.empty?
- self.notify_only_broken_pipelines = true if self.respond_to?(:notify_only_broken_pipelines)
+ self.notify_only_broken_pipelines = true if respond_to?(:notify_only_broken_pipelines)
self.branches_to_be_notified = "default"
self.labels_to_be_notified_behavior = MATCH_ANY_LABEL
- elsif !self.notify_only_default_branch.nil?
+ elsif !notify_only_default_branch.nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
@@ -55,7 +57,7 @@ module Integrations
# users haven't specified one already. When users edit the integration and
# select a value for this new property, it will override everything.
- self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
+ self.branches_to_be_notified ||= notify_only_default_branch == 'true' ? "default" : "all"
end
end
@@ -237,7 +239,7 @@ module Integrations
case object_kind
when "push", "tag_push"
Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
- when "issue"
+ when "issue", "incident"
Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
@@ -249,8 +251,8 @@ module Integrations
Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
Integrations::ChatMessage::DeploymentMessage.new(data) if notify_for_ref?(data)
- when "incident"
- Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
+ when "group_mention"
+ Integrations::ChatMessage::GroupMentionMessage.new(data)
end
end
# rubocop:enable Metrics/CyclomaticComplexity
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index 65aec8b278f..09a0c9ba361 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -7,8 +7,6 @@ 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
@@ -18,7 +16,6 @@ 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
diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb
index e2c689f9435..6c7ea9aed7c 100644
--- a/app/models/integrations/chat_message/alert_message.rb
+++ b/app/models/integrations/chat_message/alert_message.rb
@@ -34,12 +34,12 @@ module Integrations
"Alert firing in #{strip_markup(project_name)}"
end
- private
-
def attachment_color
"#C95823"
end
+ private
+
def attachment_fields
[
{
diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb
index 0367459dfcb..4d3e962d885 100644
--- a/app/models/integrations/chat_message/deployment_message.rb
+++ b/app/models/integrations/chat_message/deployment_message.rb
@@ -30,7 +30,7 @@ module Integrations
[{
text: format(description_message),
- color: color
+ color: attachment_color
}]
end
@@ -38,17 +38,7 @@ module Integrations
{}
end
- private
-
- def message
- if running?
- "Starting deploy to #{strip_markup(environment)}"
- else
- "Deploy to #{strip_markup(environment)} #{humanized_status}"
- end
- end
-
- def color
+ def attachment_color
case status
when 'success'
'good'
@@ -61,6 +51,16 @@ module Integrations
end
end
+ private
+
+ def message
+ if running?
+ "Starting deploy to #{strip_markup(environment)}"
+ else
+ "Deploy to #{strip_markup(environment)} #{humanized_status}"
+ end
+ end
+
def project_link
link(project_name, project_url)
end
diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb
index dd516362491..4c144bc2f68 100644
--- a/app/models/integrations/chat_message/issue_message.rb
+++ b/app/models/integrations/chat_message/issue_message.rb
@@ -41,6 +41,10 @@ module Integrations
}
end
+ def attachment_color
+ '#C95823'
+ end
+
private
def message
@@ -56,7 +60,7 @@ module Integrations
title: issue_title,
title_link: issue_url,
text: format(SlackMarkdownSanitizer.sanitize_slack_link(description)),
- color: '#C95823'
+ color: attachment_color
}]
end
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
index f8a634be336..2abe4a6e9c7 100644
--- a/app/models/integrations/chat_message/pipeline_message.rb
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -89,6 +89,15 @@ module Integrations
}
end
+ def attachment_color
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
+ else
+ 'danger'
+ end
+ end
+
private
def actually_failed_jobs(builds)
@@ -180,15 +189,6 @@ module Integrations
end
end
- def attachment_color
- case status
- when 'success'
- detailed_status == 'passed with warnings' ? 'warning' : 'good'
- else
- 'danger'
- end
- end
-
def ref_url
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
index b17e28bb6c6..ee44fc98791 100644
--- a/app/models/integrations/chat_message/push_message.rb
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -35,6 +35,10 @@ module Integrations
}
end
+ def attachment_color
+ '#345'
+ end
+
private
def humanized_action(short: false)
@@ -111,10 +115,6 @@ module Integrations
['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
end
end
-
- def attachment_color
- '#345'
- end
end
end
end
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 815e3669d78..33b2b52fa62 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -42,8 +42,15 @@ module Integrations
s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)')
end
+ override :supported_events
+ def supported_events
+ additional = group_level? ? %w[group_mention group_confidential_mention] : []
+
+ (self.class.supported_events + additional).freeze
+ end
+
def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
+ %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page deployment]
end
def configurable_channels?
@@ -68,7 +75,7 @@ module Integrations
builder.add_embed do |embed|
embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar)
embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n")
- embed.colour = 16543014 # The hex "fc6d26" as an Integer
+ embed.colour = embed_color(message)
embed.timestamp = Time.now.utc
end
end
@@ -77,6 +84,33 @@ module Integrations
false
end
+ COLOR_OVERRIDES = {
+ 'good' => '#0d532a',
+ 'warning' => '#703800',
+ 'danger' => '#8d1300'
+ }.freeze
+
+ def embed_color(message)
+ return 'fc6d26'.hex unless message.respond_to?(:attachment_color)
+
+ color = message.attachment_color
+
+ color = COLOR_OVERRIDES[color] if COLOR_OVERRIDES.key?(color)
+
+ color = color.delete_prefix('#')
+
+ normalize_color(color).hex
+ end
+
+ # Expands the short notation to the full colorcode notation
+ # 123456 -> 123456
+ # 123 -> 112233
+ def normalize_color(color)
+ return (color[0, 1] * 2) + (color[1, 1] * 2) + (color[2, 1] * 2) if color.length == 3
+
+ color
+ end
+
def custom_data(data)
super(data).merge(markdown: true)
end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 680752c3d56..6e4753470a3 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -30,12 +30,15 @@ module Integrations
end
def help
- docs_link = ActionController::Base.helpers.link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer'
- 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 }
+ docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'),
+ Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'),
+ target: '_blank', rel: 'noopener noreferrer')
+ format(
+ 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
- end
+ def default_channel_placeholder; end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
@@ -43,14 +46,20 @@ module Integrations
private
- def notify(message, opts)
+ def notify(message, _opts)
url = webhook.dup
key = parse_thread_key(message)
url = Gitlab::Utils.add_url_parameters(url, { threadKey: key }) if key
- simple_text = parse_simple_text_message(message)
- ::HangoutsChat::Sender.new(url).simple(simple_text)
+ payload = { text: parse_simple_text_message(message) }
+
+ Gitlab::HTTP.post(
+ url,
+ body: payload.to_json,
+ headers: { 'Content-Type' => 'application/json' },
+ parse: nil
+ ).response
end
# Returns an appropriate key for threading messages in google chat
diff --git a/app/models/integrations/integration_list.rb b/app/models/integrations/integration_list.rb
new file mode 100644
index 00000000000..ab03e5c0e0a
--- /dev/null
+++ b/app/models/integrations/integration_list.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Integrations
+ class IntegrationList
+ def initialize(batch, integration_hash, association)
+ @batch = batch
+ @integration_hash = integration_hash
+ @association = association
+ end
+
+ def to_array
+ [Integration, columns, values]
+ end
+
+ private
+
+ attr_reader :batch, :integration_hash, :association
+
+ def columns
+ integration_hash.keys << "#{association}_id"
+ end
+
+ def values
+ batch.select(:id).map do |record|
+ integration_hash.values << record.id
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index d8d1f860e9a..f6e99454cb1 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -11,8 +11,12 @@ module Integrations
PROJECTS_PER_PAGE = 50
JIRA_CLOUD_HOST = '.atlassian.net'
- ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze
- ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze
+ ATLASSIAN_REFERRER_GITLAB_COM = {
+ atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ'
+ }.freeze
+ ATLASSIAN_REFERRER_SELF_MANAGED = {
+ atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9'
+ }.freeze
API_ENDPOINTS = {
find_issue: "/rest/api/2/issue/%s",
@@ -28,11 +32,13 @@ module Integrations
AUTH_TYPE_BASIC = 0
AUTH_TYPE_PAT = 1
- SNOWPLOW_EVENT_CATEGORY = self.name
+ SNOWPLOW_EVENT_CATEGORY = name
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
- validates :username, presence: true, if: ->(object) { object.activated? && !object.personal_access_token_authorization? }
+ validates :username, presence: true, if: ->(object) {
+ object.activated? && !object.personal_access_token_authorization?
+ }
validates :password, presence: true, if: :activated?
validates :jira_auth_type, presence: true, inclusion: { in: [AUTH_TYPE_BASIC, AUTH_TYPE_PAT] }, if: :activated?
validates :jira_issue_prefix, untrusted_regexp: true, length: { maximum: 255 }, if: :activated?
@@ -130,7 +136,7 @@ module Integrations
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def reference_pattern(only_long: true)
+ def reference_pattern(*)
@reference_pattern ||= jira_issue_match_regex
end
@@ -144,7 +150,7 @@ module Integrations
end
def data_fields
- jira_tracker_data || self.build_jira_tracker_data
+ jira_tracker_data || build_jira_tracker_data
end
def set_default_data
@@ -186,8 +192,13 @@ module Integrations
end
def help
- jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/index') }
- s_("JiraService|You must configure Jira before enabling this integration. %{jira_doc_link_start}Learn more.%{link_end}") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
+ jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
+ url: help_page_path('integration/jira/index'))
+ format(
+ s_("JiraService|You must configure Jira before enabling this integration. " \
+ "%{jira_doc_link_start}Learn more.%{link_end}"),
+ jira_doc_link_start: jira_doc_link_start,
+ link_end: '</a>'.html_safe)
end
def title
@@ -212,7 +223,8 @@ module Integrations
{
type: SECTION_TYPE_JIRA_TRIGGER,
title: _('Trigger'),
- description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.')
+ description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link ' \
+ 'and comment (if enabled) will be created.')
},
{
type: SECTION_TYPE_CONFIGURATION,
@@ -313,7 +325,8 @@ module Integrations
override :create_cross_reference_note
def create_cross_reference_note(external_issue, mentioned_in, author)
unless can_cross_reference?(mentioned_in)
- return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false) }
+ return format(s_("JiraService|Events for %{noteable_model_name} are disabled."),
+ noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false))
end
jira_issue = find_issue(external_issue.id)
@@ -381,6 +394,10 @@ module Integrations
jira_auth_type == AUTH_TYPE_PAT
end
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/jira.svg')
+ end
+
private
def jira_issue_match_regex
@@ -398,10 +415,9 @@ module Integrations
end
def server_info
- strong_memoize(:server_info) do
- client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil
- end
+ client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil
end
+ strong_memoize_attr :server_info
def can_cross_reference?(mentioned_in)
case mentioned_in
@@ -430,7 +446,8 @@ module Integrations
true
rescue StandardError => e
path = API_ENDPOINTS[:transition_issue] % issue.id
- log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path, client_status: '400')
+ log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path,
+ client_status: '400')
false
end
@@ -488,9 +505,9 @@ module Integrations
link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue, message)
- send_message(issue, message, link_props)
- end
+ return if comment_exists?(issue, message)
+
+ send_message(issue, message, link_props)
end
def comment_message(data)
@@ -503,21 +520,22 @@ module Integrations
project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
branch =
if entity[:branch].present?
- s_('JiraService| on branch %{branch_link}') % {
- branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
- }
+ format(s_('JiraService| on branch %{branch_link}'),
+ branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch])))
end
entity_message = entity[:description].presence if all_details?
entity_message ||= entity[:title].chomp
- s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
+ format(
+ s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of ' \
+ '%{project_link}%{branch}:{quote}%{entity_message}{quote}'),
user_link: user_link,
entity_link: entity_link,
project_link: project_link,
branch: branch,
entity_message: entity_message
- }
+ )
end
def build_jira_link(title, url)
@@ -586,13 +604,13 @@ module Integrations
end
def resource_url(resource)
- "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
+ "#{Settings.gitlab.base_url.chomp('/')}#{resource}"
end
def build_entity_url(entity_type, entity_id)
polymorphic_url(
[
- self.project,
+ project,
entity_type.to_sym
],
id: entity_id,
@@ -631,7 +649,8 @@ module Integrations
yield
rescue StandardError => e
@error = e
- log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path, client_status: e.try(:code))
+ log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path,
+ client_status: e.try(:code))
nil
end
@@ -648,7 +667,8 @@ module Integrations
results = server_info
unless results.present?
- Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL", server_info: results, url: client_url)
+ Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL",
+ server_info: results, url: client_url)
return set_deployment_type_from_url
end
@@ -681,13 +701,25 @@ module Integrations
end
def jira_issues_section_description
- jira_issues_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/issues') }
- description = s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}') % { jira_issues_link_start: jira_issues_link_start, link_end: '</a>'.html_safe }
+ jira_issues_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
+ url: help_page_path('integration/jira/issues'))
+ description = format(
+ s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of ' \
+ 'your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}'),
+ jira_issues_link_start: jira_issues_link_start,
+ link_end: '</a>'.html_safe
+ )
if project&.issues_enabled?
- gitlab_issues_link_start = '<a href="%{url}">'.html_safe % { url: edit_project_path(project, anchor: 'js-shared-permissions') }
+ gitlab_issues_link_start = format('<a href="%{url}">'.html_safe, url: edit_project_path(project,
+ anchor: 'js-shared-permissions'))
description += '<br><br>'.html_safe
- description += s_("JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used.") % { gitlab_issues_link_start: gitlab_issues_link_start, link_end: '</a>'.html_safe }
+ description += format(
+ s_("JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. " \
+ "Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used."),
+ gitlab_issues_link_start: gitlab_issues_link_start,
+ link_end: '</a>'.html_safe
+ )
end
description
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index fa22bd1a73c..01efbc3e4a4 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -37,8 +37,8 @@ module Integrations
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
# opted to set a value for the new property based on the old one, if
- # users hasn't specified one already. When users edit the service and
- # selects a value for this new property, it will override everything.
+ # users haven't specified one already. When users edit the integration and
+ # select a value for this new property, it will override everything.
self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
end
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index f42a872c49e..b3cbc988dd6 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -65,6 +65,10 @@ module Integrations
end
end
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pivotal-tracker.svg')
+ end
+
private
def allowed_branch?(ref)
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 8474a5b7adf..ff8d07a1b4c 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -185,7 +185,7 @@ module Integrations
# Remove in next required stop after %16.4
# https://gitlab.com/gitlab-org/gitlab/-/issues/338838
def sync_http_integration!
- return unless manual_configuration_changed?
+ return unless manual_configuration_changed? && !manual_configuration_was.nil?
project.alert_management_http_integrations
.for_endpoint_identifier('legacy-prometheus')
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index e97c7e5e738..2feae29f627 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -125,5 +125,9 @@ module Integrations
Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
end
+
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg')
+ end
end
end
diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb
index 7c196720386..71fe6f8d6ef 100644
--- a/app/models/integrations/telegram.rb
+++ b/app/models/integrations/telegram.rb
@@ -26,6 +26,12 @@ module Integrations
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 }
+
with_options if: :activated? do
validates :token, :room, presence: true
end
@@ -60,6 +66,10 @@ module Integrations
super - ['deployment']
end
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/telegram.svg')
+ end
+
private
def set_webhook
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 58383a6a329..b207785021d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -543,7 +543,9 @@ class Issue < ApplicationRecord
end
end
- def related_issues(current_user, preload: nil)
+ def related_issues(current_user = nil, authorize: true, preload: nil)
+ return [] if new_record?
+
related_issues =
linked_issues_select
.joins("INNER JOIN issue_links ON
@@ -554,6 +556,7 @@ class Issue < ApplicationRecord
.reorder('issue_link_id')
related_issues = yield related_issues if block_given?
+ return related_issues unless authorize
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
@@ -561,6 +564,10 @@ class Issue < ApplicationRecord
filters: { read_cross_project: cross_project_filter })
end
+ def linked_items_count
+ related_issues(authorize: false).size
+ end
+
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
@@ -688,20 +695,14 @@ class Issue < ApplicationRecord
# for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
# Make sure to sync this method with issue_policy.rb
def readable_by?(user)
- if !project.issues_enabled?
- false
- elsif user.can_read_all_resources?
- true
- elsif project.personal? && project.team.owner?(user)
+ if user.can_read_all_resources?
true
- elsif confidential? && !assignee_or_author?(user)
- project.member?(user, Gitlab::Access::REPORTER)
elsif hidden?
false
- elsif project.public? || (project.internal? && !user.external?)
- project.feature_available?(:issues, user)
+ elsif project
+ project_level_readable_by?(user)
else
- project.member?(user)
+ group_level_readable_by?(user)
end
end
@@ -754,6 +755,31 @@ class Issue < ApplicationRecord
private
+ def project_level_readable_by?(user)
+ if !project.issues_enabled?
+ false
+ elsif project.personal? && project.team.owner?(user)
+ true
+ elsif confidential? && !assignee_or_author?(user)
+ project.member?(user, Gitlab::Access::REPORTER)
+ elsif project.public? || (project.internal? && !user.external?)
+ project.feature_available?(:issues, user)
+ else
+ project.member?(user)
+ end
+ end
+
+ def group_level_readable_by?(user)
+ # This should never happen as we don't support personal namespace level issues. Just additional safety.
+ return false unless namespace.is_a?(::Group)
+
+ if confidential? && !assignee_or_author?(user)
+ namespace.member?(user, Gitlab::Access::REPORTER)
+ else
+ namespace.member?(user)
+ end
+ end
+
def due_date_after_start_date
return unless start_date.present? && due_date.present?
diff --git a/app/models/issue_user_mention.rb b/app/models/issue_user_mention.rb
index ad0df0dca78..6c3bedfccca 100644
--- a/app/models/issue_user_mention.rb
+++ b/app/models/issue_user_mention.rb
@@ -5,5 +5,5 @@ class IssueUserMention < UserMention
belongs_to :note
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
end
diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb
index 046e47262dd..ec190ebf5d8 100644
--- a/app/models/lfs_download_object.rb
+++ b/app/models/lfs_download_object.rb
@@ -19,6 +19,15 @@ class LfsDownloadObject
@headers = headers || {}
end
+ def to_hash
+ {
+ oid: oid,
+ size: size,
+ link: link,
+ headers: headers
+ }.stringify_keys
+ end
+
def sanitized_uri
@sanitized_uri ||= Gitlab::UrlSanitizer.new(link)
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index 1d26c3c11e4..6af80686ec2 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -36,34 +36,24 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
enum status: { pending: 1, processed: 2 }, _prefix: :status
def self.load_batch_for_table(table, batch_size)
- if Feature.enabled?("loose_foreign_keys_batch_load_using_union")
- partition_names = Gitlab::Database::PostgresPartitionedTable.each_partition(table_name).map(&:name)
-
- unions = partition_names.map do |partition_name|
- partition_number = partition_name[/\d+/].to_i
-
- select(arel_table[Arel.star], arel_table[:partition].as('partition_number'))
- .from("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition_name} AS #{table_name}")
- .for_table(table)
- .where(partition: partition_number)
- .status_pending
- .consume_order
- .limit(batch_size)
- end
-
- select(arel_table[Arel.star])
- .from_union(unions, remove_duplicates: false, remove_order: false)
- .limit(batch_size)
- .to_a
- else
- # selecting partition as partition_number to workaround the sliding partitioning column ignore
+ partition_names = Gitlab::Database::PostgresPartitionedTable.each_partition(table_name).map(&:name)
+
+ unions = partition_names.map do |partition_name|
+ partition_number = partition_name[/\d+/].to_i
+
select(arel_table[Arel.star], arel_table[:partition].as('partition_number'))
+ .from("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition_name} AS #{table_name}")
.for_table(table)
+ .where(partition: partition_number)
.status_pending
.consume_order
.limit(batch_size)
- .to_a
end
+
+ select(arel_table[Arel.star])
+ .from_union(unions, remove_duplicates: false, remove_order: false)
+ .limit(batch_size)
+ .to_a
end
def self.mark_records_processed(records)
diff --git a/app/models/member.rb b/app/models/member.rb
index cdf40eaa8f5..77e283044ea 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -29,10 +29,8 @@ class Member < ApplicationRecord
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :member_namespace, inverse_of: :namespace_members, foreign_key: 'member_namespace_id', class_name: 'Namespace'
belongs_to :member_role
- has_one :member_task
delegate :name, :username, :email, :last_activity_on, to: :user, prefix: true
- delegate :tasks_to_be_done, to: :member_task, allow_nil: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite?
@@ -525,6 +523,7 @@ class Member < ApplicationRecord
def validate_access_level_locked_for_member_role
return unless member_role_id
+ return if member_role_changed? # it is ok to change the access level when changing member role
if access_level_changed?
errors.add(:access_level, _("cannot be changed since member is associated with a custom role"))
@@ -577,12 +576,6 @@ class Member < ApplicationRecord
def after_accept_invite
post_create_hook
-
- run_after_commit_or_now do
- if member_task
- TasksToBeDone::CreateWorker.perform_async(member_task.id, created_by_id, [user_id.to_i])
- end
- end
end
def after_decline_invite
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 52b9c3a80e3..b5a590d646e 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -62,9 +62,13 @@ class GroupMember < Member
return false unless access_level == Gitlab::Access::OWNER
return last_owner unless last_owner.nil?
- group.member_owners_excluding_project_bots.where.not(
- group: group, user_id: user_id
- ).empty?
+ owners = group.member_owners_excluding_project_bots
+
+ owners.reject! do |member|
+ member.group == group && member.user_id == user_id
+ end
+
+ owners.empty?
end
private
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
index 45cd8d8b000..707cd7bf31c 100644
--- a/app/models/members/last_group_owner_assigner.rb
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -22,7 +22,7 @@ class LastGroupOwnerAssigner
end
def owner_ids
- @owner_ids ||= owners.where(id: member_ids).ids
+ @owner_ids ||= member_ids & owners.map(&:id)
end
def member_ids
@@ -30,6 +30,6 @@ class LastGroupOwnerAssigner
end
def owners
- @owners ||= group.member_owners_excluding_project_bots.load
+ @owners ||= group.member_owners_excluding_project_bots
end
end
diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb
deleted file mode 100644
index 6cf6b1adb45..00000000000
--- a/app/models/members/member_task.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-class MemberTask < ApplicationRecord
- TASKS = {
- code: 0,
- ci: 1,
- issues: 2
- }.freeze
-
- belongs_to :member
- belongs_to :project
-
- validates :member, :project, presence: true
- validates :tasks, inclusion: { in: TASKS.values }
- validate :tasks_uniqueness
- validate :project_in_member_source
-
- scope :for_members, -> (members) { joins(:member).where(member: members) }
-
- def tasks_to_be_done
- Array(self[:tasks]).map { |task| TASKS.key(task) }
- end
-
- def tasks_to_be_done=(tasks)
- self[:tasks] = Array(tasks).map do |task|
- TASKS[task.to_sym]
- end.uniq
- end
-
- private
-
- def tasks_uniqueness
- errors.add(:tasks, 'are not unique') unless Array(tasks).length == Array(tasks).uniq.length
- end
-
- def project_in_member_source
- case member
- when GroupMember
- errors.add(:project, _('is not in the member group')) unless project.namespace == member.source
- when ProjectMember
- errors.add(:project, _('is not the member project')) unless project == member.source
- end
- end
-end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6a72ed6476e..d9726e76c4b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -337,15 +337,19 @@ class MergeRequest < ApplicationRecord
scope :by_squash_commit_sha, -> (sha) do
where(squash_commit_sha: sha)
end
- scope :by_merge_or_squash_commit_sha, -> (sha) do
- from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha)])
+ scope :by_merged_commit_sha, -> (sha) do
+ where(merged_commit_sha: sha)
+ end
+ scope :by_merged_or_merge_or_squash_commit_sha, -> (sha) do
+ from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha), by_merged_commit_sha(sha)])
end
scope :by_related_commit_sha, -> (sha) do
from_union(
[
by_commit_sha(sha),
by_squash_commit_sha(sha),
- by_merge_commit_sha(sha)
+ by_merge_commit_sha(sha),
+ by_merged_commit_sha(sha)
]
)
end
@@ -1231,19 +1235,23 @@ class MergeRequest < ApplicationRecord
}
end
- def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false)
+ def mergeable?(
+ skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false,
+ skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false)
+
return false unless mergeable_state?(
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check
+ skip_draft_check: skip_draft_check,
+ skip_approved_check: skip_approved_check,
+ skip_blocked_check: skip_blocked_check
)
check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
-
- can_be_merged? && (!should_be_rebased? || skip_rebase_check)
+ mergeable_git_state?(skip_rebase_check: skip_rebase_check)
end
- def mergeability_checks
+ def self.mergeable_state_checks
# We want to have the cheapest checks first in the list, that way we can
# fail fast before running the more expensive ones.
#
@@ -1256,17 +1264,52 @@ class MergeRequest < ApplicationRecord
]
end
- def mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false)
+ def self.mergeable_git_state_checks
+ [
+ ::MergeRequests::Mergeability::CheckConflictStatusService,
+ ::MergeRequests::Mergeability::CheckRebaseStatusService
+ ]
+ end
+
+ def self.all_mergeability_checks
+ mergeable_state_checks + mergeable_git_state_checks
+ end
+
+ def mergeable_state?(
+ skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false,
+ skip_draft_check: false, skip_blocked_check: false)
additional_checks = execute_merge_checks(
+ self.class.mergeable_state_checks,
params: {
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check
+ skip_approved_check: skip_approved_check,
+ skip_draft_check: skip_draft_check,
+ skip_blocked_check: skip_blocked_check
}
)
additional_checks.success?
end
+ def mergeable_git_state?(skip_rebase_check: false)
+ checks = execute_merge_checks(
+ self.class.mergeable_git_state_checks,
+ params: {
+ skip_rebase_check: skip_rebase_check
+ }
+ )
+
+ checks.success?
+ end
+
+ def all_mergeability_checks_results
+ execute_merge_checks(
+ self.class.all_mergeability_checks,
+ params: {},
+ execute_all: true
+ ).payload[:results]
+ end
+
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
@@ -1689,7 +1732,7 @@ class MergeRequest < ApplicationRecord
end
def has_terraform_reports?
- actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform))
+ actual_head_pipeline&.has_reports?(Ci::JobArtifact.of_report_type(:terraform))
end
def compare_accessibility_reports
@@ -1957,15 +2000,11 @@ class MergeRequest < ApplicationRecord
end
def base_pipeline
- @base_pipeline ||= project.ci_pipelines
- .order(id: :desc)
- .find_by(sha: diff_base_sha, ref: target_branch)
+ @base_pipeline ||= base_pipelines.last
end
def merge_base_pipeline
- @merge_base_pipeline ||= project.ci_pipelines
- .order(id: :desc)
- .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
+ @merge_base_pipeline ||= merge_base_pipelines.last
end
def discussions_rendered_on_frontend?
@@ -2081,9 +2120,11 @@ class MergeRequest < ApplicationRecord
false # Overridden in EE
end
- def execute_merge_checks(params: {})
+ def execute_merge_checks(checks, params: {}, execute_all: false)
# rubocop: disable CodeReuse/ServiceClass
- MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute
+ MergeRequests::Mergeability::RunChecksService
+ .new(merge_request: self, params: params)
+ .execute(checks, execute_all: execute_all)
# rubocop: enable CodeReuse/ServiceClass
end
@@ -2115,10 +2156,35 @@ class MergeRequest < ApplicationRecord
!squash && target_project.squash_always?
end
+ def current_patch_id_sha
+ return merge_request_diff.patch_id_sha if merge_request_diff.patch_id_sha.present?
+
+ base_sha = diff_refs&.base_sha
+ head_sha = diff_refs&.head_sha
+
+ return unless base_sha && head_sha
+ return if base_sha == head_sha
+
+ project.repository.get_patch_id(base_sha, head_sha)
+ end
+
private
attr_accessor :skip_fetch_ref
+ def merge_base_pipelines
+ target_branch_pipelines_for(sha: actual_head_pipeline.target_sha)
+ end
+
+ def base_pipelines
+ target_branch_pipelines_for(sha: diff_base_sha)
+ end
+
+ def target_branch_pipelines_for(sha:)
+ project.ci_pipelines
+ .where(sha: sha, ref: target_branch)
+ end
+
def set_draft_status
self.draft = draft?
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bddc03d8b21..900f4bcfeb2 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -210,6 +210,8 @@ class MergeRequestDiff < ApplicationRecord
# and save it to the database as serialized data
def save_git_content
ensure_commit_shas
+ set_patch_id_sha
+
save_commits
save_diffs
@@ -223,6 +225,16 @@ class MergeRequestDiff < ApplicationRecord
keep_around_commits unless importing?
end
+ def set_patch_id_sha
+ return unless base_commit_sha && head_commit_sha
+ return if base_commit_sha == head_commit_sha
+
+ self.patch_id_sha = project.repository&.get_patch_id(
+ base_commit_sha,
+ head_commit_sha
+ )
+ end
+
def set_as_latest_diff
# Don't set merge_head diff as latest so it won't get considered as the
# MergeRequest#merge_request_diff.
diff --git a/app/models/merge_request_user_mention.rb b/app/models/merge_request_user_mention.rb
index 3157f1ca2aa..548a91162cd 100644
--- a/app/models/merge_request_user_mention.rb
+++ b/app/models/merge_request_user_mention.rb
@@ -3,7 +3,7 @@
class MergeRequestUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :merge_request
belongs_to :note
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index eb0da368c7b..d5b9a4dc30f 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -54,6 +54,7 @@ class Milestone < ApplicationRecord
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
+ scope :preload_for_indexing, -> { includes(project: [:project_feature]) }
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
validates :group, presence: true, unless: :project
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index fb15b9fea72..27f03ed5857 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -19,6 +19,11 @@ module Ml
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :with_version_count, -> {
+ left_outer_joins(:versions)
+ .select("ml_models.*, count(ml_model_versions.id) as version_count")
+ .group(:id)
+ }
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?
@@ -32,5 +37,9 @@ module Ml
create_with(default_experiment: experiment)
.find_or_create_by(project: project, name: name)
end
+
+ def self.by_project_id_and_id(project_id, id)
+ find_by(project_id: project_id, id: id)
+ end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index ea0ea4de5b5..733b89fcaf2 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -6,7 +6,6 @@ class Namespace < ApplicationRecord
include Gitlab::VisibilityLevel
include Routable
include AfterCommitQueue
- include Storage::LegacyNamespace
include Gitlab::SQL::Pattern
include FeatureGate
include FromUnion
@@ -18,6 +17,9 @@ class Namespace < ApplicationRecord
include Ci::NamespaceSettings
include Referable
include CrossDatabaseIgnoredTables
+ include IgnorableColumns
+
+ ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16'
cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277'
@@ -97,7 +99,10 @@ class Namespace < ApplicationRecord
validates :path,
presence: true,
length: { maximum: URL_MAX_LENGTH }
- validate :container_registry_namespace_path_validation
+
+ validates :path,
+ format: { with: Gitlab::Regex.oci_repository_path_regex, message: Gitlab::Regex.oci_repository_path_regex_message },
+ if: :path_changed?
validates :path, namespace_path: true, if: ->(n) { !n.project_namespace? }
# Project path validator is used for project namespaces for now to assure
@@ -147,7 +152,6 @@ class Namespace < ApplicationRecord
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? }
- after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
after_save :reload_namespace_details
@@ -155,8 +159,6 @@ class Namespace < ApplicationRecord
after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear
- # Legacy Storage specific hooks
-
after_commit :expire_child_caches, on: :update, if: -> {
Feature.enabled?(:cached_route_lookups, self, type: :ops) &&
saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
@@ -289,13 +291,6 @@ class Namespace < ApplicationRecord
"#{self.class.reference_prefix}#{full_path}"
end
- def container_registry_namespace_path_validation
- return if Feature.disabled?(:restrict_special_characters_in_namespace_path, self)
- return if !path_changed? || path.match?(Gitlab::Regex.oci_repository_path_regex)
-
- errors.add(:path, Gitlab::Regex.oci_repository_path_regex_message)
- end
-
def package_settings
package_setting_relation || build_package_setting_relation
end
@@ -313,7 +308,7 @@ class Namespace < ApplicationRecord
end
def human_name
- owner_name
+ owner_name || path
end
def any_project_has_container_registry_tags?
diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb
index a65027733e9..f5e850830bc 100644
--- a/app/models/namespace/detail.rb
+++ b/app/models/namespace/detail.rb
@@ -1,13 +1,6 @@
# frozen_string_literal: true
class Namespace::Detail < ApplicationRecord
- include IgnorableColumns
-
- ignore_column :dashboard_notification_at, remove_with: '16.5', remove_after: '2023-08-22'
- ignore_column :dashboard_enforcement_at, remove_with: '16.5', remove_after: '2023-08-22'
- ignore_column :next_over_limit_check_at, remove_with: '16.5', remove_after: '2023-08-22'
- ignore_column :free_user_cap_over_limit_notified_at, remove_with: '16.5', remove_after: '2023-08-22'
-
belongs_to :namespace, inverse_of: :namespace_details
validates :namespace, presence: true
validates :description, length: { maximum: 255 }
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 8d5d788c738..3befcdeaec5 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -10,7 +10,7 @@ class NamespaceSetting < ApplicationRecord
belongs_to :namespace, inverse_of: :namespace_settings
enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true
- enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2 }, _suffix: true
+ enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2, ssh_certificates: 3 }, _suffix: true
attribute :default_branch_protection_defaults, default: -> { {} }
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 1ca3c8e85f3..c3348c49ea1 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -61,8 +61,6 @@ module Namespaces
# INPUT: [[4909902], [4909902,51065789], [4909902,51065793], [7135830], [15599674, 1], [15599674, 1, 3], [15599674, 2]]
# RESULT: [[4909902], [7135830], [15599674, 1], [15599674, 2]]
def shortest_traversal_ids_prefixes
- raise ArgumentError, 'Feature not supported since the `:use_traversal_ids` is disabled' unless use_traversal_ids?
-
prefixes = []
# The array needs to be sorted (O(nlogn)) to ensure shortest elements are always first
@@ -91,8 +89,6 @@ module Namespaces
end
def use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids)
-
traversal_ids.present?
end
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 6e79e3ac9a1..c63639e721a 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -12,14 +12,10 @@ module Namespaces
# list of namespace IDs, it can be faster to reference the ID in
# traversal_ids than the primary key ID column.
def as_ids
- return super unless use_traversal_ids?
-
select(Arel.sql('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)]').as('id'))
end
def roots
- return super unless use_traversal_ids?
-
root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct
unscoped.where(id: root_ids)
end
@@ -37,20 +33,14 @@ module Namespaces
end
def self_and_descendants(include_self: true)
- 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?
-
self_and_descendants(include_self: include_self).as_ids
end
def self_and_hierarchy
- return super unless use_traversal_ids_for_self_and_hierarchy_scopes?
-
unscoped.from_union([all.self_and_ancestors, all.self_and_descendants(include_self: false)])
end
@@ -74,15 +64,6 @@ module Namespaces
private
- def use_traversal_ids?
- Feature.enabled?(: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?
- end
-
def self_and_ancestors_from_inner_join(include_self: true, upto: nil, hierarchy_order: nil)
base_cte = all.reselect('namespaces.traversal_ids').as_cte(:base_ancestors_cte)
diff --git a/app/models/note.rb b/app/models/note.rb
index 8fc45436dc7..eae7a40fb4e 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.3', remove_after: '2023-08-22'
+ ignore_column :id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/
@@ -105,7 +105,7 @@ class Note < ApplicationRecord
validates :note, presence: true
validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }
validates :project, presence: true, if: :for_project_noteable?
- validates :namespace, presence: true
+ validates :namespace, presence: true, unless: :for_abuse_report?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -383,7 +383,7 @@ class Note < ApplicationRecord
end
def for_project_noteable?
- !for_personal_snippet?
+ !(for_personal_snippet? || for_abuse_report?)
end
def for_design?
@@ -394,6 +394,10 @@ class Note < ApplicationRecord
for_issue? || for_merge_request?
end
+ def for_abuse_report?
+ noteable_type == AbuseReport.name
+ end
+
def skip_project_check?
!for_project_noteable?
end
@@ -830,7 +834,11 @@ class Note < ApplicationRecord
def ensure_namespace_id
return if namespace_id.present? && !noteable_changed? && !project_changed?
- self.namespace_id = if for_project_noteable?
+ self.namespace_id = if for_issue?
+ # Some issues are not project noteables (e.g. group-level work items)
+ # so we need this separate condition
+ noteable&.namespace_id
+ elsif for_project_noteable?
project&.project_namespace_id
elsif for_personal_snippet?
noteable&.author&.namespace&.id
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
index b0f6af0d853..624a722e369 100644
--- a/app/models/note_diff_file.rb
+++ b/app/models/note_diff_file.rb
@@ -4,7 +4,7 @@ class NoteDiffFile < ApplicationRecord
include DiffFile
include IgnorableColumns
- ignore_column :diff_note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :diff_note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
scope :referencing_sha, -> (oids, project_id:) do
joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids })
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
index bb65be92b90..582b51475c2 100644
--- a/app/models/packages/protection/rule.rb
+++ b/app/models/packages/protection/rule.rb
@@ -4,18 +4,43 @@ module Packages
module Protection
class Rule < ApplicationRecord
enum package_type: Packages::Package.package_types.slice(:npm)
+ enum push_protected_up_to_access_level:
+ Gitlab::Access.sym_options_with_owner.slice(:developer, :maintainer, :owner),
+ _prefix: :push_protected_up_to
belongs_to :project, inverse_of: :package_protection_rules
validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] },
length: { maximum: 255 }
validates :package_type, presence: true
- validates :push_protected_up_to_access_level, presence: true,
- inclusion: { in: [
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::MAINTAINER,
- Gitlab::Access::OWNER
- ] }
+ validates :push_protected_up_to_access_level, presence: true
+
+ before_save :set_package_name_pattern_ilike_query, if: :package_name_pattern_changed?
+
+ scope :for_package_name, ->(package_name) {
+ return none if package_name.blank?
+
+ where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name)
+ }
+
+ def self.push_protected_from?(access_level:, package_name:, package_type:)
+ return true if [access_level, package_name, package_type].any?(&:blank?)
+
+ where(package_type: package_type, push_protected_up_to_access_level: access_level..)
+ .for_package_name(package_name)
+ .exists?
+ end
+
+ private
+
+ # We want to allow wildcard pattern (`*`) for the field `package_name_pattern`
+ # , e.g. `@my-scope/my-package-*`, etc.
+ # Therefore, we need to preprocess the field value before we can use the field in the ILIKE clause.
+ # E.g. convert wildcard character (`*`) to LIKE match character (`%`), escape certain characters, etc.
+ def set_package_name_pattern_ilike_query
+ self.package_name_pattern_ilike_query = self.class.sanitize_sql_like(package_name_pattern)
+ .tr('*', '%')
+ end
end
end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index e8becc833ca..8a02415aef4 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -55,6 +55,12 @@ module Pages
strong_memoize_attr :prefix
def unique_host
+ # When serving custom domain we don't present the unique host to avoid
+ # GitLab Pages auto-redirect to the unique domain instead of keeping serving
+ # from the custom domain.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/426435
+ return if domain.present?
+
url_builder.unique_host
end
strong_memoize_attr :unique_host
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index de7b2416258..f05ed2aac6e 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -6,8 +6,6 @@ class PagesDeployment < ApplicationRecord
include FileStoreMounter
include Gitlab::Utils::StrongMemoize
- MIGRATED_FILE_NAME = "_migrated.zip"
-
attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store }
belongs_to :project, optional: false
@@ -16,11 +14,11 @@ class PagesDeployment < ApplicationRecord
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
scope :older_than, ->(id) { where('id < ?', id) }
- scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) }
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
scope :active, -> { where(deleted_at: nil) }
+ scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
@@ -43,10 +41,6 @@ class PagesDeployment < ApplicationRecord
.update_all(updated_at: now, deleted_at: time || now)
end
- def migrated?
- file.filename == MIGRATED_FILE_NAME
- end
-
private
def set_size
diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb
index 245c0719439..478fc1c418a 100644
--- a/app/models/plan_limits.rb
+++ b/app/models/plan_limits.rb
@@ -7,7 +7,6 @@ class PlanLimits < ApplicationRecord
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'
attribute :limits_history, :ind_jsonb, default: -> { {} }
validates :limits_history, json_schema: { filename: 'plan_limits_history' }
diff --git a/app/models/preloaders/group_root_ancestor_preloader.rb b/app/models/preloaders/group_root_ancestor_preloader.rb
index 29c60e90964..410f48c8176 100644
--- a/app/models/preloaders/group_root_ancestor_preloader.rb
+++ b/app/models/preloaders/group_root_ancestor_preloader.rb
@@ -8,8 +8,6 @@ module Preloaders
end
def execute
- return unless ::Feature.enabled?(:use_traversal_ids)
-
# type == 'Group' condition located on subquery to prevent a filter in the query
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.id as source_id')
diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb
index ccb9d2eab98..1e96e139f94 100644
--- a/app/models/preloaders/project_root_ancestor_preloader.rb
+++ b/app/models/preloaders/project_root_ancestor_preloader.rb
@@ -10,7 +10,6 @@ module Preloaders
def execute
return unless @projects.is_a?(ActiveRecord::Relation)
- return unless ::Feature.enabled?(:use_traversal_ids)
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.id as source_id')
diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
index 16d46facb96..aaa54e0228b 100644
--- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
@@ -10,27 +10,11 @@ module Preloaders
end
def execute
- if ::Feature.enabled?(:use_traversal_ids)
- preload_with_traversal_ids
- else
- preload_direct_memberships
- end
+ preload_with_traversal_ids
end
private
- def preload_direct_memberships
- group_memberships = GroupMember.active_without_invites_and_requests
- .where(user: @user, source_id: @groups)
- .group(:source_id)
- .maximum(:access_level)
-
- @groups.each do |group|
- access_level = group_memberships[group.id]
- group.merge_value_to_request_store(User, @user.id, access_level) if access_level.present?
- end
- end
-
def preload_with_traversal_ids
# Diagrammatic representation of this step:
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111157#note_1271550140
diff --git a/app/models/project.rb b/app/models/project.rb
index 5989584ce43..fd226d23e77 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -390,6 +390,7 @@ class Project < ApplicationRecord
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
has_many :alert_management_http_integrations, class_name: 'AlertManagement::HttpIntegration', inverse_of: :project
+ has_many :container_registry_protection_rules, class_name: 'ContainerRegistry::Protection::Rule', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
# here.
@@ -559,13 +560,16 @@ class Project < ApplicationRecord
allow_blank: true
validates :name,
presence: true,
- length: { maximum: 255 },
- format: { with: Gitlab::Regex.project_name_regex,
- message: Gitlab::Regex.project_name_regex_message }
+ length: { maximum: 255 }
validates :path,
presence: true,
project_path: true,
length: { maximum: 255 }
+
+ validates :name,
+ format: { with: Gitlab::Regex.project_name_regex,
+ message: Gitlab::Regex.project_name_regex_message },
+ if: :name_changed?
validates :path,
format: { with: Gitlab::Regex.oci_repository_path_regex,
message: Gitlab::Regex.oci_repository_path_regex_message },
@@ -749,6 +753,7 @@ class Project < ApplicationRecord
scope :service_desk_enabled, -> { where(service_desk_enabled: true) }
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
+ scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_issues_or_mrs_available_for_user, -> (user) do
@@ -1449,7 +1454,7 @@ class Project < ApplicationRecord
super(import_url.sanitized_url)
credentials = import_url.credentials.to_h.transform_values { |value| CGI.unescape(value.to_s) }
- create_or_update_import_data(credentials: credentials)
+ build_or_assign_import_data(credentials: credentials)
else
super(value)
end
@@ -1470,9 +1475,7 @@ class Project < ApplicationRecord
valid?(:import_url) || errors.messages[:import_url].nil?
end
- # TODO: rename to build_or_assign_import_data as it doesn't save record
- # https://gitlab.com/gitlab-org/gitlab/-/issues/377319
- def create_or_update_import_data(data: nil, credentials: nil)
+ def build_or_assign_import_data(data: nil, credentials: nil)
return if data.nil? && credentials.nil?
project_import_data = import_data || build_import_data
@@ -2236,15 +2239,6 @@ class Project < ApplicationRecord
pages_metadatum&.deployed?
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)
- end
-
- def pages_available?
- Gitlab.config.pages.enabled
- end
-
def pages_show_onboarding?
!(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed)
end
@@ -2693,26 +2687,6 @@ class Project < ApplicationRecord
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
- def migrate_to_hashed_storage!
- return unless storage_upgradable?
-
- if git_transfer_in_progress?
- HashedStorage::ProjectMigrateWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
- else
- HashedStorage::ProjectMigrateWorker.perform_async(id)
- end
- end
-
- def rollback_to_legacy_storage!
- return if legacy_storage?
-
- if git_transfer_in_progress?
- HashedStorage::ProjectRollbackWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
- else
- HashedStorage::ProjectRollbackWorker.perform_async(id)
- end
- end
-
override :git_transfer_in_progress?
def git_transfer_in_progress?
GL_REPOSITORY_TYPES.any? do |type|
@@ -3195,10 +3169,6 @@ class Project < ApplicationRecord
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
-
def work_items_feature_flag_enabled?
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end
@@ -3346,7 +3316,7 @@ class Project < ApplicationRecord
end
def merge_requests_allowing_collaboration(source_branch = nil)
- relation = source_of_merge_requests.opened.where(allow_collaboration: true)
+ relation = source_of_merge_requests.from_fork.opened.where(allow_collaboration: true)
relation = relation.where(source_branch: source_branch) if source_branch
relation
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index c328e7d37c8..4d0c6029235 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -11,6 +11,7 @@ class ProjectAuthorization < ApplicationRecord
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: :project }, presence: true
+ scope :for_project, ->(projects) { where(project: projects) }
scope :non_guests, -> { where('access_level > ?', ::Gitlab::Access::GUEST) }
# TODO: To be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/418205
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 7e0722ab68c..96c1ad7def8 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -5,6 +5,11 @@ require 'carrierwave/orm/activerecord'
class ProjectImportData < ApplicationRecord
prepend_mod_with('ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ # Timeout strategy can only be changed via API, currently only with GitHub and BitBucket Server
+ OPTIMISTIC_TIMEOUT = "optimistic"
+ PESSIMISTIC_TIMEOUT = "pessimistic"
+ TIMEOUT_STRATEGIES = [OPTIMISTIC_TIMEOUT, PESSIMISTIC_TIMEOUT].freeze
+
belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
key: Settings.attr_encrypted_db_key_base,
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index 7a3ece4bc92..eca2e5a740e 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -12,6 +12,5 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :pages_deployment
scope :deployed, -> { where(deployed: true) }
- scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) }
scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 69d1a9f4aeb..d16fe996672 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -21,6 +21,8 @@ class ProjectSetting < ApplicationRecord
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
+ ignore_column :jitsu_key, remove_with: '16.7', remove_after: '2023-11-17'
+
attr_encrypted :cube_api_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 38521ae6090..586294f0dd0 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -43,15 +43,13 @@ class ProjectTeam
member
end
- def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
+ def add_members(users, access_level, current_user: nil, expires_at: nil)
Members::Projects::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
current_user: current_user,
- expires_at: expires_at,
- tasks_to_be_done: tasks_to_be_done,
- tasks_project_id: tasks_project_id
+ expires_at: expires_at
)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1c27a7a64cf..e565de9c4ba 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -688,7 +688,7 @@ class Repository
def head_tree(skip_flat_paths: true)
return if empty? || root_ref.nil?
- @head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths)
+ @head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths, ref_type: 'heads')
end
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil, ref_type: nil, rescue_not_found: true)
@@ -1244,7 +1244,14 @@ class Repository
def get_patch_id(old_revision, new_revision)
raw_repository.get_patch_id(old_revision, new_revision)
- rescue Gitlab::Git::CommandError
+ rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e
+ Gitlab::ErrorTracking.track_exception(
+ e,
+ project_id: project.id,
+ old_revision: old_revision,
+ new_revision: new_revision
+ )
+
nil
end
@@ -1258,6 +1265,12 @@ class Repository
Gitlab::Git::ObjectPool.init_from_gitaly(gitaly_object_pool, source_project&.repository)
end
+ def get_file_attributes(revision, paths, attributes)
+ raw_repository
+ .get_file_attributes(revision, paths, attributes)
+ .map(&:to_h)
+ end
+
private
def ancestor_cache_key(ancestor_id, descendant_id)
diff --git a/app/models/resource_events/abuse_report_event.rb b/app/models/resource_events/abuse_report_event.rb
index 59f88a63998..5881f87241d 100644
--- a/app/models/resource_events/abuse_report_event.rb
+++ b/app/models/resource_events/abuse_report_event.rb
@@ -16,7 +16,9 @@ module ResourceEvents
close_report: 4,
ban_user_and_close_report: 5,
block_user_and_close_report: 6,
- delete_user_and_close_report: 7
+ delete_user_and_close_report: 7,
+ trust_user: 8,
+ trust_user_and_close_report: 9
}
enum reason: {
@@ -28,7 +30,8 @@ module ResourceEvents
copyright: 6,
malware: 7,
other: 8,
- unconfirmed: 9
+ unconfirmed: 9,
+ trusted: 10
}
def success_message
diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb
index 8ccdd6f2261..5986ac8a43f 100644
--- a/app/models/service_desk/custom_email_credential.rb
+++ b/app/models/service_desk/custom_email_credential.rb
@@ -59,7 +59,7 @@ module ServiceDesk
allow_localhost: false,
allow_local_network: false
)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
errors.add(:smtp_address, e)
end
end
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
deleted file mode 100644
index 8a52539d128..00000000000
--- a/app/models/service_list.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class ServiceList
- def initialize(batch, service_hash, association)
- @batch = batch
- @service_hash = service_hash
- @association = association
- end
-
- def to_array
- [Integration, columns, values]
- end
-
- private
-
- attr_reader :batch, :service_hash, :association
-
- def columns
- service_hash.keys << "#{association}_id"
- end
-
- def values
- batch.select(:id).map do |record|
- service_hash.values << record.id
- end
- end
-end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index d4f8c1b3b0b..78b0c0849e3 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -79,6 +79,10 @@ class Snippet < ApplicationRecord
scope :with_statistics, -> { joins(:statistics) }
scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
+ scope :without_created_by_banned_user, -> do
+ where_not_exists(Users::BannedUser.where('snippets.author_id = banned_users.user_id'))
+ end
+
attr_mentionable :description
participant :author
@@ -365,6 +369,10 @@ class Snippet < ApplicationRecord
def multiple_files?
list_files.size > 1
end
+
+ def hidden_due_to_author_ban?
+ Feature.enabled?(:hide_snippets_of_banned_users) && author.banned?
+ end
end
Snippet.prepend_mod_with('Snippet')
diff --git a/app/models/snippet_user_mention.rb b/app/models/snippet_user_mention.rb
index 8ef2c579a5a..2b6845495bc 100644
--- a/app/models/snippet_user_mention.rb
+++ b/app/models/snippet_user_mention.rb
@@ -3,7 +3,7 @@
class SnippetUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :snippet
belongs_to :note
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index daa64f4e087..672a6d64127 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -157,7 +157,7 @@ class SshHostKey
url.port = url.inferred_port
[url, ip]
- rescue Gitlab::UrlBlocker::BlockedUrlError
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
raise ArgumentError, "Invalid URL"
end
diff --git a/app/models/storage/hashed.rb b/app/models/storage/hashed.rb
index 05e93f00912..5cef033e672 100644
--- a/app/models/storage/hashed.rb
+++ b/app/models/storage/hashed.rb
@@ -31,10 +31,6 @@ module Storage
"#{base_dir}/#{disk_hash}" if disk_hash
end
- def rename_repo(old_full_path: nil, new_full_path: nil)
- true
- end
-
private
# Generates the hash for the repository path and name on disk
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 0d12a629b8e..700314e277a 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -23,27 +23,5 @@ module Storage
def disk_path
project.full_path
end
-
- def rename_repo(old_full_path: nil, new_full_path: nil)
- old_full_path ||= project.full_path_before_last_save
- new_full_path ||= project.build_full_path
-
- if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path)
- # If repository moved successfully we need to send update instructions to users.
- # However we cannot allow rollback since we moved repository
- # So we basically we mute exceptions in next actions
- begin
- gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
- return true
- rescue StandardError => e
- Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}")
- # Returning false does not rollback after_* transaction but gives
- # us information about failing some of tasks
- return false
- end
- end
-
- false
- end
end
end
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index 58a154b8986..c4178d3c5f1 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -5,7 +5,7 @@ class Suggestion < ApplicationRecord
include Suggestible
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :note, inverse_of: :suggestions
validates :note, presence: true, unless: :importing?
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 332baea4449..06f0115ade6 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -125,7 +125,7 @@ module System
end
def future?
- starts_at > Time.current
+ starts_at.future?
end
def now_or_future?
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 4e71a13a3a1..dc93decce5e 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -4,7 +4,7 @@ class SystemNoteMetadata < ApplicationRecord
include Importable
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# These notes's action text might contain a reference that is external.
# We should always force a deep validation upon references that are found
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index eb72456b435..b6b4decc64b 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -5,7 +5,7 @@ class Timelog < ApplicationRecord
include IgnorableColumns
include Sortable
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
before_save :set_project
diff --git a/app/models/todo.rb b/app/models/todo.rb
index d159b51a0eb..e64dbf83a4c 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -6,7 +6,7 @@ class Todo < ApplicationRecord
include EachBatch
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# Time to wait for todos being removed when not visible for user anymore.
# Prevents TODOs being removed by mistake, for example, removing access from a user
@@ -25,6 +25,7 @@ class Todo < ApplicationRecord
REVIEW_REQUESTED = 9
MEMBER_ACCESS_REQUESTED = 10
REVIEW_SUBMITTED = 11 # This is an EE-only feature
+ OKR_CHECKIN_REQUESTED = 12 # This is an EE-only feature
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -37,7 +38,8 @@ class Todo < ApplicationRecord
DIRECTLY_ADDRESSED => :directly_addressed,
MERGE_TRAIN_REMOVED => :merge_train_removed,
MEMBER_ACCESS_REQUESTED => :member_access_requested,
- REVIEW_SUBMITTED => :review_submitted
+ REVIEW_SUBMITTED => :review_submitted,
+ OKR_CHECKIN_REQUESTED => :okr_checkin_requested
}.freeze
ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze
@@ -78,6 +80,7 @@ class Todo < ApplicationRecord
scope :for_type, -> (type) { where(target_type: type) }
scope :for_target, -> (id) { where(target_id: id) }
scope :for_commit, -> (id) { where(commit_id: id) }
+ scope :not_in_users, -> (user_ids) { where.not('todos.user_id' => user_ids) }
scope :with_entity_associations, -> do
preload(:target, :author, :note, group: :route, project: [:route, :group, { namespace: [:route, :owner] }, :project_setting])
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 4d62334800d..030e7d9e85f 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -13,10 +13,10 @@ class Tree
@repository = repository
@sha = sha
@path = path
- @ref_type = ExtractsRef.ref_type(ref_type)
+ @ref_type = ExtractsRef::RefExtractor.ref_type(ref_type)
git_repo = @repository.raw_repository
- ref = ExtractsRef.qualify_ref(@sha, ref_type)
+ ref = ExtractsRef::RefExtractor.qualify_ref(@sha, ref_type)
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, ref, @path, recursive, skip_flat_paths, rescue_not_found,
pagination_params)
diff --git a/app/models/upload.rb b/app/models/upload.rb
index a4fbc703146..59ce9a1f37a 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -2,6 +2,7 @@
class Upload < ApplicationRecord
include Checksummable
+ include EachBatch
# Upper limit for foreground checksum processing
CHECKSUM_THRESHOLD = 100.megabytes
diff --git a/app/models/user.rb b/app/models/user.rb
index c4e867ab571..4034677509f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -271,6 +271,7 @@ class User < MainClusterwide::ApplicationRecord
has_many :bulk_imports
has_many :custom_attributes, class_name: 'UserCustomAttribute'
+ has_one :trusted_with_spam_attribute, -> { UserCustomAttribute.trusted_with_spam }, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :project_callouts, class_name: 'Users::ProjectCallout'
@@ -306,6 +307,7 @@ class User < MainClusterwide::ApplicationRecord
has_many :awarded_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'awarded_by_user_id', inverse_of: :awarded_by_user
has_many :revoked_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'revoked_by_user_id', inverse_of: :revoked_by_user
has_many :achievements, through: :user_achievements, class_name: 'Achievements::Achievement', inverse_of: :users
+ has_many :vscode_settings, class_name: 'VsCode::Settings::VsCodeSetting', inverse_of: :user
#
# Validations
@@ -1234,10 +1236,6 @@ class User < MainClusterwide::ApplicationRecord
authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
- def preloaded_member_roles_for_projects(projects)
- # overridden in EE
- end
-
# rubocop: disable CodeReuse/ServiceClass
def require_ssh_key?
count = Users::KeysCountService.new(self).count
@@ -2226,8 +2224,8 @@ class User < MainClusterwide::ApplicationRecord
}
end
- def allow_possible_spam?
- custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists?
+ def trusted?
+ trusted_with_spam_attribute.present?
end
def namespace_commit_email_for_namespace(namespace)
@@ -2511,14 +2509,6 @@ class User < MainClusterwide::ApplicationRecord
def ci_namespace_mirrors_for_group_members(level)
search_members = group_members.where('access_level >= ?', level)
- # This reduces searched prefixes to only shortest ones
- # to avoid querying descendants since they are already covered
- # by ancestor namespaces. If the FF is not available fallback to
- # inefficient search: https://gitlab.com/gitlab-org/gitlab/-/issues/336436
- unless Feature.enabled?(:use_traversal_ids)
- return Ci::NamespaceMirror.contains_any_of_namespaces(search_members.pluck(:source_id))
- end
-
traversal_ids = Group.joins(:all_group_members)
.merge(search_members)
.shortest_traversal_ids_prefixes
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 15d50071bf6..728c1f4844a 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -10,13 +10,15 @@ class UserCustomAttribute < ApplicationRecord
scope :by_user_id, ->(user_id) { where(user_id: user_id) }
scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) }
scope :arkose_sessions, -> { by_key('arkose_session') }
+ scope :trusted_with_spam, -> { by_key(TRUSTED_BY) }
BLOCKED_BY = 'blocked_by'
UNBLOCKED_BY = 'unblocked_by'
ARKOSE_RISK_BAND = 'arkose_risk_band'
AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id'
AUTO_BANNED_BY_SPAM_LOG_ID = 'auto_banned_by_spam_log_id'
- ALLOW_POSSIBLE_SPAM = 'allow_possible_spam'
+ TRUSTED_BY = 'trusted_by'
+ AUTO_BANNED_BY = 'auto_banned_by'
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
class << self
@@ -50,6 +52,17 @@ class UserCustomAttribute < ApplicationRecord
return unless spam_log
custom_attribute = { user_id: spam_log.user_id, key: AUTO_BANNED_BY_SPAM_LOG_ID, value: spam_log.id }
+ upsert_custom_attributes([custom_attribute])
+ end
+
+ def set_trusted_by(user:, trusted_by:)
+ return unless user && trusted_by
+
+ custom_attribute = {
+ user_id: user.id,
+ key: UserCustomAttribute::TRUSTED_BY,
+ value: "#{trusted_by.username}/#{trusted_by.id}+#{Time.current}"
+ }
upsert_custom_attributes([custom_attribute])
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index def0765560e..60dd89c3ee7 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -73,9 +73,10 @@ module Users
new_navigation_callout: 71,
# 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022
namespace_over_storage_users_combined_alert: 73, # EE-only
- rich_text_editor: 74,
+ # 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751
vsd_feedback_banner: 75, # EE-only
- security_policy_protected_branch_modification: 76 # EE-only
+ security_policy_protected_branch_modification: 76, # EE-only
+ vulnerability_report_grouping: 77 # EE-only
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 086943884a5..276d549006f 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -23,18 +23,18 @@ module Users
scope :find_or_initialize_by_user, ->(user_id) { where(user_id: user_id).first_or_initialize }
scope :by_banned_user, -> { joins(:banned_user) }
- scope :similar_by_holder_name, ->(holder_name) do
- if holder_name.present?
- where('lower(holder_name) = lower(:value)', value: holder_name)
+ scope :similar_by_holder_name, ->(holder_name_hash) do
+ if holder_name_hash.present?
+ where(holder_name_hash: holder_name_hash)
else
none
end
end
scope :similar_to, ->(credit_card_validation) do
where(
- expiration_date: credit_card_validation.expiration_date,
- last_digits: credit_card_validation.last_digits,
- network: credit_card_validation.network
+ expiration_date_hash: credit_card_validation.expiration_date_hash,
+ last_digits_hash: credit_card_validation.last_digits_hash,
+ network_hash: credit_card_validation.network_hash
)
end
@@ -48,11 +48,11 @@ module Users
end
def similar_holder_names_count
- self.class.similar_by_holder_name(holder_name).count
+ self.class.similar_by_holder_name(holder_name_hash).count
end
def used_by_banned_user?
- self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name).exists?
+ self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name_hash).exists?
end
def set_last_digits_hash
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index f220cfd17c5..5b9255f93b1 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -3,30 +3,21 @@
module Users
class InProductMarketingEmail < ApplicationRecord
include BulkInsertSafe
+ include IgnorableColumns
- BUILD_IOS_APP_GUIDE = 'build_ios_app_guide'
- CAMPAIGNS = [BUILD_IOS_APP_GUIDE].freeze
+ ignore_column :campaign, remove_with: '16.7', remove_after: '2023-11-15'
belongs_to :user
validates :user, presence: true
-
- validates :track, :series, presence: true, if: -> { campaign.blank? }
- validates :campaign, presence: true, if: -> { track.blank? && series.blank? }
- validates :campaign, inclusion: { in: CAMPAIGNS }, allow_nil: true
+ validates :track, presence: true
+ validates :series, presence: true
validates :user_id, uniqueness: {
scope: [:track, :series],
message: 'track series email has already been sent'
}, if: -> { track.present? }
- validates :user_id, uniqueness: {
- scope: :campaign,
- message: 'campaign email has already been sent'
- }, if: -> { campaign.present? }
-
- validate :campaign_or_track_series
-
enum track: {
create: 0,
verify: 1,
@@ -44,20 +35,15 @@ module Users
INACTIVE_TRACK_NAMES = %w[invite_team experience].freeze
ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES)
- scope :for_user_with_track_and_series, -> (user, track, series) do
+ scope :for_user_with_track_and_series, ->(user, track, series) do
where(user: user, track: track, series: series)
end
- scope :without_track_and_series, -> (track, series) do
+ scope :without_track_and_series, ->(track, series) do
join_condition = for_user.and(for_track_and_series(track, series))
users_without_records(join_condition)
end
- scope :without_campaign, -> (campaign) do
- join_condition = for_user.and(for_campaign(campaign))
- users_without_records(join_condition)
- end
-
def self.users_table
User.arel_table
end
@@ -78,10 +64,6 @@ module Users
arel_table[:user_id].eq(users_table[:id])
end
- def self.for_campaign(campaign)
- arel_table[:campaign].eq(campaign)
- end
-
def self.for_track_and_series(track, series)
arel_table[:track].eq(ACTIVE_TRACKS[track])
.and(arel_table[:series]).eq(series)
@@ -92,13 +74,5 @@ module Users
email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
end
-
- private
-
- def campaign_or_track_series
- if campaign.present? && (track.present? || series.present?)
- errors.add(:campaign, 'should be a campaign or a track and series but not both')
- end
- end
end
end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index 52f16a7861f..e033445d76b 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -2,9 +2,13 @@
module Users
class PhoneNumberValidation < ApplicationRecord
+ include IgnorableColumns
+
self.primary_key = :user_id
self.table_name = 'user_phone_number_validations'
+ ignore_column :verification_attempts, remove_with: '16.7', remove_after: '2023-11-17'
+
belongs_to :user, foreign_key: :user_id
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id
diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb
new file mode 100644
index 00000000000..e55d958d2b4
--- /dev/null
+++ b/app/models/vs_code/settings/vs_code_setting.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class VsCodeSetting < ApplicationRecord
+ belongs_to :user, inverse_of: :vscode_settings
+
+ validates :setting_type, presence: true
+ validates :content, presence: true
+
+ scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) }
+ scope :by_user, ->(user) { where(user: user) }
+ end
+ end
+end
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index 650e8942132..0e3fe2cc8ac 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -9,6 +9,9 @@ class Vulnerability < ApplicationRecord
scope :with_projects, -> { includes(:project) }
+ validates :cvss, json_schema: { filename: "vulnerability_cvss_vectors", draft: 7 }
+ attribute :cvss, :ind_jsonb
+
def self.link_reference_pattern
nil
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index a7e2be0eae5..2eed693ca76 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -205,7 +205,7 @@ class WikiPage
update_attributes(attrs)
save do
- wiki.create_page(title, content, format, attrs[:message])
+ wiki.create_page(title, raw_content, format, attrs[:message])
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 62b837eeeb6..0761a213532 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -148,6 +148,8 @@ class WorkItem < Issue
end
def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil)
+ return [] if new_record?
+
linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id')
return linked_work_items unless authorize
@@ -159,6 +161,10 @@ class WorkItem < Issue
)
end
+ def linked_items_count
+ linked_work_items(authorize: false).size
+ end
+
private
override :parent_link_confidentiality
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index ea7755b03b4..32232c93d11 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -15,7 +15,6 @@ module WorkItems
validates :work_item, presence: true, uniqueness: true
validate :validate_hierarchy_restrictions
validate :validate_cyclic_reference
- validate :validate_same_project
validate :validate_max_children
validate :validate_confidentiality
validate :check_existing_related_link
@@ -50,14 +49,6 @@ module WorkItems
private
- def validate_same_project
- return if work_item.nil? || work_item_parent.nil?
-
- if work_item.resource_parent != work_item_parent.resource_parent
- errors.add :work_item_parent, _('parent must be in the same project as child.')
- end
- end
-
def validate_max_children
return unless work_item_parent
@@ -88,6 +79,14 @@ module WorkItems
end
validate_depth(restriction.maximum_depth)
+ validate_cross_hierarchy(restriction.cross_hierarchy_enabled)
+ end
+
+ def validate_cross_hierarchy(cross_hierarchy_enabled)
+ return if cross_hierarchy_enabled
+ return if work_item.resource_parent == work_item_parent.resource_parent
+
+ errors.add :work_item_parent, _('parent must be in the same project or group as child.')
end
def validate_depth(depth)
diff --git a/app/models/work_items/related_link_restriction.rb b/app/models/work_items/related_link_restriction.rb
new file mode 100644
index 00000000000..d4a66c95ffb
--- /dev/null
+++ b/app/models/work_items/related_link_restriction.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class RelatedLinkRestriction < ApplicationRecord
+ self.table_name = 'work_item_related_link_restrictions'
+
+ belongs_to :source_type, class_name: 'WorkItems::Type'
+ belongs_to :target_type, class_name: 'WorkItems::Type'
+
+ validates :source_type, presence: true
+ validates :target_type, presence: true
+ validates :target_type, uniqueness: { scope: [:source_type_id, :link_type] }
+
+ enum link_type: Enums::IssuableLink.link_types
+ end
+end
diff --git a/app/models/work_items/related_work_item_link.rb b/app/models/work_items/related_work_item_link.rb
index a911ef5f05d..fb0069541fb 100644
--- a/app/models/work_items/related_work_item_link.rb
+++ b/app/models/work_items/related_work_item_link.rb
@@ -11,7 +11,7 @@ module WorkItems
belongs_to :source, class_name: 'WorkItem'
belongs_to :target, class_name: 'WorkItem'
- validate :validate_max_number_of_links, on: :create
+ validate :validate_related_link_restrictions
class << self
extend ::Gitlab::Utils::Override
@@ -28,14 +28,39 @@ module WorkItems
end
end
- def validate_max_number_of_links
- if source && source.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT
- errors.add :source, s_('WorkItems|This work item would exceed the maximum number of linked items.')
- end
+ private
+
+ def validate_related_link_restrictions
+ return unless source && target
+
+ source_type = source.work_item_type
+ target_type = target.work_item_type
- return unless target && target.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT
+ return if link_restriction_exists?(source_type.id, target_type.id)
- errors.add :target, s_('WorkItems|This work item would exceed the maximum number of linked items.')
+ errors.add :source, format(
+ s_('%{source_type} cannot be related to %{type_type}'),
+ source_type: source_type.name.downcase.pluralize,
+ type_type: target_type.name.downcase.pluralize
+ )
+ end
+
+ def link_restriction_exists?(source_type_id, target_type_id)
+ source_restriction = find_restriction(source_type_id, target_type_id)
+ return true if source_restriction.present?
+ return false if source_type_id == target_type_id
+
+ find_restriction(target_type_id, source_type_id).present?
+ end
+
+ def find_restriction(source_type_id, target_type_id)
+ ::WorkItems::RelatedLinkRestriction.find_by_source_type_id_and_target_type_id_and_link_type(
+ source_type_id,
+ target_type_id,
+ link_type
+ )
end
end
end
+
+WorkItems::RelatedWorkItemLink.prepend_mod
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index b7ceeecbc7f..4ccef4c93d3 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -73,6 +73,7 @@ module WorkItems
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
+ Gitlab::DatabaseImporters::WorkItems::RelatedLinksRestrictionsImporter.upsert_restrictions
find_by(namespace_id: nil, base_type: type)
end
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
index 8f54cb32f43..fc6714f1e08 100644
--- a/app/models/work_items/widgets/hierarchy.rb
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -10,6 +10,26 @@ module WorkItems
def children
work_item.work_item_children_by_relative_position
end
+
+ def ancestors
+ work_item.ancestors
+ end
+
+ def self.quick_action_commands
+ [:set_parent, :add_child]
+ end
+
+ def self.quick_action_params
+ [:set_parent, :add_child]
+ end
+
+ def self.process_quick_action_param(param_name, value)
+ return super unless param_name.in?(quick_action_params) && value.present?
+
+ return { parent: value } if param_name == :set_parent
+
+ return { children: value } if param_name == :add_child
+ end
end
end
end