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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 17:22:11 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 17:22:11 +0300
commit0c872e02b2c822e3397515ec324051ff540f0cd5 (patch)
treece2fb6ce7030e4dad0f4118d21ab6453e5938cdd /app/models
parentf7e05a6853b12f02911494c4b3fe53d9540d74fc (diff)
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/abuse_report.rb2
-rw-r--r--app/models/achievements/achievement.rb18
-rw-r--r--app/models/alert_management/alert.rb20
-rw-r--r--app/models/alert_management/http_integration.rb2
-rw-r--r--app/models/analytics/cycle_analytics/aggregation.rb11
-rw-r--r--app/models/analytics/usage_trends/measurement.rb6
-rw-r--r--app/models/appearance.rb2
-rw-r--r--app/models/application_setting.rb25
-rw-r--r--app/models/application_setting_implementation.rb6
-rw-r--r--app/models/audit_event.rb10
-rw-r--r--app/models/award_emoji.rb6
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/blob_viewer/metrics_dashboard_yml.rb15
-rw-r--r--app/models/board_group_recent_visit.rb2
-rw-r--r--app/models/board_project_recent_visit.rb2
-rw-r--r--app/models/bulk_import.rb2
-rw-r--r--app/models/bulk_imports/entity.rb2
-rw-r--r--app/models/bulk_imports/export_upload.rb1
-rw-r--r--app/models/bulk_imports/tracker.rb2
-rw-r--r--app/models/ci/bridge.rb19
-rw-r--r--app/models/ci/build.rb54
-rw-r--r--app/models/ci/build_metadata.rb15
-rw-r--r--app/models/ci/build_need.rb5
-rw-r--r--app/models/ci/build_pending_state.rb4
-rw-r--r--app/models/ci/build_report_result.rb4
-rw-r--r--app/models/ci/build_runner_session.rb4
-rw-r--r--app/models/ci/build_trace_chunk.rb7
-rw-r--r--app/models/ci/build_trace_metadata.rb12
-rw-r--r--app/models/ci/freeze_period.rb59
-rw-r--r--app/models/ci/freeze_period_status.rb31
-rw-r--r--app/models/ci/job_artifact.rb9
-rw-r--r--app/models/ci/job_token/allowlist.rb42
-rw-r--r--app/models/ci/job_token/project_scope_link.rb4
-rw-r--r--app/models/ci/job_token/scope.rb59
-rw-r--r--app/models/ci/job_variable.rb3
-rw-r--r--app/models/ci/pending_build.rb3
-rw-r--r--app/models/ci/pipeline.rb13
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/processable.rb4
-rw-r--r--app/models/ci/resource_group.rb11
-rw-r--r--app/models/ci/runner.rb3
-rw-r--r--app/models/ci/runner_namespace.rb2
-rw-r--r--app/models/ci/running_build.rb11
-rw-r--r--app/models/ci/secure_file.rb11
-rw-r--r--app/models/ci/sources/pipeline.rb15
-rw-r--r--app/models/ci/unit_test_failure.rb4
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/commit.rb10
-rw-r--r--app/models/commit_range.rb2
-rw-r--r--app/models/commit_signatures/gpg_signature.rb9
-rw-r--r--app/models/commit_signatures/ssh_signature.rb9
-rw-r--r--app/models/commit_signatures/x509_commit_signature.rb9
-rw-r--r--app/models/concerns/avatarable.rb1
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/cached_commit.rb4
-rw-r--r--app/models/concerns/ci/partitionable.rb44
-rw-r--r--app/models/concerns/ci/partitionable/partitioned_filter.rb41
-rw-r--r--app/models/concerns/commit_signature.rb4
-rw-r--r--app/models/concerns/counter_attribute.rb201
-rw-r--r--app/models/concerns/has_user_type.rb6
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/milestoneable.rb23
-rw-r--r--app/models/concerns/sensitive_serializable_hash.rb2
-rw-r--r--app/models/concerns/signature_type.rb13
-rw-r--r--app/models/concerns/sortable.rb2
-rw-r--r--app/models/concerns/taskable.rb15
-rw-r--r--app/models/concerns/time_trackable.rb10
-rw-r--r--app/models/container_repository.rb19
-rw-r--r--app/models/customer_relations/organization.rb4
-rw-r--r--app/models/dependency_proxy/group_setting.rb2
-rw-r--r--app/models/deploy_token.rb1
-rw-r--r--app/models/deployment.rb9
-rw-r--r--app/models/environment.rb51
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/generic_commit_status.rb8
-rw-r--r--app/models/gpg_key.rb2
-rw-r--r--app/models/group.rb28
-rw-r--r--app/models/group_deploy_key.rb5
-rw-r--r--app/models/hooks/active_hook_filter.rb4
-rw-r--r--app/models/hooks/service_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb27
-rw-r--r--app/models/import_export_upload.rb1
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/integrations/asana.rb6
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb33
-rw-r--r--app/models/integrations/base_slack_notification.rb9
-rw-r--r--app/models/integrations/base_slash_commands.rb2
-rw-r--r--app/models/integrations/confluence.rb2
-rw-r--r--app/models/integrations/datadog.rb10
-rw-r--r--app/models/integrations/flowdock.rb43
-rw-r--r--app/models/integrations/jira.rb9
-rw-r--r--app/models/integrations/mattermost.rb2
-rw-r--r--app/models/integrations/packagist.rb8
-rw-r--r--app/models/integrations/pushover.rb2
-rw-r--r--app/models/integrations/slack.rb7
-rw-r--r--app/models/issue.rb20
-rw-r--r--app/models/issue_collection.rb44
-rw-r--r--app/models/issue_email_participant.rb2
-rw-r--r--app/models/iteration.rb3
-rw-r--r--app/models/jira_connect_installation.rb12
-rw-r--r--app/models/key.rb12
-rw-r--r--app/models/lfs_object.rb1
-rw-r--r--app/models/member.rb17
-rw-r--r--app/models/members/group_member.rb6
-rw-r--r--app/models/members/member_role.rb14
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb20
-rw-r--r--app/models/merge_request/predictions.rb7
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_diff.rb26
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/ml/candidate.rb17
-rw-r--r--app/models/ml/candidate_metadata.rb14
-rw-r--r--app/models/ml/experiment.rb1
-rw-r--r--app/models/ml/experiment_metadata.rb14
-rw-r--r--app/models/namespace.rb38
-rw-r--r--app/models/namespace_setting.rb10
-rw-r--r--app/models/namespace_statistics.rb2
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/operations/feature_flags_client.rb6
-rw-r--r--app/models/packages/package.rb1
-rw-r--r--app/models/packages/rpm/repository_file.rb10
-rw-r--r--app/models/pages/lookup_path.rb4
-rw-r--r--app/models/pages/virtual_domain.rb1
-rw-r--r--app/models/pages_domain.rb3
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb7
-rw-r--r--app/models/personal_access_token.rb3
-rw-r--r--app/models/postgresql/detached_partition.rb4
-rw-r--r--app/models/programming_language.rb18
-rw-r--r--app/models/project.rb156
-rw-r--r--app/models/project_export_job.rb29
-rw-r--r--app/models/project_statistics.rb52
-rw-r--r--app/models/projects/forks/divergence_counts.rb72
-rw-r--r--app/models/projects/import_export/relation_export_upload.rb1
-rw-r--r--app/models/prometheus_alert.rb2
-rw-r--r--app/models/protected_branch.rb6
-rw-r--r--app/models/remote_mirror.rb7
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/service_desk_setting.rb2
-rw-r--r--app/models/snippet_statistics.rb2
-rw-r--r--app/models/synthetic_note.rb1
-rw-r--r--app/models/todo.rb20
-rw-r--r--app/models/upload.rb9
-rw-r--r--app/models/user.rb67
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/models/user_preference.rb69
-rw-r--r--app/models/users/callout.rb4
-rw-r--r--app/models/users/group_callout.rb3
-rw-r--r--app/models/users/phone_number_validation.rb6
-rw-r--r--app/models/work_item.rb19
-rw-r--r--app/models/work_items/hierarchy_restriction.rb14
-rw-r--r--app/models/work_items/parent_link.rb62
-rw-r--r--app/models/work_items/type.rb88
-rw-r--r--app/models/work_items/widgets/notes.rb14
156 files changed, 1551 insertions, 787 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 7cfebf0473f..f1f22d94061 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -14,7 +14,7 @@ class AbuseReport < ApplicationRecord
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
- scope :by_user, -> (user) { where(user_id: user) }
+ scope :by_user, ->(user) { where(user_id: user) }
scope :with_users, -> { includes(:reporter, :user) }
# For CacheMarkdownField
diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb
new file mode 100644
index 00000000000..904961491b5
--- /dev/null
+++ b/app/models/achievements/achievement.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Achievements
+ class Achievement < ApplicationRecord
+ include Avatarable
+ include StripAttribute
+
+ belongs_to :namespace, inverse_of: :achievements, optional: false
+
+ strip_attributes! :name, :description
+
+ validates :name,
+ presence: true,
+ length: { maximum: 255 },
+ uniqueness: { case_sensitive: false, scope: [:namespace_id] }
+ validates :description, length: { maximum: 1024 }
+ end
+end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 9f05c87018d..a5a539eae75 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -53,7 +53,7 @@ module AlertManagement
validates :fingerprint, allow_blank: true, uniqueness: {
scope: :project,
conditions: -> { not_resolved },
- message: -> (object, data) { _('Cannot have multiple unresolved alerts') }
+ message: ->(object, data) { _('Cannot have multiple unresolved alerts') }
}, unless: :resolved?
validate :hosts_format
@@ -74,23 +74,23 @@ module AlertManagement
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :details_url, to: :present
- scope :for_iid, -> (iid) { where(iid: iid) }
- scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
- scope :for_environment, -> (environment) { where(environment: environment) }
- scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
- scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
+ scope :for_iid, ->(iid) { where(iid: iid) }
+ scope :for_fingerprint, ->(project, fingerprint) { where(project: project, fingerprint: fingerprint) }
+ scope :for_environment, ->(environment) { where(environment: environment) }
+ scope :for_assignee_username, ->(assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
+ scope :search, ->(query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :with_operations_alerts, -> { where(domain: :operations) }
- scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
- scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
- scope :order_event_count, -> (sort_order) { order(events: sort_order) }
+ scope :order_start_time, ->(sort_order) { order(started_at: sort_order) }
+ scope :order_end_time, ->(sort_order) { order(ended_at: sort_order) }
+ scope :order_event_count, ->(sort_order) { order(events: sort_order) }
# Ascending sort order sorts severity from less critical to more critical.
# Descending sort order sorts severity from more critical to less critical.
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
- scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
+ scope :order_severity, ->(sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
scope :counts_by_project_id, -> { group(:project_id).count }
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index b2686924363..906855d6dfc 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -28,7 +28,7 @@ module AlertManagement
before_validation :ensure_token
before_validation :ensure_payload_example_not_nil
- scope :for_endpoint_identifier, -> (endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) }
+ scope :for_endpoint_identifier, ->(endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) }
scope :active, -> { where(active: true) }
scope :ordered_by_id, -> { order(:id) }
diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb
index 2e58d64ae95..a888422a6b4 100644
--- a/app/models/analytics/cycle_analytics/aggregation.rb
+++ b/app/models/analytics/cycle_analytics/aggregation.rb
@@ -1,24 +1,15 @@
# frozen_string_literal: true
class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
- include IgnorableColumns
include FromUnion
belongs_to :group, optional: false
validates :incremental_runtimes_in_seconds, :incremental_processed_records, :full_runtimes_in_seconds, :full_processed_records, presence: true, length: { maximum: 10 }, allow_blank: true
- scope :priority_order, -> (column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) }
+ scope :priority_order, ->(column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) }
scope :enabled, -> { where('enabled IS TRUE') }
- # These columns were added with wrong naming convention, the columns were never used.
- ignore_column :last_full_run_processed_records, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_runtimes_in_seconds, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_issues_updated_at, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_mrs_updated_at, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_issues_id, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_merge_requests_id, remove_with: '15.1', remove_after: '2022-05-22'
-
def cursor_for(mode, model)
{
updated_at: self["last_#{mode}_#{model.table_name}_updated_at"],
diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb
index 02e239ca0ef..c1245d8dce7 100644
--- a/app/models/analytics/usage_trends/measurement.rb
+++ b/app/models/analytics/usage_trends/measurement.rb
@@ -23,9 +23,9 @@ module Analytics
validates :recorded_at, uniqueness: { scope: :identifier }
scope :order_by_latest, -> { order(recorded_at: :desc) }
- scope :with_identifier, -> (identifier) { where(identifier: identifier) }
- scope :recorded_after, -> (date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? }
- scope :recorded_before, -> (date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? }
+ scope :with_identifier, ->(identifier) { where(identifier: identifier) }
+ scope :recorded_after, ->(date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? }
+ scope :recorded_before, ->(date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? }
def self.identifier_query_mapping
{
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index bd948c2c32a..4a046b3ab20 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -3,10 +3,10 @@
class Appearance < ApplicationRecord
include CacheableAttributes
include CacheMarkdownField
- include ObjectStorage::BackgroundMove
include WithUploads
attribute :title, default: ''
+ attribute :short_title, default: ''
attribute :description, default: ''
attribute :new_project_guidelines, default: ''
attribute :profile_image_guidelines, default: ''
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index adbbddd635c..3fb1f58f3e0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -11,6 +11,7 @@ class ApplicationSetting < ApplicationRecord
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18'
+ ignore_column :send_user_confirmation_email, remove_with: '15.8', remove_after: '2022-12-18'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -20,7 +21,7 @@ class ApplicationSetting < ApplicationRecord
'Admin Area > Settings > General > Kroki'
enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
- enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }
+ enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }, _prefix: true
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
@@ -87,7 +88,7 @@ class ApplicationSetting < ApplicationRecord
validates :grafana_url,
system_hook_url: {
- blocked_message: "is blocked: %{exception_message}. " + GRAFANA_URL_ERROR_MESSAGE
+ blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}"
},
if: :grafana_url_absolute?
@@ -226,6 +227,10 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :max_terraform_state_size_bytes,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
@@ -412,12 +417,10 @@ class ApplicationSetting < ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- # rubocop:disable Cop/StaticTranslationDefinition
validates :deactivate_dormant_users_period,
presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") },
+ numericality: { only_integer: true, greater_than_or_equal_to: 90, message: N_("'%{value}' days of inactivity must be greater than or equal to 90") },
if: :deactivate_dormant_users?
- # rubocop:enable Cop/StaticTranslationDefinition
Gitlab::SSHPublicKey.supported_types.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
@@ -466,7 +469,7 @@ class ApplicationSetting < ApplicationRecord
validates :external_auth_client_key,
presence: true,
- if: -> (setting) { setting.external_auth_client_cert.present? }
+ if: ->(setting) { setting.external_auth_client_cert.present? }
validates :lets_encrypt_notification_email,
devise_email: true,
@@ -488,17 +491,17 @@ class ApplicationSetting < ApplicationRecord
validates :eks_access_key_id,
length: { in: 16..128 },
- if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
+ if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
validates :eks_secret_access_key,
presence: true,
- if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
+ if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
- if: -> (setting) { setting.external_auth_client_cert.present? }
+ if: ->(setting) { setting.external_auth_client_cert.present? }
validates :default_ci_config_path,
format: { without: %r{(\.{2}|\A/)},
@@ -687,6 +690,10 @@ class ApplicationSetting < ApplicationRecord
validates :disable_admin_oauth_scopes,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :bulk_import_enabled,
+ allow_nil: false,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 308c05d638c..229c4e68d79 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -76,6 +76,7 @@ module ApplicationSettingImplementation
eks_account_id: nil,
eks_integration_enabled: false,
eks_secret_access_key: nil,
+ email_confirmation_setting: 'off',
email_restrictions_enabled: false,
email_restrictions: nil,
external_pipeline_validation_service_timeout: nil,
@@ -113,6 +114,7 @@ module ApplicationSettingImplementation
max_attachment_size: Settings.gitlab['max_attachment_size'],
max_export_size: 0,
max_import_size: 0,
+ max_terraform_state_size_bytes: 0,
max_yaml_size_bytes: 1.megabyte,
max_yaml_depth: 100,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
@@ -146,7 +148,6 @@ module ApplicationSettingImplementation
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: default_min_key_size(:rsa),
- send_user_confirmation_email: false,
session_expire_delay: Settings.gitlab['session_expire_delay'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
@@ -243,7 +244,8 @@ module ApplicationSettingImplementation
search_rate_limit_unauthenticated: 10,
users_get_by_id_limit: 300,
users_get_by_id_limit_allowlist: [],
- can_create_group: true
+ can_create_group: true,
+ bulk_import_enabled: false
}
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 0ad17cd8869..5cc87be388f 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -28,11 +28,11 @@ class AuditEvent < ApplicationRecord
validates :entity_type, presence: true
validates :ip_address, ip_address: true
- scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
- scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
- scope :by_author_id, -> (author_id) { where(author_id: author_id) }
- scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) }
- scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) }
+ scope :by_entity_type, ->(entity_type) { where(entity_type: entity_type) }
+ scope :by_entity_id, ->(entity_id) { where(entity_id: entity_id) }
+ scope :by_author_id, ->(author_id) { where(author_id: author_id) }
+ scope :by_entity_username, ->(username) { where(entity_id: find_user_id(username)) }
+ scope :by_author_username, ->(username) { where(author_id: find_user_id(username)) }
after_initialize :initialize_details
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index e9530a80d9f..f41f0a8be84 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -23,11 +23,11 @@ class AwardEmoji < ApplicationRecord
scope :downvotes, -> { named(DOWNVOTE_NAME) }
scope :upvotes, -> { named(UPVOTE_NAME) }
- scope :named, -> (names) { where(name: names) }
- scope :awarded_by, -> (users) { where(user: users) }
+ scope :named, ->(names) { where(name: names) }
+ scope :awarded_by, ->(users) { where(user: users) }
- after_save :expire_cache
after_destroy :expire_cache
+ after_save :expire_cache
class << self
def votes_for_collection(ids, type)
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 4339d419b48..0676de10d02 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -8,6 +8,8 @@ class Badge < ApplicationRecord
# the placeholder is found.
PLACEHOLDERS = {
'project_path' => :full_path,
+ 'project_title' => :title,
+ 'project_name' => :path,
'project_id' => :id,
'default_branch' => :default_branch,
'commit_sha' => ->(project) { project.commit&.sha }
diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb
index cac6b2192d0..4b7a178566c 100644
--- a/app/models/blob_viewer/metrics_dashboard_yml.rb
+++ b/app/models/blob_viewer/metrics_dashboard_yml.rb
@@ -25,11 +25,7 @@ module BlobViewer
private
def parse_blob_data
- if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project)
- exhaustive_metrics_dashboard_validation
- else
- old_metrics_dashboard_validation
- end
+ old_metrics_dashboard_validation
end
def old_metrics_dashboard_validation
@@ -41,14 +37,5 @@ module BlobViewer
rescue ActiveModel::ValidationError => e
e.model.errors.messages.map { |messages| messages.join(': ') }
end
-
- def exhaustive_metrics_dashboard_validation
- yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
- Gitlab::Metrics::Dashboard::Validator
- .errors(yaml, dashboard_path: blob.path, project: project)
- .map(&:message)
- rescue Gitlab::Config::Loader::FormatError => e
- [e.message]
- end
end
end
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index dc273e256a8..65299d6dd12 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -12,7 +12,7 @@ class BoardGroupRecentVisit < ApplicationRecord
validates :group, presence: true
validates :board, presence: true
- scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
+ scope :by_user_parent, ->(user, group) { where(user: user, group: group) }
def self.board_parent_relation
:group
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 723afd6feab..c5122392b91 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -12,7 +12,7 @@ class BoardProjectRecentVisit < ApplicationRecord
validates :project, presence: true
validates :board, presence: true
- scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
+ scope :by_user_parent, ->(user, project) { where(user: user, project: project) }
def self.board_parent_relation
:project
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index 2200a66b3c2..2565ad5f2b8 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -17,7 +17,7 @@ class BulkImport < ApplicationRecord
enum source_type: { gitlab: 0 }
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
- scope :order_by_created_at, -> (direction) { order(created_at: direction) }
+ scope :order_by_created_at, ->(direction) { order(created_at: direction) }
state_machine :status, initial: :created do
state :created, value: 0
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index a2542e669e1..e49c4e09a50 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -53,7 +53,7 @@ class BulkImports::Entity < ApplicationRecord
scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) }
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
scope :by_bulk_import_id, ->(bulk_import_id) { where(bulk_import_id: bulk_import_id) }
- scope :order_by_created_at, -> (direction) { order(created_at: direction) }
+ scope :order_by_created_at, ->(direction) { order(created_at: direction) }
alias_attribute :destination_slug, :destination_name
diff --git a/app/models/bulk_imports/export_upload.rb b/app/models/bulk_imports/export_upload.rb
index a9cba5119af..4304032b28c 100644
--- a/app/models/bulk_imports/export_upload.rb
+++ b/app/models/bulk_imports/export_upload.rb
@@ -3,7 +3,6 @@
module BulkImports
class ExportUpload < ApplicationRecord
include WithUploads
- include ObjectStorage::BackgroundMove
self.table_name = 'bulk_import_export_uploads'
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 357f4629078..b04ef1cb7ae 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -26,7 +26,7 @@ class BulkImports::Tracker < ApplicationRecord
entity_scope = where(bulk_import_entity_id: entity_id)
next_stage_scope = entity_scope.with_status(:created).select('MIN(stage)')
- entity_scope.where(stage: next_stage_scope)
+ entity_scope.where(stage: next_stage_scope).with_status(:created)
}
def self.stage_running?(entity_id, stage)
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d6051d70503..662fb3cffa8 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -18,8 +18,11 @@ module Ci
belongs_to :project
belongs_to :trigger_request
+
+ # To be removed upon :ci_bridge_remove_sourced_pipelines feature flag removal
has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
- foreign_key: :source_job_id
+ foreign_key: :source_job_id,
+ inverse_of: :source_bridge
has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
@@ -86,8 +89,20 @@ module Ci
end
end
+ def sourced_pipelines
+ if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project)
+ raise 'Ci::Bridge does not have sourced_pipelines association'
+ end
+
+ super
+ end
+
def has_downstream_pipeline?
- sourced_pipelines.exists?
+ if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project)
+ sourced_pipeline.present?
+ else
+ sourced_pipelines.exists?
+ end
end
def downstream_pipeline_params
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f44ba124fe2..7f42b21bc87 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -7,7 +7,6 @@ module Ci
include Ci::Contextable
include TokenAuthenticatable
include AfterCommitQueue
- include ObjectStorage::BackgroundMove
include Presentable
include Importable
include Ci::HasRef
@@ -47,7 +46,7 @@ module Ci
# DELETE queries when the Ci::Build is destroyed. The next step is to remove `dependent: :destroy`.
# Details: https://gitlab.com/gitlab-org/gitlab/-/issues/24644#note_689472685
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
- has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
+ has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id, inverse_of: :job
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
has_many :pages_deployments, inverse_of: :ci_build
@@ -71,6 +70,7 @@ module Ci
delegate :harbor_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
+ delegate :enable_debug_trace!, to: :metadata
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
@@ -90,7 +90,7 @@ module Ci
scope :with_downloadable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
- .where('ci_builds.id = ci_job_artifacts.job_id')
+ .where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id")
.where(file_type: Ci::JobArtifact::DOWNLOADABLE_TYPES)
)
end
@@ -98,7 +98,7 @@ module Ci
scope :with_erasable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
- .where('ci_builds.id = ci_job_artifacts.job_id')
+ .where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id")
.where(file_type: Ci::JobArtifact.erasable_file_types)
)
end
@@ -108,11 +108,11 @@ module Ci
end
scope :with_existing_job_artifacts, ->(query) do
- where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
+ where('EXISTS (?)', ::Ci::JobArtifact.select(1).where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id").merge(query))
end
scope :without_archived_trace, -> do
- where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
+ where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id").trace)
end
scope :with_artifacts, ->(artifact_scope) do
@@ -155,7 +155,7 @@ module Ci
scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
scope :scheduled_actions, -> { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
scope :ref_protected, -> { where(protected: true) }
- scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
+ scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where("#{quoted_table_name}.id = #{Ci::BuildTraceChunk.quoted_table_name}.build_id").select(1)) }
scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
@@ -172,8 +172,6 @@ module Ci
add_authentication_token_field :token, encrypted: :required
- before_save :ensure_token, unless: :assign_token_on_scheduling?
-
after_save :stick_build_if_status_changed
after_create unless: :importing? do |build|
@@ -247,11 +245,8 @@ module Ci
!build.waiting_for_deployment_approval? # If false is returned, it stops the transition
end
- before_transition any => [:pending] do |build, transition|
- if build.assign_token_on_scheduling?
- build.ensure_token
- end
-
+ before_transition any => [:pending] do |build|
+ build.ensure_token
true
end
@@ -419,12 +414,12 @@ module Ci
end
def waiting_for_deployment_approval?
- manual? && starts_environment? && deployment&.blocked?
+ manual? && deployment_job? && deployment&.blocked?
end
def outdated_deployment?
strong_memoize(:outdated_deployment) do
- starts_environment? &&
+ deployment_job? &&
incomplete? &&
project.ci_forward_deployment_enabled? &&
deployment&.older_than_last_successful_deployment?
@@ -528,7 +523,7 @@ module Ci
environment.present?
end
- def starts_environment?
+ def deployment_job?
has_environment_keyword? && self.environment_action == 'start'
end
@@ -722,7 +717,7 @@ module Ci
end
def ensure_trace_metadata!
- Ci::BuildTraceMetadata.find_or_upsert_for!(id)
+ Ci::BuildTraceMetadata.find_or_upsert_for!(id, partition_id)
end
def artifacts_expose_as
@@ -866,6 +861,10 @@ module Ci
Gitlab::Ci::Build::Step.from_after_script(self)].compact
end
+ def runtime_hooks
+ Gitlab::Ci::Build::Hook.from_hooks(self)
+ end
+
def image
Gitlab::Ci::Build::Image.from_image(self)
end
@@ -995,7 +994,7 @@ module Ci
# Virtual deployment status depending on the environment status.
def deployment_status
- return unless starts_environment?
+ return unless deployment_job?
if success?
return successful_deployment_status
@@ -1136,8 +1135,15 @@ module Ci
end
end
- def assign_token_on_scheduling?
- ::Feature.enabled?(:ci_assign_job_token_on_scheduling, project)
+ def partition_id_token_prefix
+ partition_id.to_s(16) if Feature.enabled?(:ci_build_partition_id_token_prefix, project)
+ end
+
+ override :format_token
+ def format_token(token)
+ return token if partition_id_token_prefix.nil?
+
+ "#{partition_id_token_prefix}_#{token}"
end
protected
@@ -1208,11 +1214,11 @@ module Ci
if project.ci_cd_settings.opt_in_jwt?
id_tokens_variables
else
- legacy_jwt_variables.concat(id_tokens_variables)
+ predefined_jwt_variables.concat(id_tokens_variables)
end
end
- def legacy_jwt_variables
+ def predefined_jwt_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
jwt = Gitlab::Ci::Jwt.for_build(self)
jwt_v2 = Gitlab::Ci::JwtV2.for_build(self)
@@ -1229,7 +1235,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
id_tokens.each do |var_name, token_data|
- token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['id_token']['aud'])
+ token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['aud'])
variables.append(key: var_name, value: token, public: false, masked: true)
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 2f28509f812..9b4794abb2e 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -5,21 +5,16 @@ module Ci
# Data that should be persisted forever, should be stored with Ci::Build model.
class BuildMetadata < Ci::ApplicationRecord
BuildTimeout = Struct.new(:value, :source)
- ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_metadata_routing_table
include Ci::Partitionable
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
- self.table_name = 'ci_builds_metadata'
+ self.table_name = 'p_ci_builds_metadata'
self.primary_key = 'id'
- self.sequence_name = 'ci_builds_metadata_id_seq'
- partitionable scope: :build, through: {
- table: :p_ci_builds_metadata,
- flag: ROUTING_FEATURE_FLAG
- }
+ partitionable scope: :build
belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
@@ -63,6 +58,12 @@ module Ci
runtime_runner_features[:cancel_gracefully] == true
end
+ def enable_debug_trace!
+ self.debug_trace_enabled = true
+ save! if changes.any?
+ true
+ end
+
private
def set_build_project
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index d4cbbfac4ab..3fa17d6d286 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -2,15 +2,18 @@
module Ci
class BuildNeed < Ci::ApplicationRecord
+ include Ci::Partitionable
include BulkInsertSafe
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
+ partitionable scope: :build
+
validates :build, presence: true
validates :name, presence: true, length: { maximum: 128 }
validates :optional, inclusion: { in: [true, false] }
- scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
+ scope :scoped_build, -> { where("#{Ci::Build.quoted_table_name}.id = #{quoted_table_name}.build_id") }
scope :artifacts, -> { where(artifacts: true) }
end
end
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 53cf0697e2e..3684dac06c7 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class Ci::BuildPendingState < Ci::ApplicationRecord
+ include Ci::Partitionable
+
belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id
+ partitionable scope: :build
+
enum state: Ci::Stage.statuses
enum failure_reason: CommitStatus.failure_reasons
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index b674c1b1a0e..b2d99fab295 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -2,11 +2,15 @@
module Ci
class BuildReportResult < Ci::ApplicationRecord
+ include Ci::Partitionable
+
self.primary_key = :build_id
belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results
belongs_to :project, class_name: "Project", inverse_of: :build_report_results
+ partitionable scope: :build
+
validates :build, :project, presence: true
validates :data, json_schema: { filename: "build_report_result_data" }
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 0f37ce70964..20c0b04e228 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -4,6 +4,8 @@ module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < Ci::ApplicationRecord
+ include Ci::Partitionable
+
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
DEFAULT_PORT_NAME = 'default_port'
@@ -12,6 +14,8 @@ module Ci
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
+ partitionable scope: :build
+
validates :build, presence: true
validates :url, public_url: { schemes: %w(https) }
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7baa98b59f9..57d8b9ba368 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -2,6 +2,7 @@
module Ci
class BuildTraceChunk < Ci::ApplicationRecord
+ include Ci::Partitionable
include ::Comparable
include ::FastDestroyAll
include ::Checksummable
@@ -10,6 +11,8 @@ module Ci
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+ partitionable scope: :build
+
attribute :data_store, default: :redis_trace_chunks
after_create { metrics.increment_trace_operation(operation: :chunked) }
@@ -28,8 +31,8 @@ module Ci
redis_trace_chunks: 4
}.freeze
- STORE_TYPES = DATA_STORES.keys.to_h do |store|
- [store, "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize]
+ STORE_TYPES = DATA_STORES.keys.index_with do |store|
+ "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize
end.freeze
LIVE_STORES = %i[redis redis_trace_chunks].freeze
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index 86de90983ff..00cf1531483 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -2,6 +2,8 @@
module Ci
class BuildTraceMetadata < Ci::ApplicationRecord
+ include Ci::Partitionable
+
MAX_ATTEMPTS = 5
self.table_name = 'ci_build_trace_metadata'
self.primary_key = :build_id
@@ -9,15 +11,17 @@ module Ci
belongs_to :build, class_name: 'Ci::Build'
belongs_to :trace_artifact, class_name: 'Ci::JobArtifact'
+ partitionable scope: :build
+
validates :build, presence: true
validates :archival_attempts, presence: true
- def self.find_or_upsert_for!(build_id)
- record = find_by(build_id: build_id)
+ def self.find_or_upsert_for!(build_id, partition_id)
+ record = find_by(build_id: build_id, partition_id: partition_id)
return record if record
- upsert({ build_id: build_id }, unique_by: :build_id)
- find_by!(build_id: build_id)
+ upsert({ build_id: build_id, partition_id: partition_id }, unique_by: :build_id)
+ find_by!(build_id: build_id, partition_id: partition_id)
end
# The job is retried around 5 times during the 7 days retention period for
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index da0bbbacddd..1bf32e04a15 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -4,6 +4,10 @@ module Ci
class FreezePeriod < Ci::ApplicationRecord
include StripAttribute
include Ci::NamespacedModelName
+ include Gitlab::Utils::StrongMemoize
+
+ STATUS_ACTIVE = :active
+ STATUS_INACTIVE = :inactive
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
@@ -14,5 +18,60 @@ module Ci
validates :freeze_start, cron: true, presence: true
validates :freeze_end, cron: true, presence: true
validates :cron_timezone, cron_freeze_period_timezone: true, presence: true
+
+ def active?
+ status == STATUS_ACTIVE
+ end
+
+ def status
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:status") do
+ within_freeze_period? ? STATUS_ACTIVE : STATUS_INACTIVE
+ end
+ end
+
+ def time_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_start") do
+ freeze_start_parsed_cron.previous_time_from(time_zone_now)
+ end
+ end
+
+ def next_time_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:next_time_start") do
+ freeze_start_parsed_cron.next_time_from(time_zone_now)
+ end
+ end
+
+ def time_end_from_now
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_now") do
+ freeze_end_parsed_cron.next_time_from(time_zone_now)
+ end
+ end
+
+ def time_end_from_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_start") do
+ freeze_end_parsed_cron.next_time_from(time_start)
+ end
+ end
+
+ private
+
+ def within_freeze_period?
+ time_start <= time_zone_now && time_zone_now <= time_end_from_start
+ end
+
+ def freeze_start_parsed_cron
+ Gitlab::Ci::CronParser.new(freeze_start, cron_timezone)
+ end
+ strong_memoize_attr :freeze_start_parsed_cron
+
+ def freeze_end_parsed_cron
+ Gitlab::Ci::CronParser.new(freeze_end, cron_timezone)
+ end
+ strong_memoize_attr :freeze_end_parsed_cron
+
+ def time_zone_now
+ Time.zone.now
+ end
+ strong_memoize_attr :time_zone_now
end
end
diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb
deleted file mode 100644
index e810bb3f229..00000000000
--- a/app/models/ci/freeze_period_status.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class FreezePeriodStatus
- attr_reader :project
-
- def initialize(project:)
- @project = project
- end
-
- def execute
- project.freeze_periods.any? { |period| within_freeze_period?(period) }
- end
-
- def within_freeze_period?(period)
- start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
- end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
-
- start_freeze = start_freeze_cron.previous_time_from(time_zone_now)
- end_freeze = end_freeze_cron.next_time_from(start_freeze)
-
- start_freeze <= time_zone_now && time_zone_now <= end_freeze
- end
-
- private
-
- def time_zone_now
- @time_zone_now ||= Time.zone.now
- end
- end
-end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 922806a21c3..53c358f4eba 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -5,7 +5,6 @@ module Ci
include Ci::Partitionable
include IgnorableColumns
include AfterCommitQueue
- include ObjectStorage::BackgroundMove
include UpdateProjectStatistics
include UsageStatistics
include Sortable
@@ -52,7 +51,8 @@ module Ci
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
- requirements: 'requirements.json',
+ requirements: 'requirements.json', # Will be DEPRECATED soon: https://gitlab.com/groups/gitlab-org/-/epics/9203
+ requirements_v2: 'requirements_v2.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json',
api_fuzzing: 'gl-api-fuzzing-report.json',
cyclonedx: 'gl-sbom.cdx.json'
@@ -95,6 +95,7 @@ module Ci
load_performance: :raw,
terraform: :raw,
requirements: :raw,
+ requirements_v2: :raw,
coverage_fuzzing: :raw,
api_fuzzing: :raw
}.freeze
@@ -119,6 +120,7 @@ module Ci
sast
secret_detection
requirements
+ requirements_v2
cluster_image_scanning
cyclonedx
].freeze
@@ -209,7 +211,8 @@ module Ci
load_performance: 25, ## EE-specific
api_fuzzing: 26, ## EE-specific
cluster_image_scanning: 27, ## EE-specific
- cyclonedx: 28 ## EE-specific
+ cyclonedx: 28, ## EE-specific
+ requirements_v2: 29 ## EE-specific
}
# `file_location` indicates where actual files are stored.
diff --git a/app/models/ci/job_token/allowlist.rb b/app/models/ci/job_token/allowlist.rb
new file mode 100644
index 00000000000..9e9a0a68ebd
--- /dev/null
+++ b/app/models/ci/job_token/allowlist.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+module Ci
+ module JobToken
+ class Allowlist
+ def initialize(source_project, direction:)
+ @source_project = source_project
+ @direction = direction
+ end
+
+ def includes?(target_project)
+ source_links
+ .with_target(target_project)
+ .exists?
+ end
+
+ def projects
+ Project.from_union(target_projects, remove_duplicates: false)
+ end
+
+ private
+
+ def source_links
+ Ci::JobToken::ProjectScopeLink
+ .with_source(@source_project)
+ .where(direction: @direction)
+ end
+
+ def target_project_ids
+ source_links
+ # pluck needed to avoid ci and main db join
+ .pluck(:target_project_id)
+ end
+
+ def target_projects
+ [
+ Project.id_in(@source_project),
+ Project.id_in(target_project_ids)
+ ]
+ end
+ end
+ end
+end
diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb
index 3fdf07123e6..b784f93651a 100644
--- a/app/models/ci/job_token/project_scope_link.rb
+++ b/app/models/ci/job_token/project_scope_link.rb
@@ -12,8 +12,8 @@ module Ci
belongs_to :target_project, class_name: 'Project'
belongs_to :added_by, class_name: 'User'
- scope :from_project, ->(project) { where(source_project: project) }
- scope :to_project, ->(project) { where(target_project: project) }
+ scope :with_source, ->(project) { where(source_project: project) }
+ scope :with_target, ->(project) { where(target_project: project) }
validates :source_project, presence: true
validates :target_project, presence: true
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index 1aa49b95201..e320c0f92d1 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -1,49 +1,58 @@
# frozen_string_literal: true
-# This model represents the surface where a CI_JOB_TOKEN can be used.
-# A Scope is initialized with the project that the job token belongs to,
-# and indicates what are all the other projects that the token could access.
+# This model represents the scope of access for a CI_JOB_TOKEN.
#
-# By default a job token can only access its own project, which is the same
-# project that defines the scope.
-# By adding ScopeLinks to the scope we can allow other projects to be accessed
-# by the job token. This works as an allowlist of projects for a job token.
+# A scope is initialized with a project.
+#
+# Projects can be added to the scope by adding ScopeLinks to
+# create an allowlist of projects in either access direction (inbound, outbound).
+#
+# Currently, projects in the outbound allowlist can be accessed via the token
+# in the source project.
+#
+# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access
+# the source project.
+#
+# CI_JOB_TOKEN should be considered untrusted without these features enabled.
#
-# If a project is not included in the scope we should not allow the job user
-# to access it since operations using CI_JOB_TOKEN should be considered untrusted.
module Ci
module JobToken
class Scope
- attr_reader :source_project
+ attr_reader :current_project
- def initialize(project)
- @source_project = project
+ def initialize(current_project)
+ @current_project = current_project
end
- def includes?(target_project)
- # if the setting is disabled any project is considered to be in scope.
- return true unless source_project.ci_outbound_job_token_scope_enabled?
+ def allows?(accessed_project)
+ self_referential?(accessed_project) || outbound_allows?(accessed_project)
+ end
- target_project.id == source_project.id ||
- Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists?
+ def outbound_projects
+ outbound_allowlist.projects
end
+ # Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project
def all_projects
- Project.from_union(target_projects, remove_duplicates: false)
+ outbound_projects
end
private
- def target_project_ids
- Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
+ def outbound_allows?(accessed_project)
+ # if the setting is disabled any project is considered to be in scope.
+ return true unless @current_project.ci_outbound_job_token_scope_enabled?
+
+ outbound_allowlist.includes?(accessed_project)
+ end
+
+ def outbound_allowlist
+ Ci::JobToken::Allowlist.new(@current_project, direction: :outbound)
end
- def target_projects
- [
- Project.id_in(source_project),
- Project.id_in(target_project_ids)
- ]
+ def self_referential?(accessed_project)
+ @current_project.id == accessed_project.id
end
end
end
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index 332a78b66ae..998f0647ad5 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -2,12 +2,15 @@
module Ci
class JobVariable < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::NewHasVariable
include Ci::RawVariable
include BulkInsertSafe
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
+ partitionable scope: :job
+
alias_attribute :secret_value, :value
validates :key, uniqueness: { scope: :job_id }, unless: :dotenv_source?
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 0fa6a234a3d..2b1eb67d4f2 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -3,11 +3,14 @@
module Ci
class PendingBuild < Ci::ApplicationRecord
include EachBatch
+ include Ci::Partitionable
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :namespace, inverse_of: :pending_builds, class_name: 'Namespace'
+ partitionable scope: :build
+
validates :namespace, presence: true
scope :ref_protected, -> { where(protected: true) }
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 020f5cf9d8e..05207fb1ca0 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -350,9 +350,13 @@ module Ci
scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :for_ref, -> (ref) { where(ref: ref) }
scope :for_branch, -> (branch) { for_ref(branch).where(tag: false) }
- scope :for_id, -> (id) { where(id: id) }
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
+ scope :for_name, -> (name) do
+ name_column = Ci::PipelineMetadata.arel_table[:name]
+
+ joins(:pipeline_metadata).where(name_column.lower.eq(name.downcase))
+ end
scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) }
scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
@@ -721,7 +725,7 @@ module Ci
def freeze_period?
strong_memoize(:freeze_period) do
- Ci::FreezePeriodStatus.new(project: project).execute
+ project.freeze_periods.any?(&:active?)
end
end
@@ -1341,13 +1345,14 @@ module Ci
persistent_ref.create
end
+ # For dependent bridge jobs we reset the upstream bridge recursively
+ # to reflect that a downstream pipeline is running again
def reset_source_bridge!(current_user)
# break recursion when no source_pipeline bridge (first upstream pipeline)
return unless bridge_waiting?
return unless current_user.can?(:update_pipeline, source_bridge.pipeline)
- source_bridge.pending!
- Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass
+ Ci::EnqueueJobService.new(source_bridge, current_user: current_user).execute(&:pending!) # rubocop:disable CodeReuse/ServiceClass
end
# EE-only
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 96e5567e85e..20ff07e88ba 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -16,7 +16,7 @@ module Ci
belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
- has_many :variables, class_name: 'Ci::PipelineScheduleVariable', validate: false
+ has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
@@ -78,8 +78,6 @@ module Ci
ref.start_with? 'refs/tags/'
end
- private
-
def worker_cron_expression
Settings.cron_jobs['pipeline_schedule_worker']['cron']
end
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index 718ed14edeb..00251ea06fd 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -9,6 +9,6 @@ module Ci
alias_attribute :secret_value, :value
- validates :key, uniqueness: { scope: :pipeline_schedule_id }
+ validates :key, presence: true, uniqueness: { scope: :pipeline_schedule_id }
end
end
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index eb805ffae0a..37c82c125aa 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -104,8 +104,8 @@ module Ci
to: :pipeline
def clone(current_user:, new_job_variables_attributes: [])
- new_attributes = self.class.clone_accessors.to_h do |attribute|
- [attribute, public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
+ new_attributes = self.class.clone_accessors.index_with do |attribute|
+ public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
end
if persisted_environment.present?
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index 6d25f747a9d..b788e4f58c1 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -24,11 +24,18 @@ module Ci
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking.
def assign_resource_to(processable)
- resources.free.limit(1).update_all(build_id: processable.id) > 0
+ attrs = {
+ build_id: processable.id,
+ partition_id: processable.partition_id
+ }
+
+ resources.free.limit(1).update_all(attrs) > 0
end
def release_resource_from(processable)
- resources.retained_by(processable).update_all(build_id: nil) > 0
+ attrs = { build_id: nil, partition_id: nil }
+
+ resources.retained_by(processable).update_all(attrs) > 0
end
def upcoming_processables
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 3be627989b1..a7f3ff938c3 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -89,6 +89,9 @@ module Ci
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) }
+ scope :with_running_builds, -> do
+ where('EXISTS(?)', ::Ci::Build.running.select(1).where('ci_builds.runner_id = ci_runners.id'))
+ end
# BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
scope :deprecated_shared, -> { instance_type }
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index 82390ccc538..502ceae3675 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -15,6 +15,8 @@ module Ci
validates :runner_id, uniqueness: { scope: :namespace_id }
validate :group_runner_type
+ scope :for_runner, ->(runner_id) { where(runner_id: runner_id) }
+
def recent_runners
::Ci::Runner.belonging_to_group(namespace_id).recent
end
diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb
index ae38d54862d..43214b0c336 100644
--- a/app/models/ci/running_build.rb
+++ b/app/models/ci/running_build.rb
@@ -1,7 +1,18 @@
# frozen_string_literal: true
module Ci
+ # This model represents metadata for a running build.
+ # Despite the generic RunningBuild name, in this first iteration it applies only to shared runners
+ # (see Ci::RunningBuild.upsert_shared_runner_build!).
+ # The decision to insert all of the running builds here was deferred to avoid the pressure on the database as
+ # at this time that was not necessary.
+ # We can reconsider the decision to limit this only to shared runners when there is more evidence that inserting all
+ # of the running builds there is worth the additional pressure.
class RunningBuild < Ci::ApplicationRecord
+ include Ci::Partitionable
+
+ partitionable scope: :build
+
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :runner, class_name: 'Ci::Runner'
diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb
index df38398e5a9..1e6c48bbef5 100644
--- a/app/models/ci/secure_file.rb
+++ b/app/models/ci/secure_file.rb
@@ -17,20 +17,19 @@ module Ci
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
validates :checksum, :file_store, :name, :project_id, presence: true
validates :name, uniqueness: { scope: :project }
+
+ attribute :metadata, :ind_jsonb
validates :metadata, json_schema: { filename: "ci_secure_file_metadata" }, allow_nil: true
+ attribute :file_store, default: -> { Ci::SecureFileUploader.default_store }
+ mount_file_store_uploader Ci::SecureFileUploader
+
after_initialize :generate_key_data
before_validation :assign_checksum
scope :order_by_created_at, -> { order(created_at: :desc) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
- serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
-
- attribute :file_store, default: -> { Ci::SecureFileUploader.default_store }
-
- mount_file_store_uploader Ci::SecureFileUploader
-
def checksum_algorithm
CHECKSUM_ALGORITHM
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 2df504cd3de..855e68d1db1 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -3,6 +3,7 @@
module Ci
module Sources
class Pipeline < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::NamespacedModelName
self.table_name = "ci_sources_pipelines"
@@ -15,6 +16,11 @@ module Ci
belongs_to :source_bridge, class_name: "Ci::Bridge", foreign_key: :source_job_id
belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id
+ partitionable scope: :pipeline
+
+ before_validation :set_source_partition_id, on: :create
+ validates :source_partition_id, presence: true
+
validates :project, presence: true
validates :pipeline, presence: true
@@ -23,6 +29,15 @@ module Ci
validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
+
+ private
+
+ def set_source_partition_id
+ return if source_partition_id_changed? && source_partition_id.present?
+ return unless source_job
+
+ self.source_partition_id = source_job.partition_id
+ end
end
end
end
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index a5aa3b70e37..cfef1249164 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -2,6 +2,8 @@
module Ci
class UnitTestFailure < Ci::ApplicationRecord
+ include Ci::Partitionable
+
REPORT_WINDOW = 14.days
validates :unit_test, :build, :failed_at, presence: true
@@ -9,6 +11,8 @@ module Ci
belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+ partitionable scope: :build
+
scope :deletable, -> { where('failed_at < ?', REPORT_WINDOW.ago) }
def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current)
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index 1607d0b6d19..e2dcff13a69 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -25,5 +25,9 @@ module Clusters
active: 0,
revoked: 1
}
+
+ def to_ability_name
+ :cluster
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 54de45ebba7..5175842e5de 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -359,6 +359,10 @@ class Commit
end
def has_signature?
+ if signature_type == :SSH && !ssh_signatures_enabled?
+ return false
+ end
+
signature_type && signature_type != :NONE
end
@@ -378,6 +382,10 @@ class Commit
@signature_type ||= raw_signature_type || :NONE
end
+ def ssh_signatures_enabled?
+ Feature.enabled?(:ssh_commit_signatures, project)
+ end
+
def signature
strong_memoize(:signature) do
case signature_type
@@ -385,6 +393,8 @@ class Commit
gpg_commit.signature
when :X509
Gitlab::X509::Commit.new(self).signature
+ when :SSH
+ Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled?
else
nil
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index e2f0de52bc9..87029cb2033 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -148,7 +148,7 @@ class CommitRange
def sha_start
return unless sha_from
- exclude_start? ? sha_from + '^' : sha_from
+ exclude_start? ? "#{sha_from}^" : sha_from
end
def commit_start
diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb
index 2ae59853520..a9e8ca2dd33 100644
--- a/app/models/commit_signatures/gpg_signature.rb
+++ b/app/models/commit_signatures/gpg_signature.rb
@@ -2,6 +2,7 @@
module CommitSignatures
class GpgSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
sha_attribute :gpg_key_primary_keyid
@@ -10,6 +11,14 @@ module CommitSignatures
validates :gpg_key_primary_keyid, presence: true
+ def signed_by_user
+ gpg_key&.user
+ end
+
+ def type
+ :gpg
+ end
+
def self.with_key_and_subkeys(gpg_key)
subkey_ids = gpg_key.subkeys.pluck(:id)
diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb
index 7a8d0653fcd..1e64e2b2978 100644
--- a/app/models/commit_signatures/ssh_signature.rb
+++ b/app/models/commit_signatures/ssh_signature.rb
@@ -3,7 +3,16 @@
module CommitSignatures
class SshSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
belongs_to :key, optional: true
+
+ def type
+ :ssh
+ end
+
+ def signed_by_user
+ key&.user
+ end
end
end
diff --git a/app/models/commit_signatures/x509_commit_signature.rb b/app/models/commit_signatures/x509_commit_signature.rb
index 2cbb331dd7e..4edbc147502 100644
--- a/app/models/commit_signatures/x509_commit_signature.rb
+++ b/app/models/commit_signatures/x509_commit_signature.rb
@@ -2,15 +2,24 @@
module CommitSignatures
class X509CommitSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false
validates :x509_certificate_id, presence: true
+ def type
+ :x509
+ end
+
def x509_commit
return unless commit
Gitlab::X509::Commit.new(commit)
end
+
+ def signed_by_user
+ commit&.committer
+ end
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index b32502c3ee2..f419fa8518e 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -16,7 +16,6 @@ module Avatarable
included do
prepend ShadowMethods
- include ObjectStorage::BackgroundMove
include Gitlab::Utils::StrongMemoize
include ApplicationHelper
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index ec0cf36d875..6a855198697 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -40,7 +40,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = :common_mark
+ context[:markdown_engine] = Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE
if Feature.enabled?(:personal_snippet_reference_filters, context[:author])
context[:user] = self.parent_user
diff --git a/app/models/concerns/cached_commit.rb b/app/models/concerns/cached_commit.rb
index 183d5728743..0fb72552dd5 100644
--- a/app/models/concerns/cached_commit.rb
+++ b/app/models/concerns/cached_commit.rb
@@ -4,8 +4,8 @@ module CachedCommit
extend ActiveSupport::Concern
def to_hash
- Gitlab::Git::Commit::SERIALIZE_KEYS.each_with_object({}) do |key, hash|
- hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ Gitlab::Git::Commit::SERIALIZE_KEYS.index_with do |key|
+ public_send(key) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index 68a6714c892..d6ba0f4488f 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -25,10 +25,21 @@ module Ci
PARTITIONABLE_MODELS = %w[
CommitStatus
Ci::BuildMetadata
- Ci::Stage
+ Ci::BuildNeed
+ Ci::BuildReportResult
+ Ci::BuildRunnerSession
+ Ci::BuildTraceChunk
+ Ci::BuildTraceMetadata
+ Ci::BuildPendingState
Ci::JobArtifact
- Ci::PipelineVariable
+ Ci::JobVariable
Ci::Pipeline
+ Ci::PendingBuild
+ Ci::RunningBuild
+ Ci::PipelineVariable
+ Ci::Sources::Pipeline
+ Ci::Stage
+ Ci::UnitTestFailure
].freeze
def self.check_inclusion(klass)
@@ -57,14 +68,31 @@ module Ci
end
class_methods do
- def partitionable(scope:, through: nil)
- if through
- define_singleton_method(:routing_table_name) { through[:table] }
- define_singleton_method(:routing_table_name_flag) { through[:flag] }
+ def partitionable(scope:, through: nil, partitioned: false)
+ handle_partitionable_through(through)
+ handle_partitionable_dml(partitioned)
+ handle_partitionable_scope(scope)
+ end
- include Partitionable::Switch
- end
+ private
+
+ def handle_partitionable_through(options)
+ return unless options
+
+ define_singleton_method(:routing_table_name) { options[:table] }
+ define_singleton_method(:routing_table_name_flag) { options[:flag] }
+
+ include Partitionable::Switch
+ end
+
+ def handle_partitionable_dml(partitioned)
+ define_singleton_method(:partitioned?) { partitioned }
+ return unless partitioned
+
+ include Partitionable::PartitionedFilter
+ end
+ def handle_partitionable_scope(scope)
define_method(:partition_scope_value) do
strong_memoize(:partition_scope_value) do
next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing?
diff --git a/app/models/concerns/ci/partitionable/partitioned_filter.rb b/app/models/concerns/ci/partitionable/partitioned_filter.rb
new file mode 100644
index 00000000000..4adae3be26a
--- /dev/null
+++ b/app/models/concerns/ci/partitionable/partitioned_filter.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Ci
+ module Partitionable
+ # Used to patch the save, update, delete, destroy methods to use the
+ # partition_id attributes for their SQL queries.
+ module PartitionedFilter
+ extend ActiveSupport::Concern
+
+ if Rails::VERSION::MAJOR >= 7
+ # These methods are updated in Rails 7 to use `_primary_key_constraints_hash`
+ # by default, so this patch will no longer be required.
+ #
+ # rubocop:disable Gitlab/NoCodeCoverageComment
+ # :nocov:
+ raise "`#{__FILE__}` should be double checked" if Rails.env.test?
+
+ warn "Update `#{__FILE__}`. Patches Rails internals for partitioning"
+ # :nocov:
+ # rubocop:enable Gitlab/NoCodeCoverageComment
+ else
+ def _update_row(attribute_names, attempted_action = "update")
+ self.class._update_record(
+ attributes_with_values(attribute_names),
+ _primary_key_constraints_hash
+ )
+ end
+
+ def _delete_row
+ self.class._delete_record(_primary_key_constraints_hash)
+ end
+ end
+
+ # Introduced in Rails 7, but updated to include `partition_id` filter.
+ # https://github.com/rails/rails/blob/a4dbb153fd390ac31bb9808809e7ac4d3a2c5116/activerecord/lib/active_record/persistence.rb#L1031-L1033
+ def _primary_key_constraints_hash
+ { @primary_key => id_in_database, partition_id: partition_id } # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5bdfa9a2966..7f1fbbefd94 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -44,7 +44,7 @@ module CommitSignature
project.commit(commit_sha)
end
- def user
- commit.committer
+ def signed_by_user
+ raise NoMethodError, 'must implement `signed_by_user` method'
end
end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 03e062a9855..f1efbba67e1 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -17,14 +17,29 @@
# counter_attribute :storage_size
# end
#
+# It's possible to define a conditional counter attribute. You need to pass a proc
+# that must accept a single argument, the object instance on which this concern is
+# included.
+#
+# @example:
+#
+# class ProjectStatistics
+# include CounterAttribute
+#
+# counter_attribute :conditional_one, if: -> { |object| object.use_counter_attribute? }
+# end
+#
# To increment the counter we can use the method:
-# delayed_increment_counter(:commit_count, 3)
+# increment_counter(:commit_count, 3)
+#
+# This method would determine whether it would increment the counter using Redis,
+# or fallback to legacy increment on ActiveRecord counters.
#
# It is possible to register callbacks to be executed after increments have
# been flushed to the database. Callbacks are not executed if there are no increments
# to flush.
#
-# counter_attribute_after_flush do |statistic|
+# counter_attribute_after_commit do |statistic|
# Namespaces::ScheduleAggregationWorker.perform_async(statistic.namespace_id)
# end
#
@@ -32,99 +47,51 @@ module CounterAttribute
extend ActiveSupport::Concern
extend AfterCommitQueue
include Gitlab::ExclusiveLeaseHelpers
-
- LUA_STEAL_INCREMENT_SCRIPT = <<~EOS
- local increment_key, flushed_key = KEYS[1], KEYS[2]
- local increment_value = redis.call("get", increment_key) or 0
- local flushed_value = redis.call("incrby", flushed_key, increment_value)
- if flushed_value == 0 then
- redis.call("del", increment_key, flushed_key)
- else
- redis.call("del", increment_key)
- end
- return flushed_value
- EOS
-
- WORKER_DELAY = 10.minutes
- WORKER_LOCK_TTL = 10.minutes
+ include Gitlab::Utils::StrongMemoize
class_methods do
- def counter_attribute(attribute)
- counter_attributes << attribute
+ def counter_attribute(attribute, if: nil)
+ counter_attributes << {
+ attribute: attribute,
+ if_proc: binding.local_variable_get(:if) # can't read `if` directly
+ }
end
def counter_attributes
- @counter_attributes ||= Set.new
+ @counter_attributes ||= []
end
- def after_flush_callbacks
- @after_flush_callbacks ||= []
+ def after_commit_callbacks
+ @after_commit_callbacks ||= []
end
- # perform registered callbacks after increments have been flushed to the database
- def counter_attribute_after_flush(&callback)
- after_flush_callbacks << callback
- end
-
- def counter_attribute_enabled?(attribute)
- counter_attributes.include?(attribute)
+ # perform registered callbacks after increments have been committed to the database
+ def counter_attribute_after_commit(&callback)
+ after_commit_callbacks << callback
end
end
- # This method must only be called by FlushCounterIncrementsWorker
- # because it should run asynchronously and with exclusive lease.
- # This will
- # 1. temporarily move the pending increment for a given attribute
- # to a relative "flushed" Redis key, delete the increment key and return
- # the value. If new increments are performed at this point, the increment
- # key is recreated as part of `delayed_increment_counter`.
- # The "flushed" key is used to ensure that we can keep incrementing
- # counters in Redis while flushing existing values.
- # 2. then the value is used to update the counter in the database.
- # 3. finally the "flushed" key is deleted.
- def flush_increments_to_database!(attribute)
- lock_key = counter_lock_key(attribute)
-
- with_exclusive_lease(lock_key) do
- previous_db_value = read_attribute(attribute)
- increment_key = counter_key(attribute)
- flushed_key = counter_flushed_key(attribute)
- increment_value = steal_increments(increment_key, flushed_key)
- new_db_value = nil
-
- next if increment_value == 0
-
- transaction do
- update_counters_with_lease({ attribute => increment_value })
- redis_state { |redis| redis.del(flushed_key) }
- new_db_value = reset.read_attribute(attribute)
- end
+ def counter_attribute_enabled?(attribute)
+ counter_attribute = self.class.counter_attributes.find { |registered| registered[:attribute] == attribute }
+ return false unless counter_attribute
+ return true unless counter_attribute[:if_proc]
- execute_after_flush_callbacks
+ counter_attribute[:if_proc].call(self)
+ end
- log_flush_counter(attribute, increment_value, previous_db_value, new_db_value)
+ def counter(attribute)
+ strong_memoize_with(:counter, attribute) do
+ # This needs #to_sym because attribute could come from a Sidekiq param,
+ # which would be a string.
+ build_counter_for(attribute.to_sym)
end
end
- def delayed_increment_counter(attribute, increment)
- raise ArgumentError, "#{attribute} is not a counter attribute" unless counter_attribute_enabled?(attribute)
-
+ def increment_counter(attribute, increment)
return if increment == 0
run_after_commit_or_now do
- increment_counter(attribute, increment)
-
- FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
- end
-
- true
- end
-
- def increment_counter(attribute, increment)
- if counter_attribute_enabled?(attribute)
- new_value = redis_state do |redis|
- redis.incrby(counter_key(attribute), increment)
- end
+ new_value = counter(attribute).increment(increment)
log_increment_counter(attribute, increment, new_value)
end
@@ -137,74 +104,33 @@ module CounterAttribute
end
def reset_counter!(attribute)
- if counter_attribute_enabled?(attribute)
- detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do
- update!(attribute => 0)
- clear_counter!(attribute)
- end
-
- log_clear_counter(attribute)
+ detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do
+ counter(attribute).reset!
end
- end
- def get_counter_value(attribute)
- if counter_attribute_enabled?(attribute)
- redis_state do |redis|
- redis.get(counter_key(attribute)).to_i
- end
- end
+ log_clear_counter(attribute)
end
- def counter_key(attribute)
- "project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}"
- end
-
- def counter_flushed_key(attribute)
- counter_key(attribute) + ':flushed'
- end
-
- def counter_lock_key(attribute)
- counter_key(attribute) + ':lock'
- end
-
- def counter_attribute_enabled?(attribute)
- self.class.counter_attribute_enabled?(attribute)
+ def execute_after_commit_callbacks
+ self.class.after_commit_callbacks.each do |callback|
+ callback.call(self.reset)
+ end
end
private
- def database_lock_key
- "project:{#{project_id}}:#{self.class}:#{id}"
- end
-
- def steal_increments(increment_key, flushed_key)
- redis_state do |redis|
- redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
- end
- end
+ def build_counter_for(attribute)
+ raise ArgumentError, %(attribute "#{attribute}" does not exist) unless has_attribute?(attribute)
- def clear_counter!(attribute)
- redis_state do |redis|
- redis.del(counter_key(attribute))
- end
- end
-
- def execute_after_flush_callbacks
- self.class.after_flush_callbacks.each do |callback|
- callback.call(self)
+ if counter_attribute_enabled?(attribute)
+ Gitlab::Counters::BufferedCounter.new(self, attribute)
+ else
+ Gitlab::Counters::LegacyCounter.new(self, attribute)
end
end
- def redis_state(&block)
- Gitlab::Redis::SharedState.with(&block)
- end
-
- def with_exclusive_lease(lock_key)
- in_lock(lock_key, ttl: WORKER_LOCK_TTL) do
- yield
- end
- rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
- # a worker is already updating the counters
+ def database_lock_key
+ "project:{#{project_id}}:#{self.class}:#{id}"
end
# detect_race_on_record uses a lease to monitor access
@@ -258,19 +184,6 @@ module CounterAttribute
Gitlab::AppLogger.info(payload)
end
- def log_flush_counter(attribute, increment, previous_db_value, new_db_value)
- payload = Gitlab::ApplicationContext.current.merge(
- message: 'Flush counter attribute to database',
- attribute: attribute,
- project_id: project_id,
- increment: increment,
- previous_db_value: previous_db_value,
- new_db_value: new_db_value
- )
-
- Gitlab::AppLogger.info(payload)
- end
-
def log_clear_counter(attribute)
payload = Gitlab::ApplicationContext.current.merge(
message: 'Clear counter attribute',
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index ad070090dd5..1af655277b8 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -13,10 +13,11 @@ module HasUserType
project_bot: 6,
migration_bot: 7,
security_bot: 8,
- automation_bot: 9
+ automation_bot: 9,
+ admin_bot: 11
}.with_indifferent_access.freeze
- BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot].freeze
+ BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot admin_bot].freeze
NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
@@ -24,7 +25,6 @@ module HasUserType
scope :humans, -> { where(user_type: :human) }
scope :bots, -> { where(user_type: BOT_USER_TYPES) }
scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) }
- scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) }
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 31b2a8d7cc1..9f0cd96a8f8 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -366,7 +366,7 @@ module Issuable
select(issuable_columns)
.select(extra_select_columns)
- .from("#{table_name}")
+ .from(table_name.to_s)
.joins("JOIN LATERAL(#{highest_priority}) as highest_priorities ON TRUE")
.group(group_columns)
.reorder(highest_priority_arel_with_direction.nulls_last)
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index a95bed7ad42..e95a8a42aa6 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -9,6 +9,12 @@
module Milestoneable
extend ActiveSupport::Concern
+ class_methods do
+ def milestone_releases_subquery
+ Milestone.joins(:releases).where("#{table_name}.milestone_id = milestones.id")
+ end
+ end
+
included do
belongs_to :milestone
@@ -17,9 +23,15 @@ module Milestoneable
scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) }
- scope :any_release, -> { joins_milestone_releases }
- scope :with_release, -> (tag, project_id) { joins_milestone_releases.where(milestones: { releases: { tag: tag, project_id: project_id } }) }
- scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
+ scope :any_release, -> do
+ where("EXISTS (?)", milestone_releases_subquery)
+ end
+ scope :with_release, -> (tag, project_id) do
+ where("EXISTS (?)", milestone_releases_subquery.where(releases: { tag: tag, project_id: project_id }))
+ end
+ scope :without_particular_release, -> (tag, project_id) do
+ where("EXISTS (?)", milestone_releases_subquery.where.not(releases: { tag: tag, project_id: project_id }))
+ end
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
@@ -30,11 +42,6 @@ module Milestoneable
.where(milestone_releases: { release_id: nil })
end
- scope :joins_milestone_releases, -> do
- joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
- JOIN releases ON milestone_releases.release_id = releases.id").distinct
- end
-
private
def milestone_is_valid
diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb
index 4ad8d16fcb9..794748483e4 100644
--- a/app/models/concerns/sensitive_serializable_hash.rb
+++ b/app/models/concerns/sensitive_serializable_hash.rb
@@ -19,8 +19,6 @@ module SensitiveSerializableHash
# In general, prefer NOT to use serializable_hash / to_json / as_json in favor
# of serializers / entities instead which has an allowlist of attributes
def serializable_hash(options = nil)
- return super if options && options[:unsafe_serialization_hash]
-
options = options.try(:dup) || {}
options[:except] = Array(options[:except]).dup
diff --git a/app/models/concerns/signature_type.rb b/app/models/concerns/signature_type.rb
new file mode 100644
index 00000000000..804f42b6f72
--- /dev/null
+++ b/app/models/concerns/signature_type.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SignatureType
+ TYPES = %i[gpg ssh x509].freeze
+
+ def type
+ raise NoMethodError, 'must implement `type` method'
+ end
+
+ TYPES.each do |type|
+ define_method("#{type}?") { self.type == type }
+ end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index eccb004b503..6532a18d1b8 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -72,7 +72,7 @@ module Sortable
private
- def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
+ def highest_label_priority(target_column:, project_column:, target_type_column: nil, target_type: nil, excluded_labels: [])
query = Label.select(LabelPriority.arel_table[:priority].minimum.as('label_priority'))
.left_join_priorities
.joins(:label_links)
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index ee5774d4868..05addcf83d2 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -63,14 +63,15 @@ module Taskable
def task_status(short: false)
return '' if description.blank?
- prep, completed = if short
- ['/', '']
- else
- [' of ', ' completed']
- end
-
sum = tasks.summary
- "#{sum.complete_count}#{prep}#{sum.item_count} #{'checklist item'.pluralize(sum.item_count)}#{completed}"
+ checklist_item_noun = n_('checklist item', 'checklist items', sum.item_count)
+ if short
+ format(s_('Tasks|%{complete_count}/%{total_count} %{checklist_item_noun}'),
+checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
+ else
+ format(s_('Tasks|%{complete_count} of %{total_count} %{checklist_item_noun} completed'),
+checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
+ end
end
# Return a short string that describes the current state of this Taskable's
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 54fe9eac2bc..2b7447dc700 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -15,12 +15,13 @@ module TimeTrackable
alias_method :time_spent?, :time_spent
- default_value_for :time_estimate, value: 0, allows_nil: false
+ attribute :time_estimate, default: 0
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ after_initialize :set_time_estimate_default_value
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -67,6 +68,13 @@ module TimeTrackable
val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
end
+ def set_time_estimate_default_value
+ return if new_record?
+ return unless has_attribute?(:time_estimate)
+
+ self.time_estimate ||= self.class.column_defaults['time_estimate']
+ end
+
private
def reset_spent_time
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 7da4e31b472..db0fcd915b3 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -98,6 +98,8 @@ class ContainerRepository < ApplicationRecord
)
end
+ before_update :set_status_updated_at_to_now, if: :status_changed?
+
state_machine :migration_state, initial: :default, use_transactions: false do
state :pre_importing do
validates :migration_pre_import_started_at, presence: true
@@ -521,11 +523,20 @@ class ContainerRepository < ApplicationRecord
end
def set_delete_ongoing_status
- update_columns(status: :delete_ongoing, delete_started_at: Time.zone.now)
+ now = Time.zone.now
+ update_columns(
+ status: :delete_ongoing,
+ delete_started_at: now,
+ status_updated_at: now
+ )
end
def set_delete_scheduled_status
- update_columns(status: :delete_scheduled, delete_started_at: nil)
+ update_columns(
+ status: :delete_scheduled,
+ delete_started_at: nil,
+ status_updated_at: Time.zone.now
+ )
end
def migration_in_active_state?
@@ -623,6 +634,10 @@ class ContainerRepository < ApplicationRecord
tag
end
end
+
+ def set_status_updated_at_to_now
+ self.status_updated_at = Time.zone.now
+ end
end
ContainerRepository.prepend_mod_with('ContainerRepository')
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index 5eda9b4bf15..91656d4f846 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -85,8 +85,8 @@ class CustomerRelations::Organization < ApplicationRecord
private
def self.default_state_counts
- states.keys.each_with_object({}) do |key, memo|
- memo[key] = 0
+ states.keys.index_with do |key|
+ 0
end
end
diff --git a/app/models/dependency_proxy/group_setting.rb b/app/models/dependency_proxy/group_setting.rb
index 3a7ae66a263..b39ea36644a 100644
--- a/app/models/dependency_proxy/group_setting.rb
+++ b/app/models/dependency_proxy/group_setting.rb
@@ -3,7 +3,5 @@
class DependencyProxy::GroupSetting < ApplicationRecord
belongs_to :group
- attribute :enabled, default: true
-
validates :group, presence: true
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 66d1ce01814..498ca9c4f30 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -37,6 +37,7 @@ class DeployToken < ApplicationRecord
message: "can contain only letters, digits, '_', '-', '+', and '.'"
}
+ validates :expires_at, iso8601_date: true, on: :create
validates :deploy_token_type, presence: true
enum deploy_token_type: {
group_type: 1,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index ea92b978d3a..1254ce1c90a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -363,6 +363,10 @@ class Deployment < ApplicationRecord
deployable&.user || user
end
+ def triggered_by?(user)
+ deployed_by == user
+ end
+
def link_merge_requests(relation)
# NOTE: relation.select will perform column deduplication,
# when id == environment_id it will outputs 2 columns instead of 3
@@ -441,9 +445,10 @@ class Deployment < ApplicationRecord
# default tag limit is 100, 0 means no limit
# when refs_by_oid is passed an SHA, returns refs for that commit
def tags(limit: 100)
- project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || []
+ strong_memoize_with(:tag, limit) do
+ project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || []
+ end
end
- strong_memoize_attr :tags
private
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 2d3f342953f..f1edfb3a34b 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,6 +6,7 @@ class Environment < ApplicationRecord
include FastDestroyAll::Helpers
include Presentable
include NullifyIfBlank
+ include FromUnion
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
@@ -27,27 +28,29 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
- # NOTE: If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader.
- has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment
- has_one :last_visible_deployment, -> { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
+ # NOTE:
+ # 1) no-op arguments is to prevent accidental legacy preloading. See: https://gitlab.com/gitlab-org/gitlab/-/issues/369240
+ # 2) If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader.
+ has_one :last_deployment, -> (_env) { success.ordered }, class_name: 'Deployment', inverse_of: :environment
+ has_one :last_visible_deployment, -> (_env) { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
+ has_one :upcoming_deployment, -> (_env) { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
Deployment::FINISHED_STATUSES.each do |status|
- has_one :"last_#{status}_deployment", -> { where(status: status).ordered },
+ has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered },
class_name: 'Deployment', inverse_of: :environment
end
Deployment::UPCOMING_STATUSES.each do |status|
- has_one :"last_#{status}_deployment", -> { where(status: status).ordered_as_upcoming },
+ has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered_as_upcoming },
class_name: 'Deployment', inverse_of: :environment
end
- has_one :upcoming_deployment, -> { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+ before_validation :ensure_environment_tier
before_save :set_environment_type
- before_save :ensure_environment_tier
after_save :clear_reactive_cache!
validates :name,
@@ -68,6 +71,10 @@ class Environment < ApplicationRecord
length: { maximum: 255 },
allow_nil: true
+ # Currently, the tier presence is validaed for newly created environments.
+ # After the `BackfillEnvironmentTiers` background migration has been completed, we should remove `on: :create`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/385253.
+ validates :tier, presence: true, on: :create
validate :safe_external_url
validate :merge_request_not_changed
@@ -87,7 +94,6 @@ class Environment < ApplicationRecord
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
- scope :preload_cluster, -> { preload(last_deployment: :cluster) }
scope :preload_project, -> { preload(:project) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
@@ -96,7 +102,16 @@ class Environment < ApplicationRecord
# Search environments which have names like the given query.
# Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
scope :for_name_like, -> (query, limit: 5) do
- where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit)
+ top_level = 'LOWER(environments.name) LIKE LOWER(?) || \'%\''
+
+ where(top_level, sanitize_sql_like(query)).limit(limit)
+ end
+
+ scope :for_name_like_within_folder, -> (query, limit: 5) do
+ within_folder = 'LOWER(ltrim(environments.name, environments.environment_type'\
+ ' || \'/\')) LIKE LOWER(?) || \'%\''
+
+ where(within_folder, sanitize_sql_like(query)).limit(limit)
end
scope :for_project, -> (project) { where(project_id: project) }
@@ -106,7 +121,6 @@ class Environment < ApplicationRecord
scope :with_rank, -> do
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
end
- scope :for_id, -> (id) { where(id: id) }
scope :with_deployment, -> (sha, status: nil) do
deployments = Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)
@@ -197,12 +211,19 @@ class Environment < ApplicationRecord
update_all(auto_delete_at: at_time)
end
+ def self.nested
+ group('COALESCE(environment_type, id::text)', 'COALESCE(environment_type, name)')
+ .select('COALESCE(environment_type, id::text), COALESCE(environment_type, name) AS name',
+ 'COUNT(*) AS size', 'MAX(id) AS last_id')
+ .order('name ASC')
+ end
+
class << self
def count_by_state
environments_count_by_state = group(:state).count
- valid_states.each_with_object({}) do |state, count_hash|
- count_hash[state] = environments_count_by_state[state.to_s] || 0
+ valid_states.index_with do |state|
+ environments_count_by_state[state.to_s] || 0
end
end
end
@@ -490,6 +511,12 @@ class Environment < ApplicationRecord
environment_type.nil?
end
+ def deploy_freezes
+ Gitlab::SafeRequestStore.fetch("project:#{project_id}:freeze_periods_for_environments") do
+ project.freeze_periods
+ end
+ end
+
private
# We deliberately avoid using AddressableUrlValidator to allow users to update their environments even if they have
diff --git a/app/models/event.rb b/app/models/event.rb
index a1417db3410..ed65b367b8a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -132,7 +132,7 @@ class Event < ApplicationRecord
where(
'action IN (?) OR (target_type IN (?) AND action IN (?))',
[actions[:pushed], actions[:commented]],
- %w(MergeRequest Issue), [actions[:created], actions[:closed], actions[:merged]]
+ %w(MergeRequest Issue WorkItem), [actions[:created], actions[:closed], actions[:merged]]
)
end
@@ -380,13 +380,11 @@ class Event < ApplicationRecord
protected
def capability
- @capability ||= begin
- capabilities.flat_map do |ability, syms|
- if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
- [ability]
- else
- []
- end
+ @capability ||= capabilities.flat_map do |ability, syms|
+ if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
+ [ability]
+ else
+ []
end
end
end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 6c8bfc35334..b02074849a1 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -3,8 +3,6 @@
class GenericCommitStatus < CommitStatus
EXTERNAL_STAGE_IDX = 1_000_000
- before_validation :set_default_values
-
validates :target_url, addressable_url: true,
length: { maximum: 255 },
allow_nil: true
@@ -13,12 +11,6 @@ class GenericCommitStatus < CommitStatus
# GitHub compatible API
alias_attribute :context, :name
- def set_default_values
- self.context ||= 'default'
- self.stage ||= 'external'
- self.stage_idx ||= EXTERNAL_STAGE_IDX
- end
-
def tags
[:external]
end
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 2db074e733e..1bf35179393 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -40,8 +40,8 @@ class GpgKey < ApplicationRecord
unless: -> { errors.has_key?(:key) }
before_validation :extract_fingerprint, :extract_primary_keyid
- after_commit :update_invalid_gpg_signatures, on: :create
after_create :generate_subkeys
+ after_commit :update_invalid_gpg_signatures, on: :create
def primary_keyid
super&.upcase
diff --git a/app/models/group.rb b/app/models/group.rb
index 098116ed800..0cdd7dd8596 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -20,6 +20,7 @@ class Group < Namespace
include BulkUsersByEmailLoad
include ChronicDurationAttribute
include RunnerTokenExpirationInterval
+ include Todoable
extend ::Gitlab::Utils::Override
@@ -119,7 +120,7 @@ class Group < Namespace
has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id
- has_many :protected_branches, inverse_of: :group
+ has_many :protected_branches, inverse_of: :group, foreign_key: :namespace_id
has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting'
@@ -154,10 +155,10 @@ class Group < Namespace
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
after_create :post_create_hook
+ after_create -> { create_or_load_association(:group_feature) }
+ after_update :path_changed_hook, if: :saved_change_to_path?
after_destroy :post_destroy_hook
after_commit :update_two_factor_requirement
- after_update :path_changed_hook, if: :saved_change_to_path?
- after_create -> { create_or_load_association(:group_feature) }
scope :with_users, -> { includes(:users) }
@@ -165,7 +166,16 @@ class Group < Namespace
scope :by_id, ->(groups) { where(id: groups) }
- scope :by_ids_or_paths, -> (ids, paths) { by_id(ids).or(where(path: paths)) }
+ scope :by_ids_or_paths, -> (ids, paths) do
+ return by_id(ids) unless paths.present?
+
+ ids_by_full_path = Route
+ .for_routable_type(Namespace.name)
+ .where('LOWER(routes.path) IN (?)', paths.map(&:downcase))
+ .select(:namespace_id)
+
+ Group.from_union([by_id(ids), by_id(ids_by_full_path), where('LOWER(path) IN (?)', paths.map(&:downcase))])
+ end
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
@@ -550,6 +560,11 @@ class Group < Namespace
members_with_parents.pluck(Arel.sql('DISTINCT members.user_id'))
end
+ def self_and_hierarchy_intersecting_with_user_groups(user)
+ user_groups = GroupsFinder.new(user).execute.unscope(:order)
+ self_and_hierarchy.unscope(:order).where(id: user_groups)
+ end
+
def self_and_ancestors_ids
strong_memoize(:self_and_ancestors_ids) do
self_and_ancestors.pluck(:id)
@@ -831,6 +846,7 @@ class Group < Namespace
def has_project_with_service_desk_enabled?
Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
+ strong_memoize_attr :has_project_with_service_desk_enabled?, :has_project_with_service_desk_enabled
def activity_path
Gitlab::Routing.url_helpers.activity_group_path(self)
@@ -887,6 +903,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end
+ def work_items_mvc_feature_flag_enabled?
+ feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc)
+ end
+
def work_items_mvc_2_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2)
end
diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb
index c65b00a6de0..9495df7ab6d 100644
--- a/app/models/group_deploy_key.rb
+++ b/app/models/group_deploy_key.rb
@@ -12,6 +12,11 @@ class GroupDeployKey < Key
joins(:group_deploy_keys_groups).where(group_deploy_keys_groups: { group_id: group_ids }).uniq
end
+ # Remove usage_type because it defined in Key class but doesn't have a column in group_deploy_keys table
+ def self.defined_enums
+ super.without('usage_type')
+ end
+
def type
'DeployKey'
end
diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb
index cdcfd3f3ff5..4599ebf8717 100644
--- a/app/models/hooks/active_hook_filter.rb
+++ b/app/models/hooks/active_hook_filter.rb
@@ -18,10 +18,6 @@ class ActiveHookFilter
branch_name = Gitlab::Git.branch_name(data[:ref])
- if Feature.disabled?(:enhanced_webhook_support_regex)
- return RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name)
- end
-
case @hook.branch_filter_strategy
when 'all_branches'
true
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 27119d3a95a..94ced96bbde 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -13,4 +13,9 @@ class ServiceHook < WebHook
override :parent
delegate :parent, to: :integration
+
+ override :executable?
+ def executable?
+ true
+ end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 946cdda2e75..189291a38ec 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -41,12 +41,9 @@ class WebHook < ApplicationRecord
after_initialize :initialize_url_variables
before_validation :reset_token
- before_validation :set_branch_filter_nil, \
- if: -> { branch_filter_strategy_all_branches? && enhanced_webhook_support_regex? }
- validates :push_events_branch_filter, \
- untrusted_regexp: true, if: -> { branch_filter_strategy_regex? && enhanced_webhook_support_regex? }
- validates :push_events_branch_filter, \
- "web_hooks/wildcard_branch_filter": true, if: -> { branch_filter_strategy_wildcard? }
+ before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches?
+ validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex?
+ validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard?
validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' }
validate :no_missing_url_variables
@@ -59,8 +56,6 @@ class WebHook < ApplicationRecord
}, _prefix: true
scope :executable, -> do
- next all unless Feature.enabled?(:web_hooks_disable_failed)
-
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
end
@@ -69,23 +64,17 @@ class WebHook < ApplicationRecord
where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
end
- def self.web_hooks_disable_failed?(hook)
- Feature.enabled?(:web_hooks_disable_failed, hook.parent)
- end
-
def executable?
!temporarily_disabled? && !permanently_disabled?
end
def temporarily_disabled?
- return false unless web_hooks_disable_failed?
return false if recent_failures <= FAILURE_THRESHOLD
disabled_until.present? && disabled_until >= Time.current
end
def permanently_disabled?
- return false unless web_hooks_disable_failed?
return false if disabled_until.present?
recent_failures > FAILURE_THRESHOLD
@@ -197,7 +186,7 @@ class WebHook < ApplicationRecord
end
# See app/validators/json_schemas/web_hooks_url_variables.json
- VARIABLE_REFERENCE_RE = /\{([A-Za-z_][A-Za-z0-9_]+)\}/.freeze
+ VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze
def interpolated_url
return url unless url.include?('{')
@@ -232,10 +221,6 @@ class WebHook < ApplicationRecord
backoff_count.succ.clamp(1, MAX_FAILURES)
end
- def web_hooks_disable_failed?
- self.class.web_hooks_disable_failed?(self)
- end
-
def initialize_url_variables
self.url_variables = {} if encrypted_url_variables.nil?
end
@@ -257,10 +242,6 @@ class WebHook < ApplicationRecord
errors.add(:url, "Invalid URL template. Missing keys: #{missing}")
end
- def enhanced_webhook_support_regex?
- Feature.enabled?(:enhanced_webhook_support_regex)
- end
-
def set_branch_filter_nil
self.push_events_branch_filter = nil
end
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
index bc363cce8dd..bdb53653637 100644
--- a/app/models/import_export_upload.rb
+++ b/app/models/import_export_upload.rb
@@ -2,7 +2,6 @@
class ImportExportUpload < ApplicationRecord
include WithUploads
- include ObjectStorage::BackgroundMove
belongs_to :project
belongs_to :group
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 41278dce22d..a630a6dee11 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -19,7 +19,7 @@ class Integration < ApplicationRecord
INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
- drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira
+ drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
@@ -41,7 +41,9 @@ class Integration < ApplicationRecord
Integrations::BaseCi
Integrations::BaseIssueTracker
Integrations::BaseMonitoring
+ Integrations::BaseSlackNotification
Integrations::BaseSlashCommands
+ Integrations::BaseThirdPartyWiki
].freeze
SECTION_TYPE_CONFIGURATION = 'configuration'
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 2cfd71c9eb2..b8cfd718007 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -42,10 +42,8 @@ module Integrations
end
def client
- @_client ||= begin
- ::Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
+ @_client ||= ::Asana::Client.new do |c|
+ c.authentication :access_token, api_key
end
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index b4e97f0871e..fc5e6a88c2d 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -16,7 +16,7 @@ module Integrations
help: -> { s_('BambooService|Bamboo build plan key.') },
non_empty_password_title: -> { s_('BambooService|Enter new build key') },
non_empty_password_help: -> { s_('BambooService|Leave blank to use your current build key.') },
- placeholder: -> { s_('KEY') },
+ placeholder: -> { _('KEY') },
required: true
field :username,
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 750aa60b185..f2a707c2214 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -33,7 +33,10 @@ module Integrations
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
- validates :webhook, presence: true, public_url: true, if: :activated?
+ validates :webhook,
+ presence: true,
+ public_url: true,
+ if: -> (integration) { integration.activated? && integration.requires_webhook? }
validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
@@ -73,8 +76,6 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', help: "#{webhook_help}", required: true }.freeze,
- { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{
type: 'select',
@@ -96,19 +97,24 @@ module Integrations
['Match all of the labels', MATCH_ALL_LABELS]
]
}.freeze
- ].freeze
+ ].tap do |fields|
+ next unless requires_webhook?
+
+ fields.unshift(
+ { type: 'text', name: 'webhook', help: webhook_help, required: true }.freeze,
+ { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze
+ )
+ end.freeze
end
def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- return unless webhook.present?
-
object_kind = data[:object_kind]
+ return false unless should_execute?(object_kind)
+
data = custom_data(data)
- return unless notify_label?(data)
+ return false unless notify_label?(data)
# WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate
@@ -168,8 +174,17 @@ module Integrations
self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
+ def requires_webhook?
+ true
+ end
+
private
+ def should_execute?(object_kind)
+ supported_events.include?(object_kind) &&
+ (!requires_webhook? || webhook.present?)
+ end
+
def log_usage(_, _)
# Implement in child class
end
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index cb785afdcfe..7a2a91aa0d2 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -32,13 +32,15 @@ module Integrations
true
end
+ private
+
override :log_usage
def log_usage(event, user_id)
return unless user_id
return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
- key = "i_ecosystem_slack_service_#{event}_notification"
+ key = "#{metrics_key_prefix}_#{event}_notification"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
@@ -55,8 +57,13 @@ module Integrations
label: Integration::SNOWPLOW_EVENT_LABEL,
property: key,
user: User.find(user_id),
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context],
**optional_arguments
)
end
+
+ def metrics_key_prefix
+ raise NotImplementedError
+ end
end
end
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
index 314f0a6ee5d..11ff7547325 100644
--- a/app/models/integrations/base_slash_commands.rb
+++ b/app/models/integrations/base_slash_commands.rb
@@ -60,7 +60,7 @@ module Integrations
# rubocop: disable CodeReuse/ServiceClass
def find_chat_user(params)
- ChatNames::FindUserService.new(self, params).execute
+ ChatNames::FindUserService.new(params[:team_id], params[:user_id]).execute
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index c1c43af99bf..31e9a171d1b 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -10,7 +10,7 @@ module Integrations
validate :validate_confluence_url_is_cloud, if: :activated?
field :confluence_url,
- title: -> { s_('Confluence Cloud Workspace URL') },
+ title: -> { _('Confluence Cloud Workspace URL') },
placeholder: 'https://example.atlassian.net/wiki',
required: true
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 27bed5d3f76..80eecc14d0f 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -9,7 +9,7 @@ module Integrations
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
- pipeline job archive_trace
+ pipeline build archive_trace
].freeze
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
@@ -48,8 +48,8 @@ module Integrations
field :archive_trace_events,
storage: :attribute,
type: 'checkbox',
- title: -> { s_('Logs') },
- checkbox_label: -> { s_('Enable logs collection') },
+ title: -> { _('Logs') },
+ checkbox_label: -> { _('Enable logs collection') },
help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') }
field :datadog_service,
@@ -156,10 +156,10 @@ module Integrations
end
def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
object_kind = data[:object_kind]
object_kind = 'job' if object_kind == 'build'
- return unless supported_events.include?(object_kind)
-
data = hook_data(data, object_kind)
execute_web_hook!(data, "#{object_kind} hook")
end
diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb
index 52efb29f2c1..d7625cfb3d2 100644
--- a/app/models/integrations/flowdock.rb
+++ b/app/models/integrations/flowdock.rb
@@ -1,28 +1,12 @@
# frozen_string_literal: true
+# This integration is scheduled for removal.
+# All records must be deleted before the class can be removed.
+# https://gitlab.com/gitlab-org/gitlab/-/issues/379197
module Integrations
class Flowdock < Integration
- validates :token, presence: true, if: :activated?
-
- field :token,
- type: 'password',
- help: -> { s_('FlowdockService|Enter your Flowdock token.') },
- non_empty_password_title: -> { s_('ProjectService|Enter new token') },
- non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
- placeholder: '1b609b52537...',
- required: true
-
- def title
- 'Flowdock'
- end
-
- def description
- s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.')
- end
-
- def help
- docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer'
- s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ def readonly?
+ true
end
def self.to_param
@@ -30,22 +14,7 @@ module Integrations
end
def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- ::Flowdock::Git.post(
- data[:ref],
- data[:before],
- data[:after],
- token: token,
- repo: project.repository,
- repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
- commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s",
- diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
- )
+ %w[]
end
end
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 65492bfd9c2..45302a0bd09 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -132,11 +132,9 @@ module Integrations
end
def client
- @client ||= begin
- JIRA::Client.new(options).tap do |client|
- # Replaces JIRA default http client with our implementation
- client.request_client = Gitlab::Jira::HttpClient.new(client.options)
- end
+ @client ||= JIRA::Client.new(options).tap do |client|
+ # Replaces JIRA default http client with our implementation
+ client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
@@ -406,6 +404,7 @@ module Integrations
label: Integration::SNOWPLOW_EVENT_LABEL,
property: key,
user: user,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context],
**optional_arguments
)
end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index dd1c98ee06b..e3c5c22ad3a 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -5,7 +5,7 @@ module Integrations
include SlackMattermostNotifier
def title
- s_('Mattermost notifications')
+ _('Mattermost notifications')
end
def description
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index 7148de66aee..3973b492b6d 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -5,15 +5,15 @@ module Integrations
include HasWebHook
field :username,
- title: -> { s_('Username') },
- help: -> { s_('Enter your Packagist username.') },
+ title: -> { _('Username') },
+ help: -> { _('Enter your Packagist username.') },
placeholder: '',
required: true
field :token,
type: 'password',
- title: -> { s_('Token') },
- help: -> { s_('Enter your Packagist token.') },
+ title: -> { _('Token') },
+ help: -> { _('Enter your Packagist token.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
placeholder: '',
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 791e27c5db7..6bb6b6d60f6 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -112,7 +112,7 @@ module Integrations
user: user_key,
device: device,
priority: priority,
- title: "#{project.full_name}",
+ title: project.full_name.to_s,
message: message,
url: data[:project][:web_url],
url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name }
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index 89326b8174f..07d2d802915 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -20,5 +20,12 @@ module Integrations
def webhook_help
'https://hooks.slack.com/services/…'
end
+
+ private
+
+ override :metrics_key_prefix
+ def metrics_key_prefix
+ 'i_ecosystem_slack_service'
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index fc083002c41..1dd11ff8315 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -91,7 +91,7 @@ class Issue < ApplicationRecord
has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
- has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue
+ has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue, validate: false
has_many :prometheus_alerts, through: :prometheus_alert_events
has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
@@ -105,9 +105,10 @@ class Issue < ApplicationRecord
validates :project, presence: true
validates :issue_type, presence: true
- validates :namespace, presence: true, if: -> { project.present? }
+ validates :namespace, presence: true
validates :work_item_type, presence: true
+ validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed?
validate :due_date_after_start_date
validate :parent_link_confidentiality
@@ -180,7 +181,7 @@ class Issue < ApplicationRecord
scope :without_hidden, -> {
if Feature.enabled?(:ban_user_feature_flag)
- where.not(author_id: Users::BannedUser.all.select(:user_id))
+ where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
else
all
end
@@ -216,8 +217,8 @@ class Issue < ApplicationRecord
before_validation :ensure_namespace_id, :ensure_work_item_type
- after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
+ after_commit :expire_etag_cache, unless: :importing?
after_create_commit :record_create_action, unless: :importing?
attr_spammable :title, spam_title: true
@@ -743,6 +744,17 @@ class Issue < ApplicationRecord
self.work_item_type = WorkItems::Type.default_by_type(issue_type)
end
+
+ def allowed_work_item_type_change
+ return unless changes[:work_item_type_id]
+
+ involved_types = WorkItems::Type.where(id: changes[:work_item_type_id].compact).pluck(:base_type).uniq
+ disallowed_types = involved_types - WorkItems::Type::CHANGEABLE_BASE_TYPES
+
+ return if disallowed_types.empty?
+
+ errors.add(:work_item_type_id, format(_('can not be changed to %{new_type}'), new_type: work_item_type&.name))
+ end
end
Issue.prepend_mod_with('Issue')
diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb
deleted file mode 100644
index 05607fc3a08..00000000000
--- a/app/models/issue_collection.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-# IssueCollection can be used to reduce a list of issues down to a subset.
-#
-# IssueCollection is not meant to be some sort of Enumerable, instead it's meant
-# to take a list of issues and return a new list of issues based on some
-# criteria. For example, given a list of issues you may want to return a list of
-# issues that can be read or updated by a given user.
-class IssueCollection
- attr_reader :collection
-
- def initialize(collection)
- @collection = collection
- end
-
- # Returns all the issues that can be updated by the user.
- def updatable_by_user(user)
- return collection if user.admin?
-
- # Given all the issue projects we get a list of projects that the current
- # user has at least reporter access to.
- projects_with_reporter_access = user
- .projects_with_reporter_access_limited_to(project_ids)
- .pluck(:id)
-
- collection.select do |issue|
- if projects_with_reporter_access.include?(issue.project_id)
- true
- elsif issue.is_a?(Issue)
- issue.assignee_or_author?(user)
- else
- false
- end
- end
- end
-
- alias_method :visible_to, :updatable_by_user
-
- private
-
- def project_ids
- @project_ids ||= collection.map(&:project_id).uniq
- end
-end
diff --git a/app/models/issue_email_participant.rb b/app/models/issue_email_participant.rb
index 76a96151350..dd963bc9e7e 100644
--- a/app/models/issue_email_participant.rb
+++ b/app/models/issue_email_participant.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class IssueEmailParticipant < ApplicationRecord
+ include BulkInsertSafe
+
belongs_to :issue
validates :email, uniqueness: { scope: [:issue_id], case_sensitive: false }
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index c6269313d8b..ebec24731ed 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -4,9 +4,6 @@
class Iteration < ApplicationRecord
include IgnorableColumns
- # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372126
- ignore_column :project_id, remove_with: '15.7', remove_after: '2022-11-18'
-
self.table_name = 'sprints'
def self.reference_prefix
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 23813fa138f..0e88d1ceae9 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class JiraConnectInstallation < ApplicationRecord
+ include Gitlab::Routing
+
attr_encrypted :shared_secret,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -37,13 +39,19 @@ class JiraConnectInstallation < ApplicationRecord
def audience_url
return unless proxy?
- Gitlab::Utils.append_path(instance_url, '/-/jira_connect')
+ Gitlab::Utils.append_path(instance_url, jira_connect_base_path)
end
def audience_installed_event_url
return unless proxy?
- Gitlab::Utils.append_path(instance_url, '/-/jira_connect/events/installed')
+ Gitlab::Utils.append_path(instance_url, jira_connect_events_installed_path)
+ end
+
+ def audience_uninstalled_event_url
+ return unless proxy?
+
+ Gitlab::Utils.append_path(instance_url, jira_connect_events_uninstalled_path)
end
def proxy?
diff --git a/app/models/key.rb b/app/models/key.rb
index 78b0a38bcaa..1f2234129ed 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -32,12 +32,18 @@ class Key < ApplicationRecord
delegate :name, :email, to: :user, prefix: true
- after_commit :add_to_authorized_keys, on: :create
+ enum usage_type: {
+ auth_and_signing: 0,
+ auth: 1,
+ signing: 2
+ }
+
after_create :post_create_hook
after_create :refresh_user_cache
- after_commit :remove_from_authorized_keys, on: :destroy
after_destroy :post_destroy_hook
after_destroy :refresh_user_cache
+ after_commit :add_to_authorized_keys, on: :create
+ after_commit :remove_from_authorized_keys, on: :destroy
alias_attribute :fingerprint_md5, :fingerprint
alias_attribute :name, :title
@@ -45,6 +51,8 @@ class Key < ApplicationRecord
scope :preload_users, -> { preload(:user) }
scope :for_user, -> (user) { where(user: user) }
scope :order_last_used_at_desc, -> { reorder(arel_table[:last_used_at].desc.nulls_last) }
+ scope :auth, -> { where(usage_type: [:auth, :auth_and_signing]) }
+ scope :signing, -> { where(usage_type: [:signing, :auth_and_signing]) }
# Date is set specifically in this scope to improve query time.
scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 8aa48561e60..e1f28c0e117 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -4,7 +4,6 @@ class LfsObject < ApplicationRecord
include AfterCommitQueue
include Checksummable
include EachBatch
- include ObjectStorage::BackgroundMove
include FileStoreMounter
has_many :lfs_objects_projects
diff --git a/app/models/member.rb b/app/models/member.rb
index 80c5fd7e468..107530daf51 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -61,6 +61,7 @@ class Member < ApplicationRecord
validate :access_level_inclusion
validate :validate_member_role_access_level
validate :validate_access_level_locked_for_member_role, on: :update
+ validate :validate_member_role_belongs_to_same_root_namespace
scope :with_invited_user_state, -> do
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
@@ -515,12 +516,22 @@ class Member < ApplicationRecord
end
end
+ def validate_member_role_belongs_to_same_root_namespace
+ return unless member_role_id
+
+ return if member_namespace.id == member_role.namespace_id
+ return if member_namespace.root_ancestor.id == member_role.namespace_id
+
+ errors.add(:member_namespace, _("must be in same hierarchy as custom role's namespace"))
+ end
+
def send_invite
# override in subclass
end
def send_request
notification_service.new_access_request(self)
+ todo_service.create_member_access_request(self) if source_type != 'Project'
end
def post_create_hook
@@ -579,6 +590,12 @@ class Member < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
+ def todo_service
+ TodoService.new
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
def notifiable_options
{}
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index ad1ad1e74fe..796b05b7fff 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -55,6 +55,12 @@ class GroupMember < Member
{ group: group }
end
+ def last_owner_of_the_group?
+ return false unless access_level == Gitlab::Access::OWNER
+
+ group.member_last_owner?(self) || group.member_last_blocked_owner?(self)
+ end
+
private
override :refresh_member_authorized_projects
diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb
index b4e3d6874ef..e9d7b1d3f80 100644
--- a/app/models/members/member_role.rb
+++ b/app/models/members/member_role.rb
@@ -1,18 +1,30 @@
# frozen_string_literal: true
class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
+ include IgnorableColumns
+ ignore_column :download_code, remove_with: '15.9', remove_after: '2023-01-22'
+
has_many :members
belongs_to :namespace
validates :namespace, presence: true
validates :base_access_level, presence: true
validate :belongs_to_top_level_namespace
+ validate :validate_namespace_locked, on: :update
+
+ validates_associated :members
private
def belongs_to_top_level_namespace
return if !namespace || namespace.root?
- errors.add(:namespace, s_("must be top-level namespace"))
+ errors.add(:namespace, s_("MemberRole|must be top-level namespace"))
+ end
+
+ def validate_namespace_locked
+ return unless namespace_id_changed?
+
+ errors.add(:namespace, s_("MemberRole|can't be changed"))
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 1099e0f48c0..6aa6afb595d 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -96,6 +96,10 @@ class ProjectMember < Member
{ project: project }
end
+ def holder_of_the_personal_namespace?
+ project.personal_namespace_holder?(user)
+ end
+
private
override :access_level_inclusion
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 735c0df1529..78c6d983a3d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -121,6 +121,7 @@ class MergeRequest < ApplicationRecord
has_many :draft_notes
has_many :reviews, inverse_of: :merge_request
+ has_many :reviewed_by_users, -> { distinct }, through: :reviews, source: :author
has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request
KNOWN_MERGE_PARAMS = [
@@ -139,6 +140,7 @@ class MergeRequest < ApplicationRecord
after_create :ensure_merge_request_diff, unless: :skip_ensure_merge_request_diff
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
+ after_save :keep_around_commit, unless: :importing?
after_commit :ensure_metrics, on: [:create, :update], unless: :importing?
after_commit :expire_etag_cache, unless: :importing?
@@ -246,7 +248,9 @@ class MergeRequest < ApplicationRecord
end
after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition|
- GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ merge_request.run_after_commit do
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ end
end
# rubocop: disable CodeReuse/ServiceClass
@@ -438,8 +442,6 @@ class MergeRequest < ApplicationRecord
.pick(MergeRequest::Metrics.time_to_merge_expression)
end
- after_save :keep_around_commit, unless: :importing?
-
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
@@ -1270,7 +1272,7 @@ class MergeRequest < ApplicationRecord
end
def mergeable_discussions_state?
- return true unless project.only_allow_merge_if_all_discussions_are_resolved?
+ return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
unresolved_notes.none?(&:to_be_resolved?)
end
@@ -1382,7 +1384,7 @@ class MergeRequest < ApplicationRecord
def default_merge_commit_message(include_description: false, user: nil)
if self.target_project.merge_commit_template.present? && !include_description
- return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self, current_user: user).merge_message
+ return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).merge_commit_message
end
closes_issues_references = visible_closing_issues_for.map do |issue|
@@ -1398,7 +1400,7 @@ class MergeRequest < ApplicationRecord
message << "Closes #{closes_issues_references.to_sentence}"
end
- message << "#{description}" if include_description && description.present?
+ message << description if include_description && description.present?
message << "See merge request #{to_reference(full: true)}"
message.join("\n\n")
@@ -1406,7 +1408,7 @@ class MergeRequest < ApplicationRecord
def default_squash_commit_message(user: nil)
if self.target_project.squash_commit_template.present?
- return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self, current_user: user).squash_message
+ return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).squash_commit_message
end
title
@@ -1451,9 +1453,9 @@ class MergeRequest < ApplicationRecord
end
def mergeable_ci_state?
- return true unless project.only_allow_merge_if_pipeline_succeeds?
+ return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
return false unless actual_head_pipeline
- return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped?
+ return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped?
actual_head_pipeline.success?
end
diff --git a/app/models/merge_request/predictions.rb b/app/models/merge_request/predictions.rb
deleted file mode 100644
index ef9e00b5f74..00000000000
--- a/app/models/merge_request/predictions.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class MergeRequest::Predictions < ApplicationRecord # rubocop:disable Style/ClassAndModuleChildren
- belongs_to :merge_request, inverse_of: :predictions
-
- validates :suggested_reviewers, json_schema: { filename: 'merge_request_predictions_suggested_reviewers' }
-end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index ebbdecf8aa7..281e11c7c13 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -12,7 +12,7 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
- serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
# Sort by committed date in descending order to ensure latest commits comes on the top
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 98a9ccc2040..cff8911d84b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,7 +6,6 @@ class MergeRequestDiff < ApplicationRecord
include ManualInverseAssociation
include EachBatch
include Gitlab::Utils::StrongMemoize
- include ObjectStorage::BackgroundMove
include BulkInsertableAssociations
# Don't display more than 100 commits at once
@@ -267,7 +266,7 @@ class MergeRequestDiff < ApplicationRecord
end
# This method will rely on repository branch sha
- # in case start_commit_sha is nil. Its necesarry for old merge request diff
+ # in case start_commit_sha is nil. It's necessary for old merge request diff
# created before version 8.4 to work
def safe_start_commit_sha
start_commit_sha || merge_request.target_branch_sha
@@ -414,6 +413,29 @@ class MergeRequestDiff < ApplicationRecord
end
end
+ def paginated_diffs(page, per_page)
+ fetching_repository_diffs({}) do |comparison|
+ reorder_diff_files!
+
+ collection = Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff.new(
+ self,
+ page,
+ per_page
+ )
+
+ if comparison
+ comparison.diffs(
+ paths: collection.diff_paths,
+ page: collection.current_page,
+ per_page: collection.limit_value,
+ count: collection.total_count
+ )
+ else
+ collection
+ end
+ end
+ end
+
def diffs(diff_options = nil)
fetching_repository_diffs(diff_options) do |comparison|
# It should fetch the repository when diffs are cleaned by the system.
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 152fb195c97..7e2efa2049b 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -35,7 +35,7 @@ class MergeRequestDiffCommit < ApplicationRecord
sha_attribute :sha
alias_attribute :id, :sha
- serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
scope :with_users, -> { preload(:commit_author, :committer) }
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index f7da4418624..f24161d598f 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -11,6 +11,7 @@ module Ml
belongs_to :user
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
+ has_many :metadata, class_name: 'Ml::CandidateMetadata'
has_many :latest_metrics, -> { latest }, class_name: 'Ml::CandidateMetric', inverse_of: :candidate
attribute :iid, default: -> { SecureRandom.uuid }
@@ -18,7 +19,21 @@ module Ml
scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) }
def artifact_root
- "/ml_candidate_#{iid}/-/"
+ "/#{package_name}/#{package_version}/"
+ end
+
+ def artifact
+ ::Packages::Generic::PackageFinder.new(experiment.project).execute!(package_name, package_version)
+ rescue ActiveRecord::RecordNotFound
+ nil
+ end
+
+ def package_name
+ "ml_candidate_#{iid}"
+ end
+
+ def package_version
+ '-'
end
class << self
diff --git a/app/models/ml/candidate_metadata.rb b/app/models/ml/candidate_metadata.rb
new file mode 100644
index 00000000000..06b893c211f
--- /dev/null
+++ b/app/models/ml/candidate_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class CandidateMetadata < ApplicationRecord
+ validates :candidate, presence: true
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :candidate, message: ->(candidate, _) { "'#{candidate.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :candidate, class_name: 'Ml::Candidate'
+ end
+end
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index 05b238b960d..0a326b0e005 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -10,6 +10,7 @@ module Ml
belongs_to :project
belongs_to :user
has_many :candidates, class_name: 'Ml::Candidate'
+ has_many :metadata, class_name: 'Ml::ExperimentMetadata'
has_internal_id :iid, scope: :project
diff --git a/app/models/ml/experiment_metadata.rb b/app/models/ml/experiment_metadata.rb
new file mode 100644
index 00000000000..93496807e1a
--- /dev/null
+++ b/app/models/ml/experiment_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class ExperimentMetadata < ApplicationRecord
+ validates :experiment, presence: true
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :experiment, message: ->(exp, _) { "'#{exp.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :experiment, class_name: 'Ml::Experiment'
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 51c39ad4ec3..d7d53956656 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -86,6 +86,7 @@ class Namespace < ApplicationRecord
has_many :issues, inverse_of: :namespace
has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory'
+ has_many :achievements, class_name: 'Achievements::Achievement'
validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
@@ -131,26 +132,28 @@ class Namespace < ApplicationRecord
to: :namespace_settings, allow_nil: true
delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=,
to: :namespace_settings
+ delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
+ to: :namespace_settings
delegate :maven_package_requests_forwarding,
:pypi_package_requests_forwarding,
:npm_package_requests_forwarding,
to: :package_settings
- after_save :reload_namespace_details
-
- after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
-
before_create :sync_share_with_group_lock_with_parent
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_destroy :rm_dir
+ after_save :reload_namespace_details
+
+ after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
+
after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear
# Legacy Storage specific hooks
- after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
before_destroy(prepend: true) { prepare_for_destroy }
- after_destroy :rm_dir
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?
@@ -330,6 +333,13 @@ class Namespace < ApplicationRecord
type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?)
end
+ def bot_user_namespace?
+ return false unless user_namespace?
+ return false unless owner && owner.bot?
+
+ true
+ end
+
def owner_required?
user_namespace?
end
@@ -507,6 +517,10 @@ class Namespace < ApplicationRecord
root? && actual_plan.paid?
end
+ def prevent_delete?
+ paid?
+ end
+
def actual_limits
# We default to PlanLimits.new otherwise a lot of specs would fail
# On production each plan should already have associated limits record
@@ -541,12 +555,10 @@ class Namespace < ApplicationRecord
def shared_runners_setting
if shared_runners_enabled
SR_ENABLED
+ elsif allow_descendants_override_disabled_shared_runners
+ SR_DISABLED_WITH_OVERRIDE
else
- if allow_descendants_override_disabled_shared_runners
- SR_DISABLED_WITH_OVERRIDE
- else
- SR_DISABLED_AND_UNOVERRIDABLE
- end
+ SR_DISABLED_AND_UNOVERRIDABLE
end
end
@@ -597,6 +609,10 @@ class Namespace < ApplicationRecord
namespace_settings&.enabled_git_access_protocol
end
+ def all_ancestors_have_runner_registration_enabled?
+ namespace_settings&.all_ancestors_have_runner_registration_enabled?
+ end
+
private
def cluster_enabled_granted?
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 3e6371b0c4d..5081d5cdafe 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -59,6 +59,16 @@ class NamespaceSetting < ApplicationRecord
all_ancestors_allow_diff_preview_in_email?
end
+ def runner_registration_enabled?
+ runner_registration_enabled && all_ancestors_have_runner_registration_enabled?
+ end
+
+ def all_ancestors_have_runner_registration_enabled?
+ return true unless namespace.has_parent?
+
+ !self.class.where(namespace_id: namespace.ancestors, runner_registration_enabled: false).exists?
+ end
+
private
def all_ancestors_allow_diff_preview_in_email?
diff --git a/app/models/namespace_statistics.rb b/app/models/namespace_statistics.rb
index 04ca05d85ff..a17ca2e2c1d 100644
--- a/app/models/namespace_statistics.rb
+++ b/app/models/namespace_statistics.rb
@@ -10,8 +10,8 @@ class NamespaceStatistics < ApplicationRecord # rubocop:disable Gitlab/Namespace
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
before_save :update_storage_size
- after_save :update_root_storage_statistics, if: :saved_change_to_storage_size?
after_destroy :update_root_storage_statistics
+ after_save :update_root_storage_statistics, if: :saved_change_to_storage_size?
delegate :group_namespace?, to: :namespace
diff --git a/app/models/note.rb b/app/models/note.rb
index 8e1f4979602..052df6142c5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -168,10 +168,10 @@ class Note < ApplicationRecord
# Syncs `confidential` with `internal` as we rename the column.
# https://gitlab.com/gitlab-org/gitlab/-/issues/367923
before_create :set_internal_flag
+ after_destroy :expire_etag_cache
after_save :keep_around_commit, if: :for_project_noteable?, unless: -> { importing? || skip_keep_around_commits }
after_save :expire_etag_cache, unless: :importing?
after_save :touch_noteable, unless: :importing?
- after_destroy :expire_etag_cache
after_commit :notify_after_create, on: :create
after_commit :notify_after_destroy, on: :destroy
diff --git a/app/models/operations/feature_flags_client.rb b/app/models/operations/feature_flags_client.rb
index e8c237abbc5..5a05d76254d 100644
--- a/app/models/operations/feature_flags_client.rb
+++ b/app/models/operations/feature_flags_client.rb
@@ -19,11 +19,11 @@ module Operations
before_validation :ensure_token!
- def self.find_for_project_and_token(project, token)
- return unless project
+ def self.find_for_project_and_token(project_id, token)
+ return unless project_id
return unless token
- where(project_id: project).find_by_token(token)
+ where(project_id: project_id).find_by_token(token)
end
def self.update_last_feature_flag_updated_at!(project)
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 317db51f4ef..17c5415939c 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -149,6 +149,7 @@ class Packages::Package < ApplicationRecord
end
scope :preload_composer, -> { preload(:composer_metadatum) }
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
+ scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
diff --git a/app/models/packages/rpm/repository_file.rb b/app/models/packages/rpm/repository_file.rb
index 4b5fa59c6ee..614ec9b3e56 100644
--- a/app/models/packages/rpm/repository_file.rb
+++ b/app/models/packages/rpm/repository_file.rb
@@ -8,6 +8,8 @@ module Packages
include Packages::Installable
INSTALLABLE_STATUSES = [:default].freeze
+ FILELISTS_FILENAME = 'filelists.xml'
+ FILELISTS_SIZE_LIMITATION = 20.megabytes
enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 }
@@ -20,6 +22,14 @@ module Packages
mount_file_store_uploader Packages::Rpm::RepositoryFileUploader
update_project_statistics project_statistics_name: :packages_size
+
+ def self.has_oversized_filelists?(project_id:)
+ where(
+ project_id: project_id,
+ file_name: FILELISTS_FILENAME,
+ size: [FILELISTS_SIZE_LIMITATION..]
+ ).exists?
+ end
end
end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index c1056d4f6cb..cf0f0f9e92f 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -19,11 +19,13 @@ module Pages
def access_control
project.private_pages?
end
+ strong_memoize_attr :access_control
def https_only
domain_https = domain ? domain.https? : true
project.pages_https_only? && domain_https
end
+ strong_memoize_attr :https_only
def source
return unless deployment&.file
@@ -41,6 +43,7 @@ module Pages
file_count: deployment.file_count
}
end
+ strong_memoize_attr :source
def prefix
if project.pages_group_root?
@@ -49,6 +52,7 @@ module Pages
project.full_path.delete_prefix(trim_prefix) + '/'
end
end
+ strong_memoize_attr :prefix
private
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 119cc7fc166..fafbe449c8c 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -28,6 +28,7 @@ module Pages
paths.sort_by(&:prefix).reverse
end
+ # cache_key is required by #present_cached in ::API::Internal::Pages
def cache_key
@cache_key ||= cache&.cache_key
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 328c67a0711..4e3f4b0c328 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -17,6 +17,7 @@ class PagesDomain < ApplicationRecord
has_many :acme_orders, class_name: "PagesDomainAcmeOrder"
has_many :serverless_domain_clusters, class_name: 'Serverless::DomainCluster', inverse_of: :pages_domain
+ after_initialize :set_verification_code
before_validation :clear_auto_ssl_failure, unless: :auto_ssl_enabled
validates :domain, hostname: { allow_numeric_hostname: true }
@@ -44,8 +45,6 @@ class PagesDomain < ApplicationRecord
key: Settings.attr_encrypted_db_key_base,
algorithm: 'aes-256-cbc'
- after_initialize :set_verification_code
-
scope :for_project, ->(project) { where(project: project) }
scope :enabled, -> { where('enabled_until >= ?', Time.current) }
diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb
index 4804f620a99..37bf080ae49 100644
--- a/app/models/performance_monitoring/prometheus_dashboard.rb
+++ b/app/models/performance_monitoring/prometheus_dashboard.rb
@@ -53,8 +53,6 @@ module PerformanceMonitoring
# This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
# implementation. For new existing logic was reused to faster deliver MVC
def schema_validation_warnings
- return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project)
-
self.class.from_json(reload_schema)
[]
rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => e
@@ -65,11 +63,6 @@ module PerformanceMonitoring
private
- def run_custom_validation
- Gitlab::Metrics::Dashboard::Validator
- .errors(reload_schema, dashboard_path: path, project: environment&.project)
- end
-
# dashboard finder methods are somehow limited, #find includes checking if
# user is authorised to view selected dashboard, but modifies schema, which in some cases may
# cause false positives returned from validation, and #find_raw does not authorise users
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 3126dba9d6d..887ef36cc17 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -18,6 +18,7 @@ class PersonalAccessToken < ApplicationRecord
belongs_to :user
+ after_initialize :set_default_scopes, if: :persisted?
before_save :ensure_token
scope :active, -> { not_revoked.not_expired }
@@ -41,8 +42,6 @@ class PersonalAccessToken < ApplicationRecord
validates :scopes, presence: true
validate :validate_scopes
- after_initialize :set_default_scopes, if: :persisted?
-
def revoke!
update!(revoked: true)
end
diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb
index b0dd52c9657..d26778957d5 100644
--- a/app/models/postgresql/detached_partition.rb
+++ b/app/models/postgresql/detached_partition.rb
@@ -7,5 +7,9 @@ module Postgresql
def fully_qualified_table_name
"#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}"
end
+
+ def table_schema
+ Gitlab::Database::GitlabSchema.table_schema(table_name)
+ end
end
end
diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb
index 06e3034e56a..4156c672518 100644
--- a/app/models/programming_language.rb
+++ b/app/models/programming_language.rb
@@ -10,4 +10,22 @@ class ProgrammingLanguage < ApplicationRecord
sanitized_names = names.map(&method(:sanitize_sql_like))
where(arel_table[:name].matches_any(sanitized_names))
end
+
+ def self.most_popular(limit = 25)
+ sql = <<~SQL
+ SELECT
+ mcv
+ FROM
+ pg_stats
+ CROSS JOIN LATERAL
+ unnest(most_common_vals::text::int[]) mt(mcv)
+ WHERE
+ tablename = 'repository_languages' and attname='programming_language_id'
+ LIMIT
+ $1
+ SQL
+ ids = connection.exec_query(sql, 'SQL', [limit]).rows.flatten
+
+ where(id: ids).order(:name)
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0c4f76fb2b9..73dbb55a07b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,68 +89,56 @@ class Project < ApplicationRecord
cache_markdown_field :description, pipeline: :description
- default_value_for :packages_enabled, true
- default_value_for :archived, false
- default_value_for :resolve_outdated_diff_discussions, false
- default_value_for(:repository_storage) do
- Repository.pick_storage_shard
- end
+ attribute :packages_enabled, default: true
+ attribute :archived, default: false
+ attribute :resolve_outdated_diff_discussions, default: false
+ attribute :repository_storage, default: -> { Repository.pick_storage_shard }
+ attribute :shared_runners_enabled, default: -> { Gitlab::CurrentSettings.shared_runners_enabled }
+ attribute :only_allow_merge_if_all_discussions_are_resolved, default: false
+ attribute :remove_source_branch_after_merge, default: true
+ attribute :autoclose_referenced_issues, default: true
+ attribute :ci_config_path, default: -> { Gitlab::CurrentSettings.default_ci_config_path }
- default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
- default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
- default_value_for :remove_source_branch_after_merge, true
- default_value_for :autoclose_referenced_issues, true
- default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required },
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
+ # Storage specific hooks
+ after_initialize :use_hashed_storage
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
- before_save :ensure_runners_token
before_validation :ensure_project_namespace_in_sync
-
before_validation :set_package_registry_access_level, if: :packages_enabled_changed?
before_validation :remove_leading_spaces_on_name
-
- after_save :update_project_statistics, if: :saved_change_to_namespace_id?
-
- after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
-
- after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
-
- after_save :save_topics
-
- after_save :reload_project_namespace_details
+ after_validation :check_pending_delete
+ before_save :ensure_runners_token
after_create -> { create_or_load_association(:project_feature) }
-
after_create -> { create_or_load_association(:ci_cd_settings) }
-
after_create -> { create_or_load_association(:container_expiration_policy) }
-
after_create -> { create_or_load_association(:pages_metadatum) }
-
after_create :set_timestamps_for_create
+ after_create :check_repository_absence!
after_update :update_forks_visibility_level
-
before_destroy :remove_private_deploy_keys
+ after_destroy :remove_exports
+ after_save :update_project_statistics, if: :saved_change_to_namespace_id?
- use_fast_destroy :build_trace_chunks
+ after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
- after_destroy :remove_exports
+ after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
- after_validation :check_pending_delete
+ after_save :save_topics
- # Storage specific hooks
- after_initialize :use_hashed_storage
- after_create :check_repository_absence!
+ after_save :reload_project_namespace_details
+
+ use_fast_destroy :build_trace_chunks
has_many :project_topics, -> { order(:id) }, class_name: 'Projects::ProjectTopic'
has_many :topics, through: :project_topics, class_name: 'Projects::Topic'
@@ -196,7 +184,6 @@ class Project < ApplicationRecord
has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush'
has_one :ewm_integration, class_name: 'Integrations::Ewm'
has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki'
- has_one :flowdock_integration, class_name: 'Integrations::Flowdock'
has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat'
has_one :harbor_integration, class_name: 'Integrations::Harbor'
has_one :irker_integration, class_name: 'Integrations::Irker'
@@ -231,6 +218,17 @@ class Project < ApplicationRecord
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
has_one :forked_from_project, through: :fork_network_member
+
+ # Projects with a very large number of notes may time out destroying them
+ # through the foreign key. Additionally, the deprecated attachment uploader
+ # for notes requires us to use dependent: :destroy to avoid orphaning uploaded
+ # files.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/207222
+ # Order of this association is important for project deletion.
+ # has_many :notes` should be the first association among all `has_many` associations.
+ has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id'
has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
has_many :fork_network_projects, through: :fork_network, source: :projects
@@ -259,25 +257,30 @@ class Project < ApplicationRecord
has_one :service_desk_setting, class_name: 'ServiceDeskSetting'
# Merge requests for target project should be removed with it
- has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
+ has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
- has_many :issues
+ has_many :issues, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_many :incident_management_timeline_event_tags, inverse_of: :project, class_name: 'IncidentManagement::TimelineEventTag'
- has_many :labels, class_name: 'ProjectLabel'
- has_many :integrations
+ has_many :labels, class_name: 'ProjectLabel', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :events
has_many :milestones
- # Projects with a very large number of notes may time out destroying them
- # through the foreign key. Additionally, the deprecated attachment uploader
- # for notes requires us to use dependent: :destroy to avoid orphaning uploaded
- # files.
- #
- # https://gitlab.com/gitlab-org/gitlab/-/issues/207222
- has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
+ has_many :integrations
+ has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration'
+ has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration'
+ has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration'
+ has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration'
+ has_many :deployment_hooks_integrations, -> { deployment_hooks }, class_name: 'Integration'
+ has_many :issue_hooks_integrations, -> { issue_hooks }, class_name: 'Integration'
+ has_many :job_hooks_integrations, -> { job_hooks }, class_name: 'Integration'
+ has_many :merge_request_hooks_integrations, -> { merge_request_hooks }, class_name: 'Integration'
+ has_many :note_hooks_integrations, -> { note_hooks }, class_name: 'Integration'
+ has_many :pipeline_hooks_integrations, -> { pipeline_hooks }, class_name: 'Integration'
+ has_many :push_hooks_integrations, -> { push_hooks }, class_name: 'Integration'
+ has_many :tag_push_hooks_integrations, -> { tag_push_hooks }, class_name: 'Integration'
+ has_many :wiki_page_hooks_integrations, -> { wiki_page_hooks }, class_name: 'Integration'
has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches
@@ -380,7 +383,7 @@ class Project < ApplicationRecord
has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
- has_many :project_badges, class_name: 'ProjectBadge'
+ has_many :project_badges, class_name: 'ProjectBadge', inverse_of: :project
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
@@ -650,6 +653,11 @@ class Project < ApplicationRecord
.where(repository_languages: { programming_language_id: lang_id_query })
end
+ scope :with_programming_language_id, ->(language_id) do
+ joins(:repository_languages)
+ .where(repository_languages: { programming_language_id: language_id })
+ end
+
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) }
@@ -742,6 +750,29 @@ class Project < ApplicationRecord
end
end
+ # Defines instance methods:
+ #
+ # - only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: false)
+ # - allow_merge_on_skipped_pipeline?(inherit_group_setting: false)
+ # - only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: false)
+ # - only_allow_merge_if_pipeline_succeeds_locked?
+ # - allow_merge_on_skipped_pipeline_locked?
+ # - only_allow_merge_if_all_discussions_are_resolved_locked?
+ def self.cascading_with_parent_namespace(attribute)
+ # method overriden in EE
+ define_method("#{attribute}?") do |inherit_group_setting: false|
+ self.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ define_method("#{attribute}_locked?") do
+ false
+ end
+ end
+
+ cascading_with_parent_namespace :only_allow_merge_if_pipeline_succeeds
+ cascading_with_parent_namespace :allow_merge_on_skipped_pipeline
+ cascading_with_parent_namespace :only_allow_merge_if_all_discussions_are_resolved
+
def self.with_feature_available_for_user(feature, user)
with_project_feature.merge(ProjectFeature.with_feature_available_for_user(feature, user))
end
@@ -1691,8 +1722,14 @@ class Project < ApplicationRecord
def execute_integrations(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
- integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
- integration.async_execute(data)
+ if use_integration_relations?
+ association("#{hooks_scope}_integrations").reader.each do |integration|
+ integration.async_execute(data)
+ end
+ else
+ integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
+ integration.async_execute(data)
+ end
end
end
end
@@ -2301,6 +2338,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_PATH', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
+ .append(key: 'CI_PROJECT_NAMESPACE_ID', value: namespace.id.to_s)
.append(key: 'CI_PROJECT_ROOT_NAMESPACE', value: namespace.root_ancestor.path)
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
@@ -2700,7 +2738,13 @@ class Project < ApplicationRecord
def access_request_approvers_to_be_notified
access_request_approvers = members.owners_and_maintainers
- access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ recipients = access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+
+ if recipients.blank?
+ recipients = group.access_request_approvers_to_be_notified
+ end
+
+ recipients
end
def pages_lookup_path(trim_prefix: nil, domain: nil)
@@ -2994,6 +3038,10 @@ class Project < ApplicationRecord
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end
+ def work_items_mvc_feature_flag_enabled?
+ group&.work_items_mvc_feature_flag_enabled? || Feature.enabled?(:work_items_mvc)
+ end
+
def work_items_mvc_2_feature_flag_enabled?
group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2)
end
@@ -3321,6 +3369,12 @@ class Project < ApplicationRecord
ProjectFeature::PRIVATE
end
end
+
+ def use_integration_relations?
+ strong_memoize(:use_integration_relations) do
+ Feature.enabled?(:cache_project_integrations, self)
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb
index decc71ee193..d26ce5465cd 100644
--- a/app/models/project_export_job.rb
+++ b/app/models/project_export_job.rb
@@ -1,11 +1,24 @@
# frozen_string_literal: true
class ProjectExportJob < ApplicationRecord
+ include EachBatch
+
+ EXPIRES_IN = 7.days
+
belongs_to :project
has_many :relation_exports, class_name: 'Projects::ImportExport::RelationExport'
validates :project, :jid, :status, presence: true
+ STATUS = {
+ queued: 0,
+ started: 1,
+ finished: 2,
+ failed: 3
+ }.freeze
+
+ scope :prunable, -> { where("updated_at < ?", EXPIRES_IN.ago) }
+
state_machine :status, initial: :queued do
event :start do
transition [:queued] => :started
@@ -19,9 +32,17 @@ class ProjectExportJob < ApplicationRecord
transition [:queued, :started] => :failed
end
- state :queued, value: 0
- state :started, value: 1
- state :finished, value: 2
- state :failed, value: 3
+ state :queued, value: STATUS[:queued]
+ state :started, value: STATUS[:started]
+ state :finished, value: STATUS[:finished]
+ state :failed, value: STATUS[:failed]
+ end
+
+ class << self
+ def prune_expired_jobs
+ prunable.each_batch do |relation| # rubocop:disable Style/SymbolProc
+ relation.delete_all
+ end
+ end
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 0570be85ad1..506f6305791 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -11,21 +11,21 @@ class ProjectStatistics < ApplicationRecord
attribute :snippets_size, default: 0
counter_attribute :build_artifacts_size
+ counter_attribute :packages_size
- counter_attribute_after_flush do |project_statistic|
- project_statistic.refresh_storage_size!
+ counter_attribute_after_commit do |project_statistics|
+ project_statistics.refresh_storage_size!
- Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id)
+ Namespaces::ScheduleAggregationWorker.perform_async(project_statistics.namespace_id)
end
before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size, :container_registry_size].freeze
- INCREMENTABLE_COLUMNS = {
- packages_size: %i[storage_size],
- pipeline_artifacts_size: %i[storage_size],
- snippets_size: %i[storage_size]
- }.freeze
+ INCREMENTABLE_COLUMNS = [
+ :pipeline_artifacts_size,
+ :snippets_size
+ ].freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size, :uploads_size, :container_registry_size].freeze
STORAGE_SIZE_COMPONENTS = [
:repository_size,
@@ -120,35 +120,27 @@ class ProjectStatistics < ApplicationRecord
# we have to update the storage_size separately.
#
# For counter attributes, storage_size will be refreshed after the counter is flushed,
- # through counter_attribute_after_flush
+ # through counter_attribute_after_commit
#
# For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS
def self.increment_statistic(project, key, amount)
- raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
- return if amount == 0
-
project.statistics.try do |project_statistics|
- if counter_attribute_enabled?(key)
- project_statistics.delayed_increment_counter(key, amount)
- else
- project_statistics.legacy_increment_statistic(key, amount)
- end
+ project_statistics.increment_statistic(key, amount)
end
end
- def self.incrementable_attribute?(key)
- INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key)
- end
-
- def legacy_increment_statistic(key, amount)
- increment_columns!(key, amount)
+ def increment_statistic(key, amount)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
- Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker
- project.namespace_id)
+ increment_counter(key, amount)
end
private
+ def incrementable_attribute?(key)
+ INCREMENTABLE_COLUMNS.include?(key) || counter_attribute_enabled?(key)
+ end
+
def storage_size_components
STORAGE_SIZE_COMPONENTS
end
@@ -157,16 +149,6 @@ class ProjectStatistics < ApplicationRecord
storage_size_components.map { |component| "COALESCE (#{component}, 0)" }.join(' + ').freeze
end
- def increment_columns!(key, amount)
- increments = { key => amount }
- additional = INCREMENTABLE_COLUMNS.fetch(key, [])
- additional.each do |column|
- increments[column] = amount
- end
-
- update_counters_with_lease(increments)
- end
-
def schedule_namespace_aggregation_worker
run_after_commit do
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
diff --git a/app/models/projects/forks/divergence_counts.rb b/app/models/projects/forks/divergence_counts.rb
new file mode 100644
index 00000000000..7d630b00083
--- /dev/null
+++ b/app/models/projects/forks/divergence_counts.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Projects
+ module Forks
+ # Class for calculating the divergence of a fork with the source project
+ class DivergenceCounts
+ LATEST_COMMITS_COUNT = 10
+ EXPIRATION_TIME = 8.hours
+
+ def initialize(project, ref)
+ @project = project
+ @fork_repo = project.repository
+ @source_repo = project.fork_source.repository
+ @ref = ref
+ end
+
+ def counts
+ ahead, behind = divergence_counts
+
+ { ahead: ahead, behind: behind }
+ end
+
+ private
+
+ attr_reader :project, :fork_repo, :source_repo, :ref
+
+ def cache_key
+ @cache_key ||= ['project_forks', project.id, ref, 'divergence_counts']
+ end
+
+ def divergence_counts
+ fork_sha = fork_repo.commit(ref).sha
+ source_sha = source_repo.commit.sha
+
+ cached_source_sha, cached_fork_sha, counts = Rails.cache.read(cache_key)
+ return counts if cached_source_sha == source_sha && cached_fork_sha == fork_sha
+
+ counts = calculate_divergence_counts(fork_sha, source_sha)
+
+ Rails.cache.write(cache_key, [source_sha, fork_sha, counts], expires_in: EXPIRATION_TIME)
+
+ counts
+ end
+
+ def calculate_divergence_counts(fork_sha, source_sha)
+ # If the upstream latest commit exists in the fork repo, then
+ # it's possible to calculate divergence counts within the fork repository.
+ return fork_repo.diverging_commit_count(fork_sha, source_sha) if fork_repo.commit(source_sha)
+
+ # Otherwise, we need to find a commit that exists both in the fork and upstream
+ # in order to use this commit as a base for calculating divergence counts.
+ # Considering the fact that a user usually creates a fork to contribute to the upstream,
+ # it is expected that they have a limited number of commits ahead of upstream.
+ # Let's take the latest N commits and check their existence upstream.
+ last_commits_shas = fork_repo.commits(ref, limit: LATEST_COMMITS_COUNT).map(&:sha)
+ existence_hash = source_repo.check_objects_exist(last_commits_shas)
+ first_matched_commit_sha = last_commits_shas.find { |sha| existence_hash[sha] }
+
+ # If we can't find such a commit, we return early and tell the user that the branches
+ # have diverged and action is required.
+ return unless first_matched_commit_sha
+
+ # Otherwise, we use upstream to calculate divergence counts from the matched commit
+ ahead, behind = source_repo.diverging_commit_count(first_matched_commit_sha, source_sha)
+ # And add the number of commits a fork is ahead of the first matched commit
+ ahead += last_commits_shas.index(first_matched_commit_sha)
+
+ [ahead, behind]
+ end
+ end
+ end
+end
diff --git a/app/models/projects/import_export/relation_export_upload.rb b/app/models/projects/import_export/relation_export_upload.rb
index 965dc39d19f..12cfb3415d8 100644
--- a/app/models/projects/import_export/relation_export_upload.rb
+++ b/app/models/projects/import_export/relation_export_upload.rb
@@ -4,7 +4,6 @@ module Projects
module ImportExport
class RelationExportUpload < ApplicationRecord
include WithUploads
- include ObjectStorage::BackgroundMove
self.table_name = 'project_relation_export_uploads'
diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb
index 9080f3d9de1..59440947d71 100644
--- a/app/models/prometheus_alert.rb
+++ b/app/models/prometheus_alert.rb
@@ -20,8 +20,8 @@ class PrometheusAlert < ApplicationRecord
has_many :related_issues, through: :prometheus_alert_events
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :prometheus_alert
- after_save :clear_prometheus_adapter_cache!
after_destroy :clear_prometheus_adapter_cache!
+ after_save :clear_prometheus_adapter_cache!
validates :environment, :project, :prometheus_metric, :threshold, :operator, presence: true
validates :runbook_url, length: { maximum: 255 }, allow_blank: true,
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 80967c1b072..c59ef4cd80b 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -14,10 +14,12 @@ class ProtectedBranch < ApplicationRecord
scope :allowing_force_push,
-> { where(allow_force_push: true) }
- scope :get_ids_by_name, -> (name) { where(name: name).pluck(:id) }
-
protected_ref_access_levels :merge, :push
+ def self.get_ids_by_name(name)
+ where(name: name).pluck(:id)
+ end
+
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
# Maintainers, owners and admins are allowed to create the default branch
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index f8d500e106b..b830cf313af 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -20,12 +20,11 @@ class RemoteMirror < ApplicationRecord
belongs_to :project, inverse_of: :remote_mirrors
- validates :url, presence: true, public_url: { schemes: %w(ssh git http https), allow_blank: true, enforce_user: true }
-
- after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
- after_update :reset_fields, if: :saved_change_to_mirror_url?
+ validates :url, presence: true, public_url: { schemes: Project::VALID_MIRROR_PROTOCOLS, allow_blank: true, enforce_user: true }
before_validation :store_credentials
+ after_update :reset_fields, if: :saved_change_to_mirror_url?
+ after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
scope :enabled, -> { where(enabled: true) }
scope :started, -> { with_update_status(:started) }
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index a1753df9294..a1426540cf5 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -14,8 +14,8 @@ class ResourceLabelEvent < ResourceEvent
validates :label, presence: { unless: :importing? }, on: :create
validate :exactly_one_issuable, unless: :importing?
- after_save :expire_etag_cache
after_destroy :expire_etag_cache
+ after_save :expire_etag_cache
enum action: {
add: 1,
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index 6dd7415d928..738f18ca5e3 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -53,7 +53,7 @@ class ServiceDeskSetting < ApplicationRecord
def projects_with_same_slug_and_key_exists?
return false unless project_key
- settings = self.class.with_project_key(project_key).preload(:project)
+ settings = self.class.with_project_key(project_key).where.not(project_id: project_id).preload(:project)
project_slug = self.project.full_path_slug
settings.any? do |setting|
diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb
index 6fb6f0ef713..44bff0e1e5b 100644
--- a/app/models/snippet_statistics.rb
+++ b/app/models/snippet_statistics.rb
@@ -12,8 +12,8 @@ class SnippetStatistics < ApplicationRecord
delegate :repository, :project, :project_id, to: :snippet
- after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics?
after_destroy :update_author_root_storage_statistics, unless: :project_snippet?
+ after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics?
def update_commit_count
self.commit_count = repository.commit_count
diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb
index dea7165af9f..a60c0d2f3bc 100644
--- a/app/models/synthetic_note.rb
+++ b/app/models/synthetic_note.rb
@@ -10,6 +10,7 @@ class SyntheticNote < Note
system: true,
author: event.user,
created_at: event.created_at,
+ updated_at: event.created_at,
discussion_id: event.discussion_id,
noteable: resource,
event: event,
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f2fa0df852a..32ec4accb4b 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -19,6 +19,7 @@ class Todo < ApplicationRecord
DIRECTLY_ADDRESSED = 7
MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature
REVIEW_REQUESTED = 9
+ MEMBER_ACCESS_REQUESTED = 10
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -29,10 +30,11 @@ class Todo < ApplicationRecord
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable,
DIRECTLY_ADDRESSED => :directly_addressed,
- MERGE_TRAIN_REMOVED => :merge_train_removed
+ MERGE_TRAIN_REMOVED => :merge_train_removed,
+ MEMBER_ACCESS_REQUESTED => :member_access_requested
}.freeze
- ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED].freeze
+ ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze
belongs_to :author, class_name: "User"
belongs_to :note
@@ -198,6 +200,16 @@ class Todo < ApplicationRecord
action == MERGE_TRAIN_REMOVED
end
+ def member_access_requested?
+ action == MEMBER_ACCESS_REQUESTED
+ end
+
+ def access_request_url
+ return "" unless self.target_type == 'Namespace'
+
+ Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests')
+ end
+
def done?
state == 'done'
end
@@ -209,6 +221,8 @@ class Todo < ApplicationRecord
def body
if note.present?
note.note
+ elsif member_access_requested?
+ target.full_path
else
target.title
end
@@ -246,6 +260,8 @@ class Todo < ApplicationRecord
def target_reference
if for_commit?
target.reference_link_text
+ elsif member_access_requested?
+ target.full_path
else
target.to_reference
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index ac7ebb31abc..a4fbc703146 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -16,14 +16,13 @@ class Upload < ApplicationRecord
scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
- before_save :calculate_checksum!, if: :foreground_checksummable?
- after_commit :schedule_checksum, if: :needs_checksum?
-
- after_commit :update_project_statistics, on: [:create, :destroy], if: :project?
-
+ before_save :calculate_checksum!, if: :foreground_checksummable?
# as the FileUploader is not mounted, the default CarrierWave ActiveRecord
# hooks are not executed and the file will not be deleted
after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
+ after_commit :schedule_checksum, if: :needs_checksum?
+
+ after_commit :update_project_statistics, on: [:create, :destroy], if: :project?
class << self
def inner_join_local_uploads_projects
diff --git a/app/models/user.rb b/app/models/user.rb
index b4b8a7ef7ad..ba3f7922c9c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -46,6 +46,8 @@ class User < ApplicationRecord
MAX_USERNAME_LENGTH = 255
MIN_USERNAME_LENGTH = 2
+ MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT = 100
+
SECONDARY_EMAIL_ATTRIBUTES = [
:commit_email,
:notification_email,
@@ -58,16 +60,16 @@ class User < ApplicationRecord
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token, encrypted: :optional
- default_value_for :admin, false
- default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
- default_value_for(:can_create_group) { Gitlab::CurrentSettings.can_create_group }
- default_value_for :can_create_team, false
- default_value_for :hide_no_ssh_key, false
- default_value_for :hide_no_password, false
- default_value_for :project_view, :files
- default_value_for :notified_of_own_activity, false
- default_value_for :preferred_language, I18n.default_locale
- default_value_for :theme_id, gitlab_config.default_theme
+ attribute :admin, default: false
+ attribute :external, default: -> { Gitlab::CurrentSettings.user_default_external }
+ attribute :can_create_group, default: -> { Gitlab::CurrentSettings.can_create_group }
+ attribute :can_create_team, default: false
+ attribute :hide_no_ssh_key, default: false
+ attribute :hide_no_password, default: false
+ attribute :project_view, default: :files
+ attribute :notified_of_own_activity, default: false
+ attribute :preferred_language, default: -> { I18n.default_locale }
+ attribute :theme_id, default: -> { gitlab_config.default_theme }
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -298,16 +300,17 @@ class User < ApplicationRecord
validates :website_url, allow_blank: true, url: true, if: :website_url_changed?
+ after_initialize :set_projects_limit
before_validation :sanitize_attrs
+ before_validation :ensure_namespace_correct
+ after_validation :set_username_errors
before_save :default_private_profile_to_false
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
- before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
before_save :ensure_user_detail_assigned
- after_validation :set_username_errors
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
@@ -328,8 +331,6 @@ class User < ApplicationRecord
update_invalid_gpg_signatures if previous_changes.key?('email')
end
- after_initialize :set_projects_limit
-
# User's Layout preference
enum layout: { fixed: 0, fluid: 1 }
@@ -360,6 +361,7 @@ class User < ApplicationRecord
:diffs_deletion_color, :diffs_deletion_color=,
:diffs_addition_color, :diffs_addition_color=,
:use_legacy_web_ide, :use_legacy_web_ide=,
+ :use_new_navigation, :use_new_navigation=,
to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -376,6 +378,14 @@ class User < ApplicationRecord
accepts_nested_attributes_for :credit_card_validation, update_only: true, allow_destroy: true
state_machine :state, initial: :active do
+ # state_machine uses this method at class loading time to fetch the default
+ # value for the `state` column but in doing so it also evaluates all other
+ # columns default values which could trigger the recursive generation of
+ # ApplicationSetting records. We're setting it to `nil` here because we
+ # don't have a database default for the `state` column.
+ #
+ def owner_class_attribute_default; end
+
event :block do
transition active: :blocked
transition deactivated: :blocked
@@ -811,7 +821,7 @@ class User < ApplicationRecord
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
- find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id))
+ find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').auth.where(id: key_id))
end
def find_by_full_path(path, follow_redirects: false)
@@ -896,6 +906,18 @@ class User < ApplicationRecord
end
end
+ def admin_bot
+ email_pattern = "admin-bot%s@#{Settings.gitlab.host}"
+
+ unique_internal(where(user_type: :admin_bot), 'GitLab-Admin-Bot', email_pattern) do |u|
+ u.bio = 'Admin bot used for tasks that require admin privileges'
+ u.name = 'GitLab Admin Bot'
+ u.avatar = bot_avatar(image: 'admin-bot.png')
+ u.admin = true
+ u.confirmed_at = Time.zone.now
+ end
+ end
+
# Return true if there is only single non-internal user in the deployment,
# ghost user is ignored.
def single_user?
@@ -1759,12 +1781,10 @@ class User < ApplicationRecord
end
def ci_owned_runners
- @ci_owned_runners ||= begin
- Ci::Runner
+ @ci_owned_runners ||= Ci::Runner
.from_union([ci_owned_project_runners_from_project_members,
ci_owned_project_runners_from_group_members,
ci_owned_group_runners])
- end
end
def owns_runner?(runner)
@@ -1773,7 +1793,11 @@ class User < ApplicationRecord
def notification_email_for(notification_group)
# Return group-specific email address if present, otherwise return global notification email address
- notification_group&.notification_email_for(self) || notification_email_or_default
+ group_email = if notification_group && notification_group.respond_to?(:notification_email_for)
+ notification_group.notification_email_for(self)
+ end
+
+ group_email || notification_email_or_default
end
def notification_settings_for(source, inherit: false)
@@ -1866,6 +1890,7 @@ class User < ApplicationRecord
def invalidate_issue_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
+ Rails.cache.delete(['users', id, 'max_assigned_open_issues_count']) if Feature.enabled?(:limit_assigned_issues_count)
end
def invalidate_merge_request_cache_counts
@@ -2323,9 +2348,7 @@ class User < ApplicationRecord
end
def check_password_weakness
- if Feature.enabled?(:block_weak_passwords) &&
- password.present? &&
- Security::WeakPasswords.weak_for_user?(password, self)
+ if password.present? && Security::WeakPasswords.weak_for_user?(password, self)
errors.add(:password, _('must not contain commonly used combinations of words and letters'))
end
end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 2e662faea6a..0570bc2f395 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -19,7 +19,7 @@ class UserDetail < ApplicationRecord
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
- validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true
+ validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed?
before_validation :sanitize_attrs
before_save :prevent_nil_bio
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index c6ebd550daf..bc2c6b526b8 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -26,10 +26,10 @@ class UserPreference < ApplicationRecord
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
- default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
- default_value_for :time_display_relative, value: true, allows_nil: false
- default_value_for :time_format_in_24h, value: false, allows_nil: false
- default_value_for :render_whitespace_in_code, value: false, allows_nil: false
+ attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT }
+ attribute :time_display_relative, default: true
+ attribute :time_format_in_24h, default: false
+ attribute :render_whitespace_in_code, default: false
class << self
def notes_filters
@@ -59,6 +59,67 @@ class UserPreference < ApplicationRecord
self[notes_filter_field_for(resource)]
end
+ def tab_width
+ read_attribute(:tab_width) || self.class.column_defaults['tab_width']
+ end
+
+ def tab_width=(value)
+ if value.nil?
+ default = self.class.column_defaults['tab_width']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
+ def time_display_relative
+ value = read_attribute(:time_display_relative)
+ return value unless value.nil?
+
+ self.class.column_defaults['time_display_relative']
+ end
+
+ def time_display_relative=(value)
+ if value.nil?
+ default = self.class.column_defaults['time_display_relative']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
+ def time_format_in_24h
+ value = read_attribute(:time_format_in_24h)
+ return value unless value.nil?
+
+ self.class.column_defaults['time_format_in_24h']
+ end
+
+ def time_format_in_24h=(value)
+ if value.nil?
+ default = self.class.column_defaults['time_format_in_24h']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
+ def render_whitespace_in_code
+ value = read_attribute(:render_whitespace_in_code)
+ return value unless value.nil?
+
+ self.class.column_defaults['render_whitespace_in_code']
+ end
+
+ def render_whitespace_in_code=(value)
+ if value.nil?
+ default = self.class.column_defaults['render_whitespace_in_code']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
private
def notes_filter_field_for(resource)
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index b037d07658d..3f9353214ee 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -63,7 +63,9 @@ module Users
project_quality_summary_feedback: 59, # EE-only
merge_request_settings_moved_callout: 60,
new_top_level_group_alert: 61,
- artifacts_management_page_feedback_banner: 62
+ artifacts_management_page_feedback_banner: 62,
+ vscode_web_ide: 63,
+ vscode_web_ide_callout: 64
}
validates :feature_name,
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 3e3e424e9c9..2552407fa4c 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -23,7 +23,8 @@ module Users
namespace_storage_limit_banner_alert_threshold: 12, # EE-only
namespace_storage_limit_banner_error_threshold: 13, # EE-only
usage_quota_trial_alert: 14, # EE-only
- preview_usage_quota_free_plan_alert: 15 # EE-only
+ preview_usage_quota_free_plan_alert: 15, # EE-only
+ enforcement_at_limit_alert: 16 # EE-only
}
validates :group, presence: true
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index f6123c01fd0..b9e4e908ddd 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -31,11 +31,17 @@ module Users
validates :telesign_reference_xid,
length: { maximum: 255 }
+ scope :for_user, -> (user_id) { where(user_id: user_id) }
+
def self.related_to_banned_user?(international_dial_code, phone_number)
joins(:banned_user).where(
international_dial_code: international_dial_code,
phone_number: phone_number
).exists?
end
+
+ def validated?
+ validated_at.present?
+ end
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index ed6f9d161a6..0810c520f7e 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -38,6 +38,18 @@ class WorkItem < Issue
end
end
+ def ancestors
+ hierarchy.ancestors(hierarchy_order: :asc)
+ end
+
+ def same_type_base_and_ancestors
+ hierarchy(same_type: true).base_and_ancestors(hierarchy_order: :asc)
+ end
+
+ def same_type_descendants_depth
+ hierarchy(same_type: true).max_descendants_depth.to_i
+ end
+
private
override :parent_link_confidentiality
@@ -56,6 +68,13 @@ class WorkItem < Issue
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
end
+
+ def hierarchy(options = {})
+ base = self.class.where(id: id)
+ base = base.where(work_item_type_id: work_item_type_id) if options[:same_type]
+
+ ::Gitlab::WorkItems::WorkItemHierarchy.new(base, options: options)
+ end
end
WorkItem.prepend_mod
diff --git a/app/models/work_items/hierarchy_restriction.rb b/app/models/work_items/hierarchy_restriction.rb
new file mode 100644
index 00000000000..a253447a8db
--- /dev/null
+++ b/app/models/work_items/hierarchy_restriction.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class HierarchyRestriction < ApplicationRecord
+ self.table_name = 'work_item_hierarchy_restrictions'
+
+ belongs_to :parent_type, class_name: 'WorkItems::Type'
+ belongs_to :child_type, class_name: 'WorkItems::Type'
+
+ validates :parent_type, presence: true
+ validates :child_type, presence: true
+ validates :child_type, uniqueness: { scope: :parent_type_id }
+ end
+end
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 13d6db3e08e..33857fb08c2 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -12,12 +12,14 @@ module WorkItems
validates :work_item_parent, presence: true
validates :work_item, presence: true, uniqueness: true
- validate :validate_child_type
- validate :validate_parent_type
+ validate :validate_hierarchy_restrictions
+ validate :validate_cyclic_reference
validate :validate_same_project
validate :validate_max_children
validate :validate_confidentiality
+ scope :for_parents, ->(parent_ids) { where(work_item_parent_id: parent_ids) }
+
class << self
def has_public_children?(parent_id)
joins(:work_item).where(work_item_parent_id: parent_id, 'issues.confidential': false).exists?
@@ -33,27 +35,6 @@ module WorkItems
private
- def validate_child_type
- return unless work_item
-
- unless work_item.task?
- errors.add :work_item, _('only Task can be assigned as a child in hierarchy.')
- end
- end
-
- def validate_parent_type
- return unless work_item_parent
-
- base_type = work_item_parent.work_item_type.base_type.to_sym
- unless PARENT_TYPES.include?(base_type)
- parent_names = WorkItems::Type::BASE_TYPES.slice(*WorkItems::ParentLink::PARENT_TYPES)
- .values.map { |type| type[:name] }
-
- errors.add :work_item_parent, _('only %{parent_types} can be parent of Task.') %
- { parent_types: parent_names.to_sentence }
- end
- end
-
def validate_same_project
return if work_item.nil? || work_item_parent.nil?
@@ -79,5 +60,40 @@ module WorkItems
"parent. Make the work item confidential and try again.")
end
end
+
+ def validate_hierarchy_restrictions
+ return unless work_item && work_item_parent
+
+ restriction = ::WorkItems::HierarchyRestriction
+ .find_by_parent_type_id_and_child_type_id(work_item_parent.work_item_type_id, work_item.work_item_type_id)
+
+ if restriction.nil?
+ errors.add :work_item, _('is not allowed to add this type of parent')
+ return
+ end
+
+ validate_depth(restriction.maximum_depth)
+ end
+
+ def validate_depth(depth)
+ return unless depth
+ return if work_item.work_item_type_id != work_item_parent.work_item_type_id
+
+ if work_item_parent.same_type_base_and_ancestors.count + work_item.same_type_descendants_depth > depth
+ errors.add :work_item, _('reached maximum depth')
+ end
+ end
+
+ def validate_cyclic_reference
+ return unless work_item_parent&.id && work_item&.id
+
+ if work_item.id == work_item_parent.id
+ errors.add :work_item, _('is not allowed to point to itself')
+ end
+
+ if work_item_parent.ancestors.detect { |ancestor| work_item.id == ancestor.id }
+ errors.add :work_item, _('is already present in ancestors')
+ end
+ end
end
end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index dc30899d24f..e1f6a13f7a7 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -10,31 +10,86 @@ module WorkItems
include CacheMarkdownField
+ # type name is used in restrictions DB seeder to assure restrictions for
+ # default types are pre-filled
+ TYPE_NAMES = {
+ issue: 'Issue',
+ incident: 'Incident',
+ test_case: 'Test Case',
+ requirement: 'Requirement',
+ task: 'Task',
+ objective: 'Objective',
+ key_result: 'Key Result'
+ }.freeze
+
# Base types need to exist on the DB on app startup
# This constant is used by the DB seeder
# TODO - where to add new icon names created?
BASE_TYPES = {
- issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
- incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
- test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only
- requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only
- task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 },
- objective: { name: 'Objective', icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only
- key_result: { name: 'Key Result', icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
+ issue: { name: TYPE_NAMES[:issue], icon_name: 'issue-type-issue', enum_value: 0 },
+ incident: { name: TYPE_NAMES[:incident], icon_name: 'issue-type-incident', enum_value: 1 },
+ test_case: { name: TYPE_NAMES[:test_case], icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only
+ requirement: { name: TYPE_NAMES[:requirement], icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only
+ task: { name: TYPE_NAMES[:task], icon_name: 'issue-type-task', enum_value: 4 },
+ objective: { name: TYPE_NAMES[:objective], icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only
+ key_result: { name: TYPE_NAMES[:key_result], icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
}.freeze
WIDGETS_FOR_TYPE = {
- issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
- Widgets::Milestone],
- incident: [Widgets::Description, Widgets::Hierarchy],
- test_case: [Widgets::Description],
- requirement: [Widgets::Description],
- task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
- Widgets::Milestone],
- objective: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::Milestone],
- key_result: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::StartAndDueDate]
+ issue: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::StartAndDueDate,
+ Widgets::Milestone,
+ Widgets::Notes
+ ],
+ incident: [
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::Notes
+ ],
+ test_case: [
+ Widgets::Description,
+ Widgets::Notes
+ ],
+ requirement: [
+ Widgets::Description,
+ Widgets::Notes
+ ],
+ task: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::StartAndDueDate,
+ Widgets::Milestone,
+ Widgets::Notes
+ ],
+ objective: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::Milestone,
+ Widgets::Notes
+ ],
+ key_result: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::StartAndDueDate,
+ Widgets::Notes
+ ]
}.freeze
+ # A list of types user can change between - both original and new
+ # type must be included in this list. This is needed for legacy issues
+ # where it's possible to switch between issue and incident.
+ CHANGEABLE_BASE_TYPES = %w[issue incident test_case].freeze
+
WI_TYPES_WITH_CREATED_HEADER = %w[issue incident].freeze
cache_markdown_field :description, pipeline: :single_line
@@ -66,6 +121,7 @@ module WorkItems
return found_type if found_type
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
+ Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
find_by(namespace_id: nil, base_type: type)
end
diff --git a/app/models/work_items/widgets/notes.rb b/app/models/work_items/widgets/notes.rb
new file mode 100644
index 00000000000..bde94ea8f43
--- /dev/null
+++ b/app/models/work_items/widgets/notes.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Notes < Base
+ delegate :notes, to: :work_item
+ delegate_missing_to :work_item
+
+ def declarative_policy_delegate
+ work_item
+ end
+ end
+ end
+end