summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/models
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
downloadgitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/alert_management/alert.rb93
-rw-r--r--app/models/analytics/cycle_analytics/stage_event_hash.rb42
-rw-r--r--app/models/application_record.rb33
-rw-r--r--app/models/application_setting.rb22
-rw-r--r--app/models/application_setting_implementation.rb4
-rw-r--r--app/models/ci/application_record.rb15
-rw-r--r--app/models/ci/build.rb31
-rw-r--r--app/models/ci/build_metadata.rb3
-rw-r--r--app/models/ci/build_need.rb4
-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.rb3
-rw-r--r--app/models/ci/build_trace_chunk.rb3
-rw-r--r--app/models/ci/build_trace_metadata.rb13
-rw-r--r--app/models/ci/build_trace_section.rb17
-rw-r--r--app/models/ci/build_trace_section_name.rb13
-rw-r--r--app/models/ci/ci_database_record.rb (renamed from app/models/ci/base_model.rb)2
-rw-r--r--app/models/ci/daily_build_group_report_result.rb4
-rw-r--r--app/models/ci/deleted_object.rb4
-rw-r--r--app/models/ci/freeze_period.rb6
-rw-r--r--app/models/ci/group_variable.rb3
-rw-r--r--app/models/ci/instance_variable.rb3
-rw-r--r--app/models/ci/job_artifact.rb3
-rw-r--r--app/models/ci/job_variable.rb3
-rw-r--r--app/models/ci/pending_build.rb13
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/ci/pipeline_artifact.rb3
-rw-r--r--app/models/ci/pipeline_chat_data.rb4
-rw-r--r--app/models/ci/pipeline_config.rb4
-rw-r--r--app/models/ci/pipeline_message.rb4
-rw-r--r--app/models/ci/pipeline_schedule.rb5
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb3
-rw-r--r--app/models/ci/pipeline_variable.rb3
-rw-r--r--app/models/ci/processable.rb6
-rw-r--r--app/models/ci/ref.rb3
-rw-r--r--app/models/ci/resource.rb19
-rw-r--r--app/models/ci/resource_group.rb4
-rw-r--r--app/models/ci/runner.rb12
-rw-r--r--app/models/ci/runner_namespace.rb4
-rw-r--r--app/models/ci/runner_project.rb4
-rw-r--r--app/models/ci/running_build.rb4
-rw-r--r--app/models/ci/sources/pipeline.rb4
-rw-r--r--app/models/ci/stage.rb3
-rw-r--r--app/models/ci/trigger.rb3
-rw-r--r--app/models/ci/trigger_request.rb4
-rw-r--r--app/models/ci/unit_test.rb4
-rw-r--r--app/models/ci/unit_test_failure.rb4
-rw-r--r--app/models/ci/variable.rb3
-rw-r--r--app/models/ci_platform_metric.rb4
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/commit.rb16
-rw-r--r--app/models/commit_status.rb3
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb18
-rw-r--r--app/models/concerns/any_field_validation.rb25
-rw-r--r--app/models/concerns/cache_markdown_field.rb25
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb2
-rw-r--r--app/models/concerns/ci/has_status.rb1
-rw-r--r--app/models/concerns/ci/metadatable.rb10
-rw-r--r--app/models/concerns/ci/namespaced_model_name.rb13
-rw-r--r--app/models/concerns/counter_attribute.rb3
-rw-r--r--app/models/concerns/each_batch.rb6
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb8
-rw-r--r--app/models/concerns/expirable.rb3
-rw-r--r--app/models/concerns/has_integrations.rb12
-rw-r--r--app/models/concerns/incident_management/escalatable.rb104
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/limitable.rb2
-rw-r--r--app/models/concerns/mentionable.rb19
-rw-r--r--app/models/concerns/packages/debian/distribution.rb19
-rw-r--r--app/models/concerns/project_features_compatibility.rb7
-rw-r--r--app/models/concerns/restricted_signup.rb52
-rw-r--r--app/models/concerns/select_for_project_authorization.rb2
-rw-r--r--app/models/concerns/sha256_attribute.rb2
-rw-r--r--app/models/concerns/sha_attribute.rb2
-rw-r--r--app/models/concerns/spammable.rb2
-rw-r--r--app/models/concerns/strip_attribute.rb8
-rw-r--r--app/models/concerns/time_trackable.rb8
-rw-r--r--app/models/concerns/timebox.rb2
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb33
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb28
-rw-r--r--app/models/concerns/x509_serial_number_attribute.rb2
-rw-r--r--app/models/customer_relations/organization.rb31
-rw-r--r--app/models/deploy_token.rb15
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/design_management/version.rb2
-rw-r--r--app/models/diff_discussion.rb6
-rw-r--r--app/models/discussion.rb9
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/error_tracking/client_key.rb22
-rw-r--r--app/models/error_tracking/error.rb69
-rw-r--r--app/models/error_tracking/error_event.rb65
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb21
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/group.rb15
-rw-r--r--app/models/group_deploy_token.rb9
-rw-r--r--app/models/hooks/web_hook.rb13
-rw-r--r--app/models/incident_management/issuable_escalation_status.rb15
-rw-r--r--app/models/instance_configuration.rb76
-rw-r--r--app/models/integration.rb43
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/datadog.rb56
-rw-r--r--app/models/integrations/irker.rb40
-rw-r--r--app/models/integrations/jenkins.rb2
-rw-r--r--app/models/integrations/jira.rb10
-rw-r--r--app/models/integrations/microsoft_teams.rb2
-rw-r--r--app/models/integrations/packagist.rb29
-rw-r--r--app/models/integrations/pushover.rb51
-rw-r--r--app/models/integrations/teamcity.rb2
-rw-r--r--app/models/integrations/unify_circuit.rb2
-rw-r--r--app/models/internal_id.rb4
-rw-r--r--app/models/issuable_severity.rb8
-rw-r--r--app/models/issue.rb56
-rw-r--r--app/models/jira_connect_installation.rb1
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/member.rb26
-rw-r--r--app/models/members/group_member.rb16
-rw-r--r--app/models/members/project_member.rb24
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb10
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/milestone.rb30
-rw-r--r--app/models/namespace.rb38
-rw-r--r--app/models/namespace_setting.rb27
-rw-r--r--app/models/namespaces/traversal/linear.rb29
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb68
-rw-r--r--app/models/namespaces/traversal/recursive.rb1
-rw-r--r--app/models/namespaces/traversal/recursive_scopes.rb36
-rw-r--r--app/models/note.rb25
-rw-r--r--app/models/notification_setting.rb6
-rw-r--r--app/models/operations/feature_flags/strategy.rb2
-rw-r--r--app/models/packages/debian.rb2
-rw-r--r--app/models/packages/event.rb8
-rw-r--r--app/models/packages/npm.rb13
-rw-r--r--app/models/packages/package.rb14
-rw-r--r--app/models/packages/package_file.rb24
-rw-r--r--app/models/personal_access_token.rb4
-rw-r--r--app/models/postgresql/detached_partition.rb7
-rw-r--r--app/models/postgresql/replication_slot.rb50
-rw-r--r--app/models/preloaders/user_max_access_level_in_projects_preloader.rb6
-rw-r--r--app/models/project.rb90
-rw-r--r--app/models/project_feature.rb25
-rw-r--r--app/models/project_setting.rb4
-rw-r--r--app/models/project_team.rb8
-rw-r--r--app/models/projects/ci_feature_usage.rb27
-rw-r--r--app/models/release_highlight.rb8
-rw-r--r--app/models/remote_mirror.rb52
-rw-r--r--app/models/repository.rb41
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/terraform/state.rb1
-rw-r--r--app/models/timelog.rb8
-rw-r--r--app/models/tree.rb6
-rw-r--r--app/models/user.rb112
-rw-r--r--app/models/user_callout.rb5
-rw-r--r--app/models/user_detail.rb1
-rw-r--r--app/models/user_interacted_project.rb12
-rw-r--r--app/models/users/banned_user.rb12
-rw-r--r--app/models/users/in_product_marketing_email.rb5
-rw-r--r--app/models/work_item/type.rb38
160 files changed, 1774 insertions, 788 deletions
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index d0e4163dcdb..f40d0cd2fa4 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -9,24 +9,12 @@ module AlertManagement
include ShaAttribute
include Sortable
include Noteable
+ include Mentionable
include Gitlab::SQL::Pattern
include Presentable
include Gitlab::Utils::StrongMemoize
include Referable
-
- STATUSES = {
- triggered: 0,
- acknowledged: 1,
- resolved: 2,
- ignored: 3
- }.freeze
-
- STATUS_DESCRIPTIONS = {
- triggered: 'Investigation has not started',
- acknowledged: 'Someone is actively investigating the problem',
- resolved: 'No further work is required',
- ignored: 'No action will be taken on the alert'
- }.freeze
+ include ::IncidentManagement::Escalatable
belongs_to :project
belongs_to :issue, optional: true
@@ -44,6 +32,9 @@ module AlertManagement
sha_attribute :fingerprint
+ # Allow :ended_at to be managed by Escalatable
+ alias_attribute :resolved_at, :ended_at
+
TITLE_MAX_LENGTH = 200
DESCRIPTION_MAX_LENGTH = 1_000
SERVICE_MAX_LENGTH = 100
@@ -57,7 +48,6 @@ module AlertManagement
validates :project, presence: true
validates :events, presence: true
validates :severity, presence: true
- validates :status, presence: true
validates :started_at, presence: true
validates :fingerprint, allow_blank: true, uniqueness: {
scope: :project,
@@ -80,52 +70,10 @@ module AlertManagement
threat_monitoring: 1
}
- state_machine :status, initial: :triggered do
- state :triggered, value: STATUSES[:triggered]
-
- state :acknowledged, value: STATUSES[:acknowledged]
-
- state :resolved, value: STATUSES[:resolved] do
- validates :ended_at, presence: true
- end
-
- state :ignored, value: STATUSES[:ignored]
-
- state :triggered, :acknowledged, :ignored do
- validates :ended_at, absence: true
- end
-
- event :trigger do
- transition any => :triggered
- end
-
- event :acknowledge do
- transition any => :acknowledged
- end
-
- event :resolve do
- transition any => :resolved
- end
-
- event :ignore do
- transition any => :ignored
- end
-
- before_transition to: [:triggered, :acknowledged, :ignored] do |alert, _transition|
- alert.ended_at = nil
- end
-
- before_transition to: :resolved do |alert, transition|
- ended_at = transition.args.first
- alert.ended_at = ended_at || Time.current
- end
- end
-
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
- scope :for_status, -> (status) { with_status(status) }
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)) }
@@ -146,36 +94,14 @@ module AlertManagement
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) }
- # Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
- # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
- # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
- scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
-
scope :counts_by_project_id, -> { group(:project_id).count }
alias_method :state, :status_name
- def self.state_machine_statuses
- @state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
- end
- private_class_method :state_machine_statuses
-
- def self.status_value(name)
- state_machine_statuses[name]
- end
-
- def self.status_name(raw_status)
- state_machine_statuses.key(raw_status)
- end
-
def self.counts_by_status
group(:status).count.transform_keys { |k| status_name(k) }
end
- def self.status_names
- @status_names ||= state_machine_statuses.keys
- end
-
def self.sort_by_attribute(method)
case method.to_s
when 'started_at_asc' then order_start_time(:asc)
@@ -229,15 +155,6 @@ module AlertManagement
self.class.open_status?(status_name)
end
- def status_event_for(status)
- self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
- end
-
- def change_status_to(new_status)
- event = status_event_for(new_status)
- event && fire_status_event(event)
- end
-
def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
diff --git a/app/models/analytics/cycle_analytics/stage_event_hash.rb b/app/models/analytics/cycle_analytics/stage_event_hash.rb
new file mode 100644
index 00000000000..0e1e9b3ef67
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/stage_event_hash.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class StageEventHash < ApplicationRecord
+ has_many :cycle_analytics_project_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :stage_event_hash
+
+ validates :hash_sha256, presence: true
+
+ # Creates or queries the id of the corresponding stage event hash code
+ def self.record_id_by_hash_sha256(hash)
+ casted_hash_code = Arel::Nodes.build_quoted(hash, Analytics::CycleAnalytics::StageEventHash.arel_table[:hash_sha256]).to_sql
+
+ # Atomic, safe insert without retrying
+ query = <<~SQL
+ WITH insert_cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
+ INSERT INTO #{quoted_table_name} (hash_sha256) VALUES (#{casted_hash_code}) ON CONFLICT DO NOTHING RETURNING ID
+ )
+ SELECT ids.id FROM (
+ (SELECT id FROM #{quoted_table_name} WHERE hash_sha256=#{casted_hash_code} LIMIT 1)
+ UNION ALL
+ (SELECT id FROM insert_cte LIMIT 1)
+ ) AS ids LIMIT 1
+ SQL
+
+ connection.execute(query).first['id']
+ end
+
+ def self.cleanup_if_unused(id)
+ unused_hashes_for(id)
+ .where(id: id)
+ .delete_all
+ end
+
+ def self.unused_hashes_for(id)
+ exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1)
+ where.not('EXISTS (?)', exists_query)
+ end
+ end
+ end
+end
+Analytics::CycleAnalytics::StageEventHash.prepend_mod_with('Analytics::CycleAnalytics::StageEventHash')
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 527b67712ee..d9375b55e89 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -63,11 +63,27 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_find_or_create_by(*args, &block)
+ return optimized_safe_find_or_create_by(*args, &block) if Feature.enabled?(:optimize_safe_find_or_create_by, default_enabled: :yaml)
+
safe_ensure_unique(retries: 1) do
find_or_create_by(*args, &block)
end
end
+ def self.optimized_safe_find_or_create_by(*args, &block)
+ record = find_by(*args)
+ return record if record.present?
+
+ # We need to use `all.create` to make this implementation follow `find_or_create_by` which delegates this in
+ # https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/querying.rb#L22
+ #
+ # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
+ # and that skips some code that adds the newly created record to the association.
+ transaction(requires_new: true) { all.create(*args, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by(*args)
+ end
+
def create_or_load_association(association_name)
association(association_name).create unless association(association_name).loaded?
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
@@ -87,6 +103,23 @@ class ApplicationRecord < ActiveRecord::Base
enum(enum_mod.key => values)
end
+ def self.transaction(**options, &block)
+ if options[:requires_new] && track_subtransactions?
+ ::Gitlab::Database::Metrics.subtransactions_increment(self.name)
+ end
+
+ super(**options, &block)
+ end
+
+ def self.track_subtransactions?
+ ::Feature.enabled?(:active_record_subtransactions_counter, type: :ops, default_enabled: :yaml) &&
+ connection.transaction_open?
+ end
+
+ def self.cached_column_list
+ self.column_names.map { |column_name| self.arel_table[column_name] }
+ end
+
def readable_by?(user)
Ability.allowed?(user, "read_#{to_ability_name}".to_sym, self)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a7140cc0718..c4b6bcb9395 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,6 +5,11 @@ class ApplicationSetting < ApplicationRecord
include CacheMarkdownField
include TokenAuthenticatable
include ChronicDurationAttribute
+ include IgnorableColumns
+
+ ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
+ ignore_column :seat_link_enabled, remove_with: '14.4', remove_after: '2021-09-22'
+ ignore_column :cloud_license_enabled, remove_with: '14.4', remove_after: '2021-09-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -30,7 +35,7 @@ class ApplicationSetting < ApplicationRecord
def self.kroki_formats_attributes
{
blockdiag: {
- label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)'
+ label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, and RackDiag)'
},
bpmn: {
label: 'BPMN'
@@ -451,6 +456,9 @@ class ApplicationSetting < ApplicationRecord
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
+ validates :customers_dot_jwt_signing_key,
+ rsa_key: true, allow_nil: true
+
validates :rate_limiting_response_text,
length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
allow_blank: true
@@ -554,6 +562,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :slack_app_secret, encryption_options_base_32_aes_256_gcm
attr_encrypted :slack_app_verification_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :customers_dot_jwt_signing_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :cloud_license_auth_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_32_aes_256_gcm
@@ -564,6 +573,7 @@ class ApplicationSetting < ApplicationRecord
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
+ before_validation :sanitize_default_branch_name
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -593,6 +603,14 @@ class ApplicationSetting < ApplicationRecord
!!(sourcegraph_url =~ %r{\Ahttps://(www\.)?sourcegraph\.com})
end
+ def sanitize_default_branch_name
+ self.default_branch_name = if default_branch_name.blank?
+ nil
+ else
+ Sanitize.fragment(self.default_branch_name)
+ end
+ end
+
def instance_review_permitted?
users_count = Rails.cache.fetch('limited_users_count', expires_in: 1.day) do
::User.limit(INSTANCE_REVIEW_MIN_USERS + 1).count(:all)
@@ -627,7 +645,7 @@ class ApplicationSetting < ApplicationRecord
# prevent this from happening, we do a sanity check that the
# primary key constraint is present before inserting a new entry.
def self.check_schema!
- return if ActiveRecord::Base.connection.primary_key(self.table_name).present?
+ return if connection.primary_key(self.table_name).present?
raise "The `#{self.table_name}` table is missing a primary key constraint in the database schema"
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index d7a594af84c..060c831a11b 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -219,11 +219,11 @@ module ApplicationSettingImplementation
end
def home_page_url_column_exists?
- ::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url)
+ ::Gitlab::Database.main.cached_column_exists?(:application_settings, :home_page_url)
end
def help_page_support_url_column_exists?
- ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url)
+ ::Gitlab::Database.main.cached_column_exists?(:application_settings, :help_page_support_url)
end
def disabled_oauth_sign_in_sources=(sources)
diff --git a/app/models/ci/application_record.rb b/app/models/ci/application_record.rb
new file mode 100644
index 00000000000..9d4a8f0648e
--- /dev/null
+++ b/app/models/ci/application_record.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Ci
+ class ApplicationRecord < ::ApplicationRecord
+ self.abstract_class = true
+
+ def self.table_name_prefix
+ 'ci_'
+ end
+
+ def self.model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.demodulize)
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4328f3f7a4b..1ca291a659b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -39,7 +39,6 @@ module Ci
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id
- has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
@@ -54,6 +53,7 @@ module Ci
end
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
+ has_one :trace_metadata, class_name: 'Ci::BuildTraceMetadata', inverse_of: :build
accepts_nested_attributes_for :runner_session, update_only: true
accepts_nested_attributes_for :job_variables
@@ -103,7 +103,6 @@ module Ci
end
scope :unstarted, -> { where(runner_id: nil) }
- scope :ignore_failures, -> { where(allow_failure: false) }
scope :with_downloadable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
@@ -120,10 +119,6 @@ module Ci
where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
end
- scope :with_archived_trace, -> do
- with_existing_job_artifacts(Ci::JobArtifact.trace)
- end
-
scope :without_archived_trace, -> do
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
@@ -134,7 +129,6 @@ module Ci
end
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
- scope :eager_load_job_artifacts_archive, -> { includes(:job_artifacts_archive) }
scope :eager_load_tags, -> { includes(:tags) }
scope :eager_load_everything, -> do
@@ -158,7 +152,7 @@ module Ci
scope :with_project_and_metadata, -> do
if Feature.enabled?(:non_public_artifacts, type: :development)
- joins(:metadata).includes(:project, :metadata)
+ joins(:metadata).includes(:metadata).preload(:project)
end
end
@@ -466,13 +460,9 @@ module Ci
end
def retryable?
- if Feature.enabled?(:prevent_retry_of_retried_jobs, project, default_enabled: :yaml)
- return false if retried? || archived?
+ return false if retried? || archived?
- success? || failed? || canceled?
- else
- !archived? && (success? || failed? || canceled?)
- end
+ success? || failed? || canceled?
end
def retries_count
@@ -559,6 +549,7 @@ module Ci
.concat(persisted_variables)
.concat(dependency_proxy_variables)
.concat(job_jwt_variables)
+ .concat(kubernetes_variables)
.concat(scoped_variables)
.concat(job_variables)
.concat(persisted_environment_variables)
@@ -648,12 +639,6 @@ module Ci
update(coverage: coverage) if coverage.present?
end
- # rubocop: disable CodeReuse/ServiceClass
- def parse_trace_sections!
- ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
- end
- # rubocop: enable CodeReuse/ServiceClass
-
def trace
Gitlab::Ci::Trace.new(self)
end
@@ -907,7 +892,7 @@ module Ci
end
def valid_dependency?
- return false if artifacts_expired?
+ return false if artifacts_expired? && !pipeline.artifacts_locked?
return false if erased?
true
@@ -1183,6 +1168,10 @@ module Ci
end
end
+ def kubernetes_variables
+ [] # Overridden in EE
+ end
+
def conditionally_allow_failure!(exit_code)
return unless exit_code
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 50775f578f0..90237a4be52 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -3,10 +3,9 @@
module Ci
# The purpose of this class is to store Build related data that can be disposed.
# Data that should be persisted forever, should be stored with Ci::Build model.
- class BuildMetadata < ApplicationRecord
+ class BuildMetadata < Ci::ApplicationRecord
BuildTimeout = Struct.new(:value, :source)
- extend Gitlab::Ci::Model
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 4a59c25cbb0..003659570b3 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class BuildNeed < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class BuildNeed < Ci::ApplicationRecord
include BulkInsertSafe
include IgnorableColumns
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 299c67f441d..53cf0697e2e 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-class Ci::BuildPendingState < ApplicationRecord
- extend Gitlab::Ci::Model
-
+class Ci::BuildPendingState < Ci::ApplicationRecord
belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id
enum state: Ci::Stage.statuses
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index eb6a0700006..2c08fc4c8bf 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class BuildReportResult < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class BuildReportResult < Ci::ApplicationRecord
self.primary_key = :build_id
belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 2aa856dbc64..45de47116cd 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -3,8 +3,7 @@
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 < ApplicationRecord
- extend Gitlab::Ci::Model
+ class BuildRunnerSession < Ci::ApplicationRecord
include IgnorableColumns
ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 3fa9a484b0c..7a15d7ba940 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class BuildTraceChunk < ApplicationRecord
- extend ::Gitlab::Ci::Model
+ class BuildTraceChunk < Ci::ApplicationRecord
include ::Comparable
include ::FastDestroyAll
include ::Checksummable
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
new file mode 100644
index 00000000000..05bdb3d8b7b
--- /dev/null
+++ b/app/models/ci/build_trace_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildTraceMetadata < Ci::ApplicationRecord
+ self.table_name = 'ci_build_trace_metadata'
+ self.primary_key = :build_id
+
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :trace_artifact, class_name: 'Ci::JobArtifact'
+
+ validates :build, presence: true
+ end
+end
diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb
deleted file mode 100644
index 036f611a61c..00000000000
--- a/app/models/ci/build_trace_section.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class BuildTraceSection < ApplicationRecord
- extend SuppressCompositePrimaryKeyWarning
- extend Gitlab::Ci::Model
- include IgnorableColumns
-
- belongs_to :build, class_name: 'Ci::Build'
- belongs_to :project
- belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName'
-
- validates :section_name, :build, :project, presence: true, allow_blank: false
-
- ignore_column :build_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
- end
-end
diff --git a/app/models/ci/build_trace_section_name.rb b/app/models/ci/build_trace_section_name.rb
deleted file mode 100644
index c065cfea14e..00000000000
--- a/app/models/ci/build_trace_section_name.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class BuildTraceSectionName < ApplicationRecord
- extend Gitlab::Ci::Model
-
- belongs_to :project
- has_many :trace_sections, class_name: 'Ci::BuildTraceSection', foreign_key: :section_name_id
-
- validates :name, :project, presence: true, allow_blank: false
- validates :name, uniqueness: { scope: :project_id }
- end
-end
diff --git a/app/models/ci/base_model.rb b/app/models/ci/ci_database_record.rb
index 8fb752ead1d..e2b832a28e7 100644
--- a/app/models/ci/base_model.rb
+++ b/app/models/ci/ci_database_record.rb
@@ -7,7 +7,7 @@ module Ci
# This class is part of a migration to move all CI classes to a new separate database.
# Initially we are only going to be moving the `Ci::InstanceVariable` model and it will be duplicated in the main and CI tables
# Do not extend this class in any other models.
- class BaseModel < ::ApplicationRecord
+ class CiDatabaseRecord < Ci::ApplicationRecord
self.abstract_class = true
if Gitlab::Database.has_config?(:ci)
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index b46d32474c6..598d1456a48 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class DailyBuildGroupReportResult < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class DailyBuildGroupReportResult < Ci::ApplicationRecord
PARAM_TYPES = %w[coverage].freeze
belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
diff --git a/app/models/ci/deleted_object.rb b/app/models/ci/deleted_object.rb
index b2a949c9bb5..aba7b73aba9 100644
--- a/app/models/ci/deleted_object.rb
+++ b/app/models/ci/deleted_object.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class DeletedObject < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class DeletedObject < Ci::ApplicationRecord
mount_uploader :file, DeletedObjectUploader
scope :ready_for_destruction, ->(limit) do
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index d215372bb45..da0bbbacddd 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
module Ci
- class FreezePeriod < ApplicationRecord
+ class FreezePeriod < Ci::ApplicationRecord
include StripAttribute
- self.table_name = 'ci_freeze_periods'
+ include Ci::NamespacedModelName
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
belongs_to :project, inverse_of: :freeze_periods
- strip_attributes :freeze_start, :freeze_end
+ strip_attributes! :freeze_start, :freeze_end
validates :freeze_start, cron: true, presence: true
validates :freeze_end, cron: true, presence: true
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 2928ce801ad..165bee5c54d 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class GroupVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class GroupVariable < Ci::ApplicationRecord
include Ci::HasVariable
include Presentable
include Ci::Maskable
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 5aee4c924af..f4aa935b983 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class InstanceVariable < ::Ci::BaseModel
- extend Gitlab::Ci::Model
+ class InstanceVariable < Ci::CiDatabaseRecord
extend Gitlab::ProcessMemoryCache::Helper
include Ci::NewHasVariable
include Ci::Maskable
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 46c976d5616..1f0da4345f2 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Ci
- class JobArtifact < ApplicationRecord
+ class JobArtifact < Ci::ApplicationRecord
include AfterCommitQueue
include ObjectStorage::BackgroundMove
include UpdateProjectStatistics
@@ -10,7 +10,6 @@ module Ci
include Artifactable
include FileStoreMounter
include EachBatch
- extend Gitlab::Ci::Model
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index 7eea8a37150..44bd3fe8901 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class JobVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class JobVariable < Ci::ApplicationRecord
include Ci::NewHasVariable
include BulkInsertSafe
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 0663052f51d..7cf3a387516 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -1,14 +1,16 @@
# frozen_string_literal: true
module Ci
- class PendingBuild < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class PendingBuild < Ci::ApplicationRecord
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :namespace, inverse_of: :pending_builds, class_name: 'Namespace'
+
+ validates :namespace, presence: true
scope :ref_protected, -> { where(protected: true) }
scope :queued_before, ->(time) { where(arel_table[:created_at].lt(time)) }
+ scope :with_instance_runners, -> { where(instance_runners_enabled: true) }
def self.upsert_from_build!(build)
entry = self.new(args_from_build(build))
@@ -22,7 +24,8 @@ module Ci
args = {
build: build,
project: build.project,
- protected: build.protected?
+ protected: build.protected?,
+ namespace: build.project.namespace
}
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
@@ -56,3 +59,5 @@ module Ci
private_class_method :builds_access_level?
end
end
+
+Ci::PendingBuild.prepend_mod_with('Ci::PendingBuild')
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5d079f57267..70e67953e31 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Pipeline < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Pipeline < Ci::ApplicationRecord
include Ci::HasStatus
include Importable
include AfterCommitQueue
@@ -319,6 +318,7 @@ module Ci
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
scope :eager_load_project, -> { eager_load(project: [:route, { namespace: :route }]) }
+ scope :with_pipeline_source, -> (source) { where(source: source)}
scope :outside_pipeline_family, ->(pipeline) do
where.not(id: pipeline.same_family_pipeline_ids)
@@ -378,11 +378,15 @@ module Ci
end
def self.latest_successful_for_refs(refs)
- relation = newest_first(ref: refs).success
+ return Ci::Pipeline.none if refs.empty?
- relation.each_with_object({}) do |pipeline, hash|
- hash[pipeline.ref] ||= pipeline
- end
+ refs_values = refs.map { |ref| "(#{connection.quote(ref)})" }.join(",")
+ join_query = success.where("refs_values.ref = ci_pipelines.ref").order(id: :desc).limit(1)
+
+ Ci::Pipeline
+ .from("(VALUES #{refs_values}) refs_values (ref)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{Ci::Pipeline.table_name} ON TRUE")
+ .index_by(&:ref)
end
def self.latest_running_for_ref(ref)
@@ -393,6 +397,10 @@ module Ci
newest_first(ref: ref).failed.take
end
+ def self.jobs_count_in_alive_pipelines
+ created_after(24.hours.ago).alive.joins(:builds).count
+ end
+
# Returns a Hash containing the latest pipeline for every given
# commit.
#
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 889c5d094a7..2284a05bcc9 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -3,8 +3,7 @@
# This class is being used to persist additional artifacts after a pipeline completes, which is a great place to cache a computed result in object storage
module Ci
- class PipelineArtifact < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineArtifact < Ci::ApplicationRecord
include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
diff --git a/app/models/ci/pipeline_chat_data.rb b/app/models/ci/pipeline_chat_data.rb
index 65466a8c6f8..ba20c993e36 100644
--- a/app/models/ci/pipeline_chat_data.rb
+++ b/app/models/ci/pipeline_chat_data.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Ci
- class PipelineChatData < ApplicationRecord
+ class PipelineChatData < Ci::ApplicationRecord
+ include Ci::NamespacedModelName
+
self.table_name = 'ci_pipeline_chat_data'
belongs_to :chat_name
diff --git a/app/models/ci/pipeline_config.rb b/app/models/ci/pipeline_config.rb
index d5a8da2bc1e..e2dcad653d7 100644
--- a/app/models/ci/pipeline_config.rb
+++ b/app/models/ci/pipeline_config.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineConfig < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class PipelineConfig < Ci::ApplicationRecord
self.table_name = 'ci_pipelines_config'
self.primary_key = :pipeline_id
diff --git a/app/models/ci/pipeline_message.rb b/app/models/ci/pipeline_message.rb
index a47ec554462..5668da915e6 100644
--- a/app/models/ci/pipeline_message.rb
+++ b/app/models/ci/pipeline_message.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineMessage < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class PipelineMessage < Ci::ApplicationRecord
MAX_CONTENT_LENGTH = 10_000
belongs_to :pipeline
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index effe2d95a99..b915495ac38 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineSchedule < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineSchedule < Ci::ApplicationRecord
extend ::Gitlab::Utils::Override
include Importable
include StripAttribute
@@ -25,7 +24,7 @@ module Ci
validates :description, presence: true
validates :variables, nested_attributes_duplicates: true
- strip_attributes :cron
+ strip_attributes! :cron
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index adef9911ae1..84a24609cc7 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineScheduleVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineScheduleVariable < Ci::ApplicationRecord
include Ci::HasVariable
belongs_to :pipeline_schedule
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 84ca4833cd7..a0e8886414b 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineVariable < Ci::ApplicationRecord
include Ci::HasVariable
belongs_to :pipeline
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index e2f257eab25..30d335fd7d5 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -169,11 +169,7 @@ module Ci
end
def all_dependencies
- if Feature.enabled?(:preload_associations_jobs_request_api_endpoint, project, default_enabled: :yaml)
- strong_memoize(:all_dependencies) do
- dependencies.all
- end
- else
+ strong_memoize(:all_dependencies) do
dependencies.all
end
end
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 3d71a5f2c96..af5fdabff6e 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Ref < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Ref < Ci::ApplicationRecord
include AfterCommitQueue
include Gitlab::OptimisticLocking
diff --git a/app/models/ci/resource.rb b/app/models/ci/resource.rb
index e0e1fab642d..ee094fa2007 100644
--- a/app/models/ci/resource.rb
+++ b/app/models/ci/resource.rb
@@ -1,13 +1,26 @@
# frozen_string_literal: true
module Ci
- class Resource < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class Resource < Ci::ApplicationRecord
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources
belongs_to :processable, class_name: 'Ci::Processable', foreign_key: 'build_id', inverse_of: :resource
scope :free, -> { where(processable: nil) }
+ scope :retained, -> { where.not(processable: nil) }
scope :retained_by, -> (processable) { where(processable: processable) }
+
+ class << self
+ # In some cases, state machine hooks in `Ci::Build` are skipped
+ # even if the job status transitions to a complete state.
+ # For example, `Ci::Build#doom!` (a.k.a `data_integrity_failure`) doesn't execute state machine hooks.
+ # To handle these edge cases, we check the staleness of the jobs that currently
+ # assigned to the resources, and release if it's stale.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/335537#note_632925914 for more information.
+ def stale_processables
+ Ci::Processable.where(id: retained.select(:build_id))
+ .complete
+ .updated_at_before(5.minutes.ago)
+ end
+ end
end
end
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index 85fbe03e1c9..8a7456041e6 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class ResourceGroup < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class ResourceGroup < Ci::ApplicationRecord
belongs_to :project, inverse_of: :resource_groups
has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index a541dca47de..432c3a408a9 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Runner < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Runner < Ci::ApplicationRecord
include Gitlab::SQL::Pattern
include RedisCacheable
include ChronicDurationAttribute
@@ -12,6 +11,7 @@ module Ci
include FeatureGate
include Gitlab::Utils::StrongMemoize
include TaggableQueries
+ include Presentable
add_authentication_token_field :token, encrypted: :optional
@@ -61,13 +61,7 @@ module Ci
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', online_contact_time_deadline) }
scope :recent, -> { where('ci_runners.created_at > :date OR ci_runners.contacted_at > :date', date: 3.months.ago) }
- # The following query using negation is cheaper than using `contacted_at <= ?`
- # because there are less runners online than have been created. The
- # resulting query is quickly finding online ones and then uses the regular
- # indexed search and rejects the ones that are in the previous set. If we
- # did `contacted_at <= ?` the query would effectively have to do a seq
- # scan.
- scope :offline, -> { where.not(id: online) }
+ scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
scope :not_connected, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index 41a4c9012ff..d1353b97ed9 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
module Ci
- class RunnerNamespace < ApplicationRecord
- extend Gitlab::Ci::Model
+ class RunnerNamespace < Ci::ApplicationRecord
include Limitable
self.limit_name = 'ci_registered_group_runners'
self.limit_scope = :group
self.limit_relation = :recent_runners
self.limit_feature_flag = :ci_runner_limits
+ self.limit_feature_flag_for_override = :ci_runner_limits_override
belongs_to :runner, inverse_of: :runner_namespaces
belongs_to :namespace, inverse_of: :runner_namespaces, class_name: '::Namespace'
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index af2595ce4af..e1c435e9b1f 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
module Ci
- class RunnerProject < ApplicationRecord
- extend Gitlab::Ci::Model
+ class RunnerProject < Ci::ApplicationRecord
include Limitable
self.limit_name = 'ci_registered_project_runners'
self.limit_scope = :project
self.limit_relation = :recent_runners
self.limit_feature_flag = :ci_runner_limits
+ self.limit_feature_flag_for_override = :ci_runner_limits_override
belongs_to :runner, inverse_of: :runner_projects
belongs_to :project, inverse_of: :runner_projects
diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb
index 9446cfa05da..ae38d54862d 100644
--- a/app/models/ci/running_build.rb
+++ b/app/models/ci/running_build.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class RunningBuild < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class RunningBuild < Ci::ApplicationRecord
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :runner, class_name: 'Ci::Runner'
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index f19aac213be..f78caf710a6 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -2,7 +2,9 @@
module Ci
module Sources
- class Pipeline < ApplicationRecord
+ class Pipeline < Ci::ApplicationRecord
+ include Ci::NamespacedModelName
+
self.table_name = "ci_sources_pipelines"
belongs_to :project, class_name: "Project"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d00066b778d..39e26bf2785 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Stage < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Stage < Ci::ApplicationRecord
include Importable
include Ci::HasStatus
include Gitlab::OptimisticLocking
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 6e27abb9f5b..595315f14ab 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Trigger < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Trigger < Ci::ApplicationRecord
include Presentable
belongs_to :project
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 5daf3dd192d..b645f7ee2bb 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class TriggerRequest < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class TriggerRequest < Ci::ApplicationRecord
belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id
has_many :builds
diff --git a/app/models/ci/unit_test.rb b/app/models/ci/unit_test.rb
index 9fddd9c6002..96b701840ea 100644
--- a/app/models/ci/unit_test.rb
+++ b/app/models/ci/unit_test.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class UnitTest < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class UnitTest < Ci::ApplicationRecord
MAX_NAME_SIZE = 255
MAX_SUITE_NAME_SIZE = 255
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index 480f9cefb8e..a5aa3b70e37 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class UnitTestFailure < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class UnitTestFailure < Ci::ApplicationRecord
REPORT_WINDOW = 14.days
validates :unit_test, :build, :failed_at, presence: true
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 84505befc5c..1e91f248fc4 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Variable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Variable < Ci::ApplicationRecord
include Ci::HasVariable
include Presentable
include Ci::Maskable
diff --git a/app/models/ci_platform_metric.rb b/app/models/ci_platform_metric.rb
index ac4ab391bbf..db6b73b43f7 100644
--- a/app/models/ci_platform_metric.rb
+++ b/app/models/ci_platform_metric.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
-class CiPlatformMetric < ApplicationRecord
+class CiPlatformMetric < Ci::ApplicationRecord
include BulkInsertSafe
+ self.table_name = 'ci_platform_metrics'
+
PLATFORM_TARGET_MAX_LENGTH = 255
validates :recorded_at, presence: true
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 3785023c9af..993ccb33655 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.30.0'
+ VERSION = '0.31.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 8e7f526c512..6c8b4ae1139 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -3,6 +3,7 @@
class Commit
extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache
+ extend Gitlab::Utils::Override
include ActiveModel::Conversion
include Noteable
@@ -327,7 +328,7 @@ class Commit
end
def user_mentions
- CommitUserMention.where(commit_id: self.id)
+ user_mention_class.where(commit_id: self.id)
end
def discussion_notes
@@ -554,6 +555,19 @@ class Commit
Ability.allowed?(user, :read_commit, self)
end
+ override :user_mention_class
+ def user_mention_class
+ CommitUserMention
+ end
+
+ override :user_mention_identifier
+ def user_mention_identifier
+ {
+ commit_id: id,
+ note_id: nil
+ }
+ end
+
private
def expire_note_etag_cache_for_related_mrs
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cf23cd3be67..b34d64de101 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class CommitStatus < ApplicationRecord
+class CommitStatus < Ci::ApplicationRecord
include Ci::HasStatus
include Importable
include AfterCommitQueue
@@ -58,6 +58,7 @@ class CommitStatus < ApplicationRecord
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :eager_load_pipeline, -> { eager_load(:pipeline, project: { namespace: :route }) }
scope :with_pipeline, -> { joins(:pipeline) }
+ scope :updated_at_before, ->(date) { where('updated_at < ?', date) }
scope :updated_before, ->(lookback:, timeout:) {
where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
}
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 2a0274f5706..7bb6004ca83 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -10,6 +10,7 @@ module Analytics
included do
belongs_to :start_event_label, class_name: 'GroupLabel', optional: true
belongs_to :end_event_label, class_name: 'GroupLabel', optional: true
+ belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', foreign_key: :stage_event_hash_id, optional: true
validates :name, presence: true
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
@@ -28,6 +29,9 @@ module Analytics
scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
+
+ before_save :ensure_stage_event_hash_id
+ after_commit :cleanup_old_stage_event_hash
end
def parent=(_)
@@ -133,6 +137,20 @@ module Analytics
.id_in(label_id)
.exists?
end
+
+ def ensure_stage_event_hash_id
+ previous_stage_event_hash = stage_event_hash&.hash_sha256
+
+ if previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash
+ self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code)
+ end
+ end
+
+ def cleanup_old_stage_event_hash
+ if stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was
+ Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was)
+ end
+ end
end
end
end
diff --git a/app/models/concerns/any_field_validation.rb b/app/models/concerns/any_field_validation.rb
deleted file mode 100644
index 987c4e7800e..00000000000
--- a/app/models/concerns/any_field_validation.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-# This module enables a record to be valid if any field is present
-#
-# Overwrite one_of_required_fields to set one of which fields must be present
-module AnyFieldValidation
- extend ActiveSupport::Concern
-
- included do
- validate :any_field_present
- end
-
- private
-
- def any_field_present
- return unless one_of_required_fields.all? { |field| self[field].blank? }
-
- errors.add(:base, _("At least one field of %{one_of_required_fields} must be present") %
- { one_of_required_fields: one_of_required_fields })
- end
-
- def one_of_required_fields
- raise NotImplementedError
- end
-end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 79b622c8dad..44d9beff27e 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -160,6 +160,8 @@ module CacheMarkdownField
# We can only store mentions if the mentionable is a database object
return unless self.is_a?(ApplicationRecord)
+ return store_mentions_without_subtransaction! if Feature.enabled?(:store_mentions_without_subtransaction, default_enabled: :yaml)
+
refs = all_references(self.author)
references = {}
@@ -190,6 +192,29 @@ module CacheMarkdownField
true
end
+ def store_mentions_without_subtransaction!
+ identifier = user_mention_identifier
+
+ # this may happen due to notes polymorphism, so noteable_id may point to a record
+ # that no longer exists as we cannot have FK on noteable_id
+ return if identifier.blank?
+
+ refs = all_references(self.author)
+
+ references = {}
+ references[:mentioned_users_ids] = refs.mentioned_user_ids.presence
+ references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
+ references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence
+
+ if references.compact.any?
+ user_mention_class.upsert(references.merge(identifier), unique_by: identifier.compact.keys)
+ else
+ user_mention_class.delete_by(identifier)
+ end
+
+ true
+ end
+
def mentionable_attributes_changed?(changes = saved_changes)
return false unless is_a?(Mentionable)
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index 5d24e15d518..e58e5ddc966 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -127,7 +127,7 @@ module CascadingNamespaceSettingAttribute
end
def alias_boolean(attribute)
- return unless Gitlab::Database.exists? && type_for_attribute(attribute).type == :boolean
+ return unless Gitlab::Database.main.exists? && type_for_attribute(attribute).type == :boolean
alias_method :"#{attribute}?", attribute
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index f3c254053b5..c1299e3d468 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -93,6 +93,7 @@ module Ci
scope :running_or_pending, -> { with_status(:running, :pending) }
scope :finished, -> { with_status(:success, :failed, :canceled) }
scope :failed_or_canceled, -> { with_status(:failed, :canceled) }
+ scope :complete, -> { with_status(completed_statuses) }
scope :incomplete, -> { without_statuses(completed_statuses) }
scope :cancelable, -> do
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 114435d5a21..ec86746ae54 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -76,14 +76,8 @@ module Ci
end
def write_metadata_attribute(legacy_key, metadata_key, value)
- # save to metadata or this model depending on the state of feature flag
- if Feature.enabled?(:ci_build_metadata_config, project, default_enabled: :yaml)
- ensure_metadata.write_attribute(metadata_key, value)
- write_attribute(legacy_key, nil)
- else
- write_attribute(legacy_key, value)
- metadata&.write_attribute(metadata_key, nil)
- end
+ ensure_metadata.write_attribute(metadata_key, value)
+ write_attribute(legacy_key, nil)
end
end
end
diff --git a/app/models/concerns/ci/namespaced_model_name.rb b/app/models/concerns/ci/namespaced_model_name.rb
new file mode 100644
index 00000000000..e941a3a7a0c
--- /dev/null
+++ b/app/models/concerns/ci/namespaced_model_name.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ module NamespacedModelName
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, Ci)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 829b2a6ef21..4bfeba338d2 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -128,8 +128,7 @@ module CounterAttribute
end
def counter_attribute_enabled?(attribute)
- Feature.enabled?(:efficient_counter_attribute, project) &&
- self.class.counter_attributes.include?(attribute)
+ self.class.counter_attributes.include?(attribute)
end
private
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index a59f00d73ec..443e1ab53b4 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -91,7 +91,11 @@ module EachBatch
# Any ORDER BYs are useless for this relation and can lead to less
# efficient UPDATE queries, hence we get rid of it.
- yield relation.except(:order), index
+ relation = relation.except(:order)
+
+ # Using unscoped is necessary to prevent leaking the current scope used by
+ # ActiveRecord to chain `each_batch` method.
+ unscoped { yield relation, index }
break unless stop
end
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index c42b046592f..94d11c871ca 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -37,7 +37,9 @@ module Enums
merge_request_event: 10,
external_pull_request_event: 11,
parent_pipeline: 12,
- ondemand_dast_scan: 13
+ ondemand_dast_scan: 13,
+ ondemand_dast_validation: 14,
+ security_orchestration_policy: 15
}
end
@@ -48,8 +50,10 @@ module Enums
# parent pipeline. It's up to the parent to affect the ref CI status
# - when an ondemand_dast_scan pipeline runs it is for testing purpose and should
# not affect the ref CI status.
+ # - when an ondemand_dast_validation pipeline runs it is for validating a DAST site
+ # profile and should not affect the ref CI status.
def self.dangling_sources
- sources.slice(:webide, :parent_pipeline, :ondemand_dast_scan)
+ sources.slice(:webide, :parent_pipeline, :ondemand_dast_scan, :ondemand_dast_validation, :security_orchestration_policy)
end
# CI sources are those pipeline events that affect the CI status of the ref
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
index 512822089ba..e029ada84f0 100644
--- a/app/models/concerns/expirable.rb
+++ b/app/models/concerns/expirable.rb
@@ -13,6 +13,9 @@ module Expirable
expires? && expires_at <= Time.current
end
+ # Used in subclasses that override expired?
+ alias_method :expired_original?, :expired?
+
def expires?
expires_at.present?
end
diff --git a/app/models/concerns/has_integrations.rb b/app/models/concerns/has_integrations.rb
index 25650ae56ad..76e03d68600 100644
--- a/app/models/concerns/has_integrations.rb
+++ b/app/models/concerns/has_integrations.rb
@@ -4,18 +4,6 @@ module HasIntegrations
extend ActiveSupport::Concern
class_methods do
- def with_custom_integration_for(integration, page = nil, per = nil)
- custom_integration_project_ids = Integration
- .select(:project_id)
- .where(type: integration.type)
- .where(inherit_from_id: nil)
- .where.not(project_id: nil)
- .page(page)
- .per(per)
-
- Project.where(id: custom_integration_project_ids)
- end
-
def without_integration(integration)
integrations = Integration
.select('1')
diff --git a/app/models/concerns/incident_management/escalatable.rb b/app/models/concerns/incident_management/escalatable.rb
new file mode 100644
index 00000000000..78dce63f59e
--- /dev/null
+++ b/app/models/concerns/incident_management/escalatable.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ # Shared functionality for a `#status` field, representing
+ # whether action is required. In EE, this corresponds
+ # to paging functionality with EscalationPolicies.
+ #
+ # This module is only responsible for setting the status and
+ # possible status-related timestamps (EX triggered_at/resolved_at)
+ # for the implementing class. The relationships between these
+ # values and other related timestamps/logic should be managed from
+ # the object class itself. (EX Alert#ended_at = Alert#resolved_at)
+ module Escalatable
+ extend ActiveSupport::Concern
+
+ STATUSES = {
+ triggered: 0,
+ acknowledged: 1,
+ resolved: 2,
+ ignored: 3
+ }.freeze
+
+ STATUS_DESCRIPTIONS = {
+ triggered: 'Investigation has not started',
+ acknowledged: 'Someone is actively investigating the problem',
+ resolved: 'The problem has been addressed',
+ ignored: 'No action will be taken'
+ }.freeze
+
+ included do
+ validates :status, presence: true
+
+ # Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
+ # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
+ scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
+
+ state_machine :status, initial: :triggered do
+ state :triggered, value: STATUSES[:triggered]
+
+ state :acknowledged, value: STATUSES[:acknowledged]
+
+ state :resolved, value: STATUSES[:resolved] do
+ validates :resolved_at, presence: true
+ end
+
+ state :ignored, value: STATUSES[:ignored]
+
+ state :triggered, :acknowledged, :ignored do
+ validates :resolved_at, absence: true
+ end
+
+ event :trigger do
+ transition any => :triggered
+ end
+
+ event :acknowledge do
+ transition any => :acknowledged
+ end
+
+ event :resolve do
+ transition any => :resolved
+ end
+
+ event :ignore do
+ transition any => :ignored
+ end
+
+ before_transition to: [:triggered, :acknowledged, :ignored] do |escalatable, _transition|
+ escalatable.resolved_at = nil
+ end
+
+ before_transition to: :resolved do |escalatable, transition|
+ resolved_at = transition.args.first
+ escalatable.resolved_at = resolved_at || Time.current
+ end
+ end
+
+ class << self
+ def status_value(name)
+ state_machine_statuses[name]
+ end
+
+ def status_name(raw_status)
+ state_machine_statuses.key(raw_status)
+ end
+
+ def status_names
+ @status_names ||= state_machine_statuses.keys
+ end
+
+ private
+
+ def state_machine_statuses
+ @state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
+ end
+ end
+
+ def status_event_for(status)
+ self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d5e2e63402f..8d0f8b01d64 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -152,7 +152,7 @@ module Issuable
participant :notes_with_associations
participant :assignees
- strip_attributes :title
+ strip_attributes! :title
class << self
def labels_hash
@@ -374,6 +374,8 @@ module Issuable
grouping_columns << milestone_table[:due_date]
elsif %w(merged_at_desc merged_at_asc).include?(sort)
grouping_columns << MergeRequest::Metrics.arel_table[:merged_at]
+ elsif %w(closed_at_desc closed_at_asc).include?(sort)
+ grouping_columns << MergeRequest::Metrics.arel_table[:closed_at]
end
grouping_columns
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index 41efea65c5a..fab1aa21634 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -9,6 +9,7 @@ module Limitable
class_attribute :limit_relation
class_attribute :limit_name
class_attribute :limit_feature_flag
+ class_attribute :limit_feature_flag_for_override # Allows selectively disabling by actor (as per https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor)
self.limit_name = self.name.demodulize.tableize
validate :validate_plan_limit_not_exceeded, on: :create
@@ -28,6 +29,7 @@ module Limitable
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation, default_enabled: :yaml)
+ return if limit_feature_flag_for_override && ::Feature.enabled?(limit_feature_flag_for_override, scope_relation, default_enabled: :yaml)
relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
limits = scope_relation.actual_limits
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index f1baa923ec5..4df9e32d8ec 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -161,6 +161,21 @@ module Mentionable
create_cross_references!(author)
end
+ def user_mention_class
+ user_mention_association.klass
+ end
+
+ # Identifier for the user mention that is parsed from model description rather then its related notes.
+ # Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
+ # Other mentionable models like DesignManagement::Design, will never have such record as those do not have
+ # a description attribute.
+ def user_mention_identifier
+ {
+ user_mention_association.foreign_key => id,
+ note_id: nil
+ }
+ end
+
private
def extracted_mentionables(refs)
@@ -199,6 +214,10 @@ module Mentionable
{}
end
+ def user_mention_association
+ association(:user_mentions).reflection
+ end
+
# User mention that is parsed from model description rather then its related notes.
# Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
# Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 159f0044c82..196bec04be6 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -77,23 +77,16 @@ module Packages
validates container_type, presence: true
validates :file_store, presence: true
-
- validates :file_signature, absence: true
- validates :signing_keys, absence: true
+ validates :signed_file_store, presence: true
scope :with_container, ->(subject) { where(container_type => subject) }
scope :with_codename, ->(codename) { where(codename: codename) }
scope :with_suite, ->(suite) { where(suite: suite) }
scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) }
- attr_encrypted :signing_keys,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_32,
- algorithm: 'aes-256-gcm',
- encode: false,
- encode_iv: false
-
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
+ mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader
+ after_save :update_signed_file_store, if: :saved_change_to_signed_file?
def component_names
components.pluck(:name).sort
@@ -131,6 +124,12 @@ module Packages
self.class.with_container(container).with_codename(suite).exists?
end
+
+ def update_signed_file_store
+ # The signed_file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:signed_file_store, signed_file.object_store)
+ end
end
end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 484c91e0833..0cab874a240 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -90,6 +90,13 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:container_registry_access_level, value)
end
+ # TODO: Remove this method after we drop support for project create/edit APIs to set the
+ # container_registry_enabled attribute. They can instead set the container_registry_access_level
+ # attribute.
+ def container_registry_enabled=(value)
+ write_feature_attribute_boolean(:container_registry_access_level, value)
+ end
+
private
def write_feature_attribute_boolean(field, value)
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
new file mode 100644
index 00000000000..587f8c35ff7
--- /dev/null
+++ b/app/models/concerns/restricted_signup.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+module RestrictedSignup
+ extend ActiveSupport::Concern
+
+ private
+
+ def validate_admin_signup_restrictions(email)
+ return if allowed_domain?(email)
+
+ if allowlist_present?
+ return _('domain is not authorized for sign-up.')
+ elsif denied_domain?(email)
+ return _('is not from an allowed domain.')
+ elsif restricted_email?(email)
+ return _('is not allowed. Try again with a different email address, or contact your GitLab admin.')
+ end
+
+ nil
+ end
+
+ def denied_domain?(email)
+ return false unless Gitlab::CurrentSettings.domain_denylist_enabled?
+
+ denied_domains = Gitlab::CurrentSettings.domain_denylist
+ denied_domains.present? && domain_matches?(denied_domains, email)
+ end
+
+ def allowlist_present?
+ Gitlab::CurrentSettings.domain_allowlist.present?
+ end
+
+ def allowed_domain?(email)
+ allowed_domains = Gitlab::CurrentSettings.domain_allowlist
+ allowlist_present? && domain_matches?(allowed_domains, email)
+ end
+
+ def restricted_email?(email)
+ return false unless Gitlab::CurrentSettings.email_restrictions_enabled?
+
+ restrictions = Gitlab::CurrentSettings.email_restrictions
+ restrictions.present? && Gitlab::UntrustedRegexp.new(restrictions).match?(email)
+ end
+
+ def domain_matches?(email_domains, email)
+ signup_domain = Mail::Address.new(email).domain
+ email_domains.any? do |domain|
+ escaped = Regexp.escape(domain).gsub('\*', '.*?')
+ regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
+ signup_domain =~ regexp
+ end
+ end
+end
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
index 4fae36f7b8d..49342e30db6 100644
--- a/app/models/concerns/select_for_project_authorization.rb
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -5,7 +5,7 @@ module SelectForProjectAuthorization
class_methods do
def select_for_project_authorization
- select("projects.id AS project_id, members.access_level")
+ select("projects.id AS project_id", "members.access_level")
end
def select_as_maintainer_for_project_authorization
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
index 4921f7f1a7e..17fda6c806c 100644
--- a/app/models/concerns/sha256_attribute.rb
+++ b/app/models/concerns/sha256_attribute.rb
@@ -39,7 +39,7 @@ module Sha256Attribute
end
def database_exists?
- Gitlab::Database.exists?
+ Gitlab::Database.main.exists?
end
end
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index f6f5dbce4b6..27277bc5296 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -32,7 +32,7 @@ module ShaAttribute
end
def database_exists?
- Gitlab::Database.exists?
+ Gitlab::Database.main.exists?
end
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 2daea388939..4901cd832ff 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -111,7 +111,7 @@ module Spammable
end
# Override in Spammable if further checks are necessary
- def check_for_spam?
+ def check_for_spam?(user:)
true
end
diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb
index 8f6a6244dd3..1c433a3275e 100644
--- a/app/models/concerns/strip_attribute.rb
+++ b/app/models/concerns/strip_attribute.rb
@@ -7,7 +7,7 @@
# Usage:
#
# class Milestone < ApplicationRecord
-# strip_attributes :title
+# strip_attributes! :title
# end
#
#
@@ -15,7 +15,7 @@ module StripAttribute
extend ActiveSupport::Concern
class_methods do
- def strip_attributes(*attrs)
+ def strip_attributes!(*attrs)
strip_attrs.concat(attrs)
end
@@ -25,10 +25,10 @@ module StripAttribute
end
included do
- before_validation :strip_attributes
+ before_validation :strip_attributes!
end
- def strip_attributes
+ def strip_attributes!
self.class.strip_attrs.each do |attr|
self[attr].strip! if self[attr] && self[attr].respond_to?(:strip!)
end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 89b42eec727..54fe9eac2bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -11,7 +11,7 @@ module TimeTrackable
extend ActiveSupport::Concern
included do
- attr_reader :time_spent, :time_spent_user, :spent_at
+ attr_reader :time_spent, :time_spent_user, :spent_at, :summary
alias_method :time_spent?, :time_spent
@@ -20,7 +20,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -29,6 +29,7 @@ module TimeTrackable
@time_spent_note_id = options[:note_id]
@time_spent_user = User.find(options[:user_id])
@spent_at = options[:spent_at]
+ @summary = options[:summary]
@original_total_time_spent = nil
return if @time_spent == 0
@@ -78,7 +79,8 @@ module TimeTrackable
time_spent: time_spent,
note_id: @time_spent_note_id,
user: @time_spent_user,
- spent_at: @spent_at
+ spent_at: @spent_at,
+ summary: @summary
)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 8dc58f8dca1..79cbe225e5a 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -106,7 +106,7 @@ module Timebox
.where('due_date is NULL or due_date >= ?', start_date)
end
- strip_attributes :title
+ strip_attributes! :title
alias_attribute :name, :title
end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index f0e5e010e70..a656856487d 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -2,6 +2,35 @@
module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
-end
+ def matches_signatures(other_signatures, other_uuid)
+ other_signature_types = other_signatures.index_by(&:algorithm_type)
+
+ # highest first
+ match_result = nil
+ signatures.sort_by(&:priority).reverse_each do |signature|
+ matching_other_signature = other_signature_types[signature.algorithm_type]
+ next if matching_other_signature.nil?
+
+ match_result = matching_other_signature == signature
+ break
+ end
-VulnerabilityFindingHelpers.prepend_mod_with('VulnerabilityFindingHelpers')
+ if match_result.nil?
+ [uuid, *signature_uuids].include?(other_uuid)
+ else
+ match_result
+ end
+ end
+
+ def signature_uuids
+ signatures.map do |signature|
+ hex_sha = signature.signature_hex
+ ::Security::VulnerabilityUUID.generate(
+ report_type: report_type,
+ location_fingerprint: hex_sha,
+ primary_identifier_fingerprint: primary_identifier&.fingerprint,
+ project_id: project_id
+ )
+ end
+ end
+end
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
index f98c1e93aaf..71a12b4077b 100644
--- a/app/models/concerns/vulnerability_finding_signature_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -2,6 +2,30 @@
module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
-end
+ # If the location object describes a physical location within a file
+ # (filename + line numbers), the 'location' algorithm_type should be used
+ # If the location object describes arbitrary data, then the 'hash'
+ # algorithm_type should be used.
+
+ ALGORITHM_TYPES = { hash: 1, location: 2, scope_offset: 3 }.with_indifferent_access.freeze
+
+ class_methods do
+ def priority(algorithm_type)
+ raise ArgumentError, "No priority for #{algorithm_type.inspect}" unless ALGORITHM_TYPES.key?(algorithm_type)
+
+ ALGORITHM_TYPES[algorithm_type]
+ end
-VulnerabilityFindingSignatureHelpers.prepend_mod_with('VulnerabilityFindingSignatureHelpers')
+ def algorithm_types
+ ALGORITHM_TYPES
+ end
+ end
+
+ def priority
+ self.class.priority(algorithm_type)
+ end
+
+ def algorithm_types
+ self.class.algorithm_types
+ end
+end
diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb
index dbba80eff53..dfb1e151b41 100644
--- a/app/models/concerns/x509_serial_number_attribute.rb
+++ b/app/models/concerns/x509_serial_number_attribute.rb
@@ -39,7 +39,7 @@ module X509SerialNumberAttribute
end
def database_exists?
- Gitlab::Database.exists?
+ Gitlab::Database.main.exists?
end
end
end
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
new file mode 100644
index 00000000000..caf1cd68cc5
--- /dev/null
+++ b/app/models/customer_relations/organization.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CustomerRelations::Organization < ApplicationRecord
+ self.table_name = "customer_relations_organizations"
+
+ belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id'
+
+ before_validation :strip_whitespace!
+
+ enum state: {
+ inactive: 0,
+ active: 1
+ }
+
+ validates :group, presence: true
+ validates :name, presence: true
+ validates :name, uniqueness: { case_sensitive: false, scope: [:group_id] }
+ validates :name, length: { maximum: 255 }
+ validates :description, length: { maximum: 1024 }
+
+ def self.find_by_name(group_id, name)
+ where(group: group_id)
+ .where('LOWER(name) = LOWER(?)', name)
+ end
+
+ private
+
+ def strip_whitespace!
+ name&.strip!
+ end
+end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 5fa9f2ef9f9..326d3fb8470 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -10,6 +10,7 @@ class DeployToken < ApplicationRecord
AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
read_package_registry write_package_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
+ REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze
default_value_for(:expires_at) { Forever.date }
@@ -46,6 +47,12 @@ class DeployToken < ApplicationRecord
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
end
+ def valid_for_dependency_proxy?
+ group_type? &&
+ active? &&
+ REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) }
+ end
+
def revoke!
update!(revoked: true)
end
@@ -73,6 +80,14 @@ class DeployToken < ApplicationRecord
holder.has_access_to?(requested_project)
end
+ def has_access_to_group?(requested_group)
+ return false unless active?
+ return false unless group_type?
+ return false unless holder
+
+ holder.has_access_to_group?(requested_group)
+ end
+
# This is temporal. Currently we limit DeployToken
# to a single project or group, later we're going to
# extend that to be for multiple projects and namespaces.
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 313aeb1eda7..4a690ccc67e 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -289,7 +289,7 @@ class Deployment < ApplicationRecord
"#{id} as deployment_id",
"#{environment_id} as environment_id").to_sql
- # We don't use `Gitlab::Database.bulk_insert` here so that we don't need to
+ # We don't use `Gitlab::Database.main.bulk_insert` here so that we don't need to
# first pluck lots of IDs into memory.
#
# We also ignore any duplicates so this method can be called multiple times
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index ca65cf38f0d..6cda03557d1 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -88,7 +88,7 @@ module DesignManagement
rows = design_actions.map { |action| action.row_attrs(version) }
- Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 642e93f7912..f4d665cf279 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -43,9 +43,13 @@ class DiffDiscussion < Discussion
end
def cache_key
+ positions_json = diff_note_positions.map { |dnp| dnp.position.to_json }
+ positions_sha = Digest::SHA1.hexdigest(positions_json.join(':')) if positions_json.any?
+
[
super,
- Digest::SHA1.hexdigest(position.to_json)
+ Digest::SHA1.hexdigest(position.to_json),
+ positions_sha
].join(':')
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 076d8cc280c..203e14f1227 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -163,16 +163,15 @@ class Discussion
end
def cache_key
- # Need this so cache will be invalidated when note within a discussion
- # has been deleted.
- notes_sha = Digest::SHA1.hexdigest(notes.map(&:id).join(':'))
+ # Need to use the notes' cache key so cache will be invalidated when note
+ # within a discussion has been deleted or has different data after post
+ # processing of content.
+ notes_sha = Digest::SHA1.hexdigest(notes.map(&:post_processed_cache_key).join(':'))
[
CACHE_VERSION,
- notes.last.latest_cached_markdown_version,
id,
notes_sha,
- notes.max_by(&:updated_at).updated_at,
resolved_at
].join(':')
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 558963c98c4..963249c018a 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -5,6 +5,7 @@ class Environment < ApplicationRecord
include ReactiveCaching
include FastDestroyAll::Helpers
include Presentable
+ include NullifyIfBlank
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
@@ -14,6 +15,7 @@ class Environment < ApplicationRecord
belongs_to :project, required: true
use_fast_destroy :all_deployments
+ nullify_if_blank :external_url
has_many :all_deployments, class_name: 'Deployment'
has_many :deployments, -> { visible }
@@ -33,7 +35,6 @@ class Environment < ApplicationRecord
has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
- before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
before_save :set_environment_type
@@ -77,6 +78,7 @@ class Environment < ApplicationRecord
scope :for_name, -> (name) { where(name: name) }
scope :preload_cluster, -> { preload(last_deployment: :cluster) }
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) }
##
# Search environments which have names like the given query.
@@ -230,10 +232,6 @@ class Environment < ApplicationRecord
ref.to_s == last_deployment.try(:ref)
end
- def nullify_external_url
- self.external_url = nil if self.external_url.blank?
- end
-
def set_environment_type
names = name.split('/')
diff --git a/app/models/error_tracking/client_key.rb b/app/models/error_tracking/client_key.rb
new file mode 100644
index 00000000000..9d12c0ed6f1
--- /dev/null
+++ b/app/models/error_tracking/client_key.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class ErrorTracking::ClientKey < ApplicationRecord
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :public_key, presence: true, length: { maximum: 255 }
+
+ scope :active, -> { where(active: true) }
+
+ after_initialize :generate_key
+
+ def self.find_by_public_key(key)
+ find_by(public_key: key)
+ end
+
+ private
+
+ def generate_key
+ self.public_key = "glet_#{SecureRandom.hex}"
+ end
+end
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 012dcc4418f..32932c4d045 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -5,10 +5,19 @@ class ErrorTracking::Error < ApplicationRecord
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
+ scope :for_status, -> (status) { where(status: status) }
+
validates :project, presence: true
validates :name, presence: true
validates :description, presence: true
validates :actor, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ unresolved: 0,
+ resolved: 1,
+ ignored: 2
+ }
def self.report_error(name:, description:, actor:, platform:, timestamp:)
safe_find_or_create_by(
@@ -20,4 +29,64 @@ class ErrorTracking::Error < ApplicationRecord
error.update!(last_seen_at: timestamp)
end
end
+
+ def title
+ if description.present?
+ "#{name} #{description}"
+ else
+ name
+ end
+ end
+
+ def title_truncated
+ title.truncate(64)
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_error
+ Gitlab::ErrorTracking::Error.new(
+ id: id,
+ title: title_truncated,
+ message: description,
+ culprit: actor,
+ first_seen: first_seen_at,
+ last_seen: last_seen_at,
+ status: status,
+ count: events_count
+ )
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_detailed_error
+ Gitlab::ErrorTracking::DetailedError.new(
+ id: id,
+ title: title_truncated,
+ message: description,
+ culprit: actor,
+ first_seen: first_seen_at.to_s,
+ last_seen: last_seen_at.to_s,
+ count: events_count,
+ user_count: 0, # we don't support user count yet.
+ project_id: project.id,
+ status: status,
+ tags: { level: nil, logger: nil },
+ external_url: external_url,
+ external_base_url: external_base_url
+ )
+ end
+
+ private
+
+ # For compatibility with sentry integration
+ def external_url
+ Gitlab::Routing.url_helpers.details_namespace_project_error_tracking_index_url(
+ namespace_id: project.namespace,
+ project_id: project,
+ issue_id: id)
+ end
+
+ # For compatibility with sentry integration
+ def external_base_url
+ Gitlab::Routing.url_helpers.root_url
+ end
end
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index ed14a1bce41..4de13de7e2e 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -8,4 +8,69 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
validates :error, presence: true
validates :description, presence: true
validates :occurred_at, presence: true
+
+ def stacktrace
+ @stacktrace ||= build_stacktrace
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_error_event
+ Gitlab::ErrorTracking::ErrorEvent.new(
+ issue_id: error_id,
+ date_received: occurred_at,
+ stack_trace_entries: stacktrace
+ )
+ end
+
+ private
+
+ def build_stacktrace
+ raw_stacktrace = find_stacktrace_from_payload
+
+ return [] unless raw_stacktrace
+
+ raw_stacktrace.map do |entry|
+ {
+ 'lineNo' => entry['lineno'],
+ 'context' => build_stacktrace_context(entry),
+ 'filename' => entry['filename'],
+ 'function' => entry['function'],
+ 'colNo' => 0 # we don't support colNo yet.
+ }
+ end
+ end
+
+ def find_stacktrace_from_payload
+ exception_entry = payload.dig('exception')
+
+ if exception_entry
+ exception_values = exception_entry.dig('values')
+ stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
+ stack_trace_entry&.dig('stacktrace', 'frames')
+ end
+ end
+
+ def build_stacktrace_context(entry)
+ context = []
+ error_line = entry['context_line']
+ error_line_no = entry['lineno']
+ pre_context = entry['pre_context']
+ post_context = entry['post_context']
+
+ context += lines_with_position(pre_context, error_line_no - pre_context.size)
+ context += lines_with_position([error_line], error_line_no)
+ context += lines_with_position(post_context, error_line_no + 1)
+
+ context.reject(&:blank?)
+ end
+
+ def lines_with_position(lines, position)
+ return [] if lines.blank?
+
+ lines.map.with_index do |line, index|
+ next unless line
+
+ [position + index, line]
+ end
+ end
end
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index c729b002852..c5a77427588 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -31,12 +31,13 @@ module ErrorTracking
validates :api_url, length: { maximum: 255 }, public_url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
validates :enabled, inclusion: { in: [true, false] }
+ validates :integrated, inclusion: { in: [true, false] }
- validates :api_url, presence: { message: 'is a required field' }, if: :enabled
-
- validate :validate_api_url_path, if: :enabled
-
- validates :token, presence: { message: 'is a required field' }, if: :enabled
+ with_options if: :sentry_enabled do
+ validates :api_url, presence: { message: 'is a required field' }
+ validates :token, presence: { message: 'is a required field' }
+ validate :validate_api_url_path
+ end
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -45,6 +46,14 @@ module ErrorTracking
after_save :clear_reactive_cache!
+ def sentry_enabled
+ enabled && !integrated_client?
+ end
+
+ def integrated_client?
+ integrated && ::Feature.enabled?(:integrated_error_tracking, project)
+ end
+
def api_url=(value)
super
clear_memoization(:api_url_slugs)
@@ -79,7 +88,7 @@ module ErrorTracking
def sentry_client
strong_memoize(:sentry_client) do
- ErrorTracking::SentryClient.new(api_url, token)
+ ::ErrorTracking::SentryClient.new(api_url, token)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 14d20b0d6c4..f6174589a84 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -434,9 +434,9 @@ class Event < ApplicationRecord
def design_action_names
{
- created: _('uploaded'),
- updated: _('revised'),
- destroyed: _('deleted')
+ created: _('added'),
+ updated: _('updated'),
+ destroyed: _('removed')
}
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1e7308499a0..f6b45a755e4 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -80,7 +80,7 @@ class Group < Namespace
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- delegate :prevent_sharing_groups_outside_hierarchy, to: :namespace_settings
+ delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, to: :namespace_settings
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -158,7 +158,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where(project_namespace: { share_with_group_lock: false })
- .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ .select("projects.id AS project_id", "LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
@@ -296,7 +296,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- Members::Groups::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass
+ Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
@@ -306,7 +306,7 @@ class Group < Namespace
end
def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false)
- Members::Groups::CreatorService.new(self, # rubocop:todo CodeReuse/ServiceClass
+ Members::Groups::CreatorService.new(self, # rubocop:disable CodeReuse/ServiceClass
user,
access_level,
current_user: current_user,
@@ -463,7 +463,7 @@ class Group < Namespace
id
end
- group_hierarchy_members = GroupMember.where(source_id: source_ids)
+ group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list)
GroupMember.from_union([group_hierarchy_members,
members_from_self_and_ancestor_group_shares]).authorizable
@@ -481,6 +481,7 @@ class Group < Namespace
group_hierarchy_members = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(source_id: source_ids)
+ .select(*GroupMember.cached_column_list)
GroupMember.from_union([group_hierarchy_members,
members_from_self_and_ancestor_group_shares])
@@ -729,6 +730,10 @@ class Group < Namespace
end
# rubocop: enable CodeReuse/ServiceClass
+ def timelogs
+ Timelog.in_group(self)
+ end
+
private
def max_member_access(user_ids)
diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb
index 084a8672460..d9667e7c74d 100644
--- a/app/models/group_deploy_token.rb
+++ b/app/models/group_deploy_token.rb
@@ -11,9 +11,14 @@ class GroupDeployToken < ApplicationRecord
def has_access_to?(requested_project)
requested_project_group = requested_project&.group
return false unless requested_project_group
- return true if requested_project_group.id == group_id
- requested_project_group
+ has_access_to_group?(requested_project_group)
+ end
+
+ def has_access_to_group?(requested_group)
+ return true if requested_group.id == group_id
+
+ requested_group
.ancestors
.where(id: group_id)
.exists?
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 5f8fa4bca0a..9a78fe3971c 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -69,21 +69,26 @@ class WebHook < ApplicationRecord
end
def disable!
- update!(recent_failures: FAILURE_THRESHOLD + 1)
+ update_attribute(:recent_failures, FAILURE_THRESHOLD + 1)
end
def enable!
return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
- update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ save(validate: false)
end
def backoff!
- update!(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
+ assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
+ save(validate: false)
end
def failed!
- update!(recent_failures: recent_failures + 1) if recent_failures < MAX_FAILURES
+ return unless recent_failures < MAX_FAILURES
+
+ assign_attributes(recent_failures: recent_failures + 1)
+ save(validate: false)
end
# Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
diff --git a/app/models/incident_management/issuable_escalation_status.rb b/app/models/incident_management/issuable_escalation_status.rb
new file mode 100644
index 00000000000..88aef104d88
--- /dev/null
+++ b/app/models/incident_management/issuable_escalation_status.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class IssuableEscalationStatus < ApplicationRecord
+ include ::IncidentManagement::Escalatable
+
+ self.table_name = 'incident_management_issuable_escalation_statuses'
+
+ belongs_to :issue
+
+ validates :issue, presence: true, uniqueness: true
+ end
+end
+
+IncidentManagement::IssuableEscalationStatus.prepend_mod_with('IncidentManagement::IssuableEscalationStatus')
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index f401c23e453..09a60e9dd10 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -13,7 +13,9 @@ class InstanceConfiguration
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
- gitlab_ci: gitlab_ci }.deep_symbolize_keys
+ gitlab_ci: gitlab_ci,
+ package_file_size_limits: package_file_size_limits,
+ rate_limits: rate_limits }.deep_symbolize_keys
end
end
@@ -43,6 +45,66 @@ class InstanceConfiguration
default: 100.megabytes })
end
+ def package_file_size_limits
+ Plan.all.to_h { |plan| [plan.name.capitalize, plan_file_size_limits(plan)] }
+ end
+
+ def plan_file_size_limits(plan)
+ {
+ conan: plan.actual_limits[:conan_max_file_size],
+ maven: plan.actual_limits[:maven_max_file_size],
+ npm: plan.actual_limits[:npm_max_file_size],
+ nuget: plan.actual_limits[:nuget_max_file_size],
+ pypi: plan.actual_limits[:pypi_max_file_size],
+ terraform_module: plan.actual_limits[:terraform_module_max_file_size],
+ generic: plan.actual_limits[:generic_packages_max_file_size]
+ }
+ end
+
+ def rate_limits
+ {
+ unauthenticated: {
+ enabled: application_settings[:throttle_unauthenticated_enabled],
+ requests_per_period: application_settings[:throttle_unauthenticated_requests_per_period],
+ period_in_seconds: application_settings[:throttle_unauthenticated_period_in_seconds]
+ },
+ authenticated_api: {
+ enabled: application_settings[:throttle_authenticated_api_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_api_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_api_period_in_seconds]
+ },
+ authenticated_web: {
+ enabled: application_settings[:throttle_authenticated_web_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_web_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_web_period_in_seconds]
+ },
+ protected_paths: {
+ enabled: application_settings[:throttle_protected_paths_enabled],
+ requests_per_period: application_settings[:throttle_protected_paths_requests_per_period],
+ period_in_seconds: application_settings[:throttle_protected_paths_period_in_seconds]
+ },
+ unauthenticated_packages_api: {
+ enabled: application_settings[:throttle_unauthenticated_packages_api_enabled],
+ requests_per_period: application_settings[:throttle_unauthenticated_packages_api_requests_per_period],
+ period_in_seconds: application_settings[:throttle_unauthenticated_packages_api_period_in_seconds]
+ },
+ authenticated_packages_api: {
+ enabled: application_settings[:throttle_authenticated_packages_api_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_packages_api_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_packages_api_period_in_seconds]
+ },
+ issue_creation: application_setting_limit_per_minute(:issues_create_limit),
+ note_creation: application_setting_limit_per_minute(:notes_create_limit),
+ project_export: application_setting_limit_per_minute(:project_export_limit),
+ project_export_download: application_setting_limit_per_minute(:project_download_export_limit),
+ project_import: application_setting_limit_per_minute(:project_import_limit),
+ group_export: application_setting_limit_per_minute(:group_export_limit),
+ group_export_download: application_setting_limit_per_minute(:group_download_export_limit),
+ group_import: application_setting_limit_per_minute(:group_import_limit),
+ raw_blob: application_setting_limit_per_minute(:raw_blob_request_limit)
+ }
+ end
+
def ssh_algorithm_file(algorithm)
File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
end
@@ -70,4 +132,16 @@ class InstanceConfiguration
def ssh_algorithm_sha256(ssh_file_content)
Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end
+
+ def application_settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def application_setting_limit_per_minute(setting)
+ {
+ enabled: application_settings[setting] > 0,
+ requests_per_period: application_settings[setting],
+ period_in_seconds: 1.minute
+ }
+ end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index ea1e3840f6c..a9c865569d0 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -62,15 +62,13 @@ class Integration < ApplicationRecord
belongs_to :group, inverse_of: :integrations
has_one :service_hook, inverse_of: :integration, foreign_key: :service_id
- validates :project_id, presence: true, unless: -> { template? || instance_level? || group_level? }
- validates :group_id, presence: true, unless: -> { template? || instance_level? || project_level? }
- validates :project_id, :group_id, absence: true, if: -> { template? || instance_level? }
+ validates :project_id, presence: true, unless: -> { instance_level? || group_level? }
+ validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
+ validates :project_id, :group_id, absence: true, if: -> { instance_level? }
validates :type, presence: true, exclusion: BASE_CLASSES
- validates :type, uniqueness: { scope: :template }, if: :template?
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
validates :type, uniqueness: { scope: :group_id }, if: :group_level?
- validate :validate_is_instance_or_template
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
@@ -79,9 +77,9 @@ class Integration < ApplicationRecord
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
- scope :inherit, -> { where.not(inherit_from_id: nil) }
+ scope :with_default_settings, -> { where.not(inherit_from_id: nil) }
+ scope :with_custom_settings, -> { where(inherit_from_id: nil) }
scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
- scope :for_template, -> { where(template: true, type: available_integration_types(include_project_specific: false)) }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
scope :push_hooks, -> { where(push_events: true, active: true) }
@@ -169,25 +167,10 @@ class Integration < ApplicationRecord
'push'
end
- def self.find_or_create_templates
- create_nonexistent_templates
- for_template
+ def self.event_description(event)
+ IntegrationsHelper.integration_event_description(event)
end
- def self.create_nonexistent_templates
- nonexistent_integrations = build_nonexistent_integrations_for(for_template)
- return if nonexistent_integrations.empty?
-
- # Create within a transaction to perform the lowest possible SQL queries.
- transaction do
- nonexistent_integrations.each do |integration|
- integration.template = true
- integration.save
- end
- end
- end
- private_class_method :create_nonexistent_templates
-
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
return unless name.in?(available_integration_names(include_project_specific: false))
@@ -275,7 +258,6 @@ class Integration < ApplicationRecord
data_fields.integration = new_integration
end
- new_integration.template = false
new_integration.instance = false
new_integration.project_id = project_id
new_integration.group_id = group_id
@@ -292,7 +274,7 @@ class Integration < ApplicationRecord
end
def self.closest_group_integration(type, scope)
- group_ids = scope.ancestors.select(:id)
+ group_ids = scope.ancestors(hierarchy_order: :asc).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
@@ -306,12 +288,11 @@ class Integration < ApplicationRecord
end
private_class_method :instance_level_integration
- def self.create_from_active_default_integrations(scope, association, with_templates: false)
+ def self.create_from_active_default_integrations(scope, association)
group_ids = sorted_ancestors(scope).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
from_union([
- with_templates ? active.where(template: true) : none,
active.where(instance: true),
active.where(group_id: group_ids, inherit_from_id: nil)
]).order(Arel.sql("type ASC, array_position(#{array}::bigint[], #{table_name}.group_id), instance DESC")).group_by(&:type).each do |type, records|
@@ -384,7 +365,7 @@ class Integration < ApplicationRecord
end
def to_integration_hash
- as_json(methods: :type, except: %w[id template instance project_id group_id])
+ as_json(methods: :type, except: %w[id instance project_id group_id])
end
def to_data_fields_hash
@@ -503,10 +484,6 @@ class Integration < ApplicationRecord
end
end
- def validate_is_instance_or_template
- errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level?
- end
-
def validate_belongs_to_project_or_group
errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level?
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 590be52151c..1a7cbaa34c7 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -18,7 +18,7 @@ module Integrations
attr_accessor :response
- before_update :reset_password
+ before_validation :reset_password
def reset_password
if bamboo_url_changed? && !password_touched?
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 27c2fcf266b..5516e6bc2c0 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -2,6 +2,7 @@
module Integrations
class Datadog < Integration
+ include ActionView::Helpers::UrlHelper
include HasWebHook
extend Gitlab::Utils::Override
@@ -47,11 +48,12 @@ module Integrations
end
def description
- 'Trace your GitLab pipelines with Datadog'
+ s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.')
end
def help
- nil
+ docs_link = link_to s_('DatadogIntegration|How do I set up this integration?'), Rails.application.routes.url_helpers.help_page_url('integration/datadog'), target: '_blank', rel: 'noopener noreferrer'
+ s_('DatadogIntegration|Send CI/CD pipeline information to Datadog to monitor for job failures and troubleshoot performance issues. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -64,14 +66,19 @@ module Integrations
type: 'text',
name: 'datadog_site',
placeholder: DEFAULT_DOMAIN,
- help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
+ help: ERB::Util.html_escape(
+ s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe
+ },
required: false
},
{
type: 'text',
name: 'api_url',
- title: 'API URL',
- help: '(Advanced) Define the full URL for your Datadog site directly',
+ title: s_('DatadogIntegration|API URL'),
+ help: s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.'),
required: false
},
{
@@ -80,21 +87,34 @@ module Integrations
title: _('API key'),
non_empty_password_title: s_('ProjectService|Enter new API key'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
- help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
+ help: ERB::Util.html_escape(
+ s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
+ ) % {
+ linkOpen: '<a href="%s" target="_blank" rel="noopener noreferrer">'.html_safe % api_keys_url,
+ linkClose: '</a>'.html_safe
+ },
required: true
},
{
type: 'text',
name: 'datadog_service',
- title: 'Service',
+ title: s_('DatadogIntegration|Service'),
placeholder: 'gitlab-ci',
- help: 'Name of this GitLab instance that all data will be tagged with'
+ help: s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.')
},
{
type: 'text',
name: 'datadog_env',
- title: 'Env',
- help: 'The environment tag that traces will be tagged with'
+ title: s_('DatadogIntegration|Environment'),
+ placeholder: 'ci',
+ help: ERB::Util.html_escape(
+ s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
}
]
end
@@ -123,18 +143,18 @@ module Integrations
object_kind = 'job' if object_kind == 'build'
return unless supported_events.include?(object_kind)
+ data = data.with_retried_builds if data.respond_to?(:with_retried_builds)
+
execute_web_hook!(data, "#{object_kind} hook")
end
def test(data)
- begin
- result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 200
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result[:message] }
+ result = execute(data)
+
+ {
+ success: (200..299).cover?(result[:http_status]),
+ result: result[:message]
+ }
end
private
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index 7048dd641ea..cea4aa2038d 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -4,6 +4,8 @@ require 'uri'
module Integrations
class Irker < Integration
+ include ActionView::Helpers::UrlHelper
+
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
@@ -12,11 +14,11 @@ module Integrations
before_validation :get_channels
def title
- 'Irker (IRC gateway)'
+ s_('IrkerService|irker (IRC gateway)')
end
def description
- 'Send IRC messages.'
+ s_('IrkerService|Send update messages to an irker server.')
end
def self.to_param
@@ -42,33 +44,25 @@ module Integrations
end
def fields
+ recipients_docs_link = link_to s_('IrkerService|How to enter channels or users?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/irker', anchor: 'enter-irker-recipients'), target: '_blank', rel: 'noopener noreferrer'
[
- { type: 'text', name: 'server_host', placeholder: 'localhost',
- help: 'Irker daemon hostname (defaults to localhost)' },
- { type: 'text', name: 'server_port', placeholder: 6659,
- help: 'Irker daemon port (defaults to 6659)' },
- { type: 'text', name: 'default_irc_uri', title: 'Default IRC URI',
- help: 'A default IRC URI to prepend before each recipient (optional)',
+ { type: 'text', name: 'server_host', placeholder: 'localhost', title: s_('IrkerService|Server host (optional)'),
+ help: s_('IrkerService|irker daemon hostname (defaults to localhost).') },
+ { type: 'text', name: 'server_port', placeholder: 6659, title: s_('IrkerService|Server port (optional)'),
+ help: s_('IrkerService|irker daemon port (defaults to 6659).') },
+ { type: 'text', name: 'default_irc_uri', title: s_('IrkerService|Default IRC URI (optional)'),
+ help: s_('IrkerService|URI to add before each recipient.'),
placeholder: 'irc://irc.network.net:6697/' },
- { type: 'textarea', name: 'recipients',
- placeholder: 'Recipients/channels separated by whitespaces', required: true,
- help: 'Recipients have to be specified with a full URI: '\
- 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
- 'you want the channel to be a nickname instead, append ",isnick" to ' \
- 'the channel name; if the channel is protected by a secret password, ' \
- ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
- ' want to use a password, you have to omit the "#" on the channel). If you ' \
- ' specify a default IRC URI to prepend before each recipient, you can just ' \
- ' give a channel name.' },
- { type: 'checkbox', name: 'colorize_messages' }
+ { type: 'textarea', name: 'recipients', title: s_('IrkerService|Recipients'),
+ placeholder: 'irc[s]://irc.network.net[:port]/#channel', required: true,
+ help: s_('IrkerService|Channels and users separated by whitespaces. %{recipients_docs_link}').html_safe % { recipients_docs_link: recipients_docs_link.html_safe } },
+ { type: 'checkbox', name: 'colorize_messages', title: _('Colorize messages') }
]
end
def help
- ' NOTE: Irker does NOT have built-in authentication, which makes it' \
- ' vulnerable to spamming IRC channels if it is hosted outside of a ' \
- ' firewall. Please make sure you run the daemon within a secured network ' \
- ' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.'
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/irker', anchor: 'set-up-an-irker-daemon'), target: '_blank', rel: 'noopener noreferrer'
+ s_('IrkerService|Send update messages to an irker server. Before you can use this, you need to set up the irker daemon. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
private
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 55fc60990f3..e5c1d5ad0d7 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -8,7 +8,7 @@ module Integrations
prop_accessor :jenkins_url, :project_name, :username, :password
- before_update :reset_password
+ before_validation :reset_password
validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
validates :project_name, presence: true, if: :activated?
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 1dc5c0db9e3..ec6adc87bf4 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -33,7 +33,7 @@ module Integrations
data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
:vulnerabilities_enabled, :vulnerabilities_issuetype
- before_update :reset_password
+ before_validation :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
@@ -65,7 +65,10 @@ module Integrations
end
def reset_password
- data_fields.password = nil if reset_password?
+ return unless reset_password?
+
+ data_fields.password = nil
+ properties.delete('password') if properties
end
def set_default_data
@@ -536,8 +539,7 @@ module Integrations
end
def update_deployment_type?
- (api_url_changed? || url_changed? || username_changed? || password_changed?) &&
- testable?
+ api_url_changed? || url_changed? || username_changed? || password_changed?
end
def update_deployment_type
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 91e6800f03c..5aad25e8ddc 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -15,7 +15,7 @@ module Integrations
end
def help
- '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>'
+ '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>'
end
def webhook_placeholder
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index fb0917db02b..f616bc5faf2 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -18,7 +18,7 @@ module Integrations
end
def description
- s_('Integrations|Update your Packagist projects.')
+ s_('Integrations|Keep your PHP dependencies updated on Packagist.')
end
def self.to_param
@@ -27,9 +27,30 @@ module Integrations
def fields
[
- { type: 'text', name: 'username', placeholder: '', required: true },
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
+ {
+ type: 'text',
+ name: 'username',
+ title: _('Username'),
+ help: s_('Enter your Packagist username.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'token',
+ title: _('Token'),
+ help: s_('Enter your Packagist token.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'server',
+ title: _('Server (optional)'),
+ help: s_('Enter your Packagist server. Defaults to https://packagist.org.'),
+ placeholder: 'https://packagist.org',
+ required: false
+ }
]
end
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index b0cadc7ef4e..db39a4c68bd 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -21,18 +21,46 @@ module Integrations
def fields
[
- { type: 'text', name: 'api_key', title: _('API key'), placeholder: s_('PushoverService|Your application key'), required: true },
- { type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true },
- { type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') },
- { type: 'select', name: 'priority', required: true, choices:
+ {
+ type: 'text',
+ name: 'api_key',
+ title: _('API key'),
+ help: s_('PushoverService|Enter your application key.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'user_key',
+ title: _('User key'),
+ help: s_('PushoverService|Enter your user key.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'device',
+ title: _('Devices (optional)'),
+ help: s_('PushoverService|Leave blank for all active devices.'),
+ placeholder: ''
+ },
+ {
+ type: 'select',
+ name: 'priority',
+ required: true,
+ choices:
[
- [s_('PushoverService|Lowest Priority'), -2],
- [s_('PushoverService|Low Priority'), -1],
- [s_('PushoverService|Normal Priority'), 0],
- [s_('PushoverService|High Priority'), 1]
+ [s_('PushoverService|Lowest priority'), -2],
+ [s_('PushoverService|Low priority'), -1],
+ [s_('PushoverService|Normal priority'), 0],
+ [s_('PushoverService|High priority'), 1]
],
- default_choice: 0 },
- { type: 'select', name: 'sound', choices:
+ default_choice: 0
+ },
+ {
+ type: 'select',
+ name: 'sound',
+ choices:
[
['Device default sound', nil],
['Pushover (default)', 'pushover'],
@@ -57,7 +85,8 @@ module Integrations
['Pushover Echo (long)', 'echo'],
['Up Down (long)', 'updown'],
['None (silent)', 'none']
- ] }
+ ]
+ }
]
end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 135c304b57e..3f868b57597 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -18,7 +18,7 @@ module Integrations
attr_accessor :response
- before_update :reset_password
+ before_validation :reset_password
class << self
def to_param
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 834222834e9..ad6a9164d00 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -18,7 +18,7 @@ module Integrations
'This service sends notifications about projects events to a Unify Circuit conversation.<br />
To set up this service:
<ol>
- <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
+ <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448" target="_blank" rel="noopener noreferrer">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index f114094d69c..a54de3c82d1 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -83,7 +83,7 @@ class InternalId < ApplicationRecord
self.internal_id_transactions_total.increment(
operation: operation,
usage: usage.to_s,
- in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s
+ in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s # rubocop: disable Database/MultipleDatabases
)
end
@@ -317,7 +317,7 @@ class InternalId < ApplicationRecord
stmt.set(arel_table[:last_value] => new_value)
stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints
- ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value')
+ ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value') # rubocop: disable Database/MultipleDatabases
end
def create_record!(subject, scope, usage, init)
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
index 35d03a544bd..928301e1da6 100644
--- a/app/models/issuable_severity.rb
+++ b/app/models/issuable_severity.rb
@@ -10,6 +10,14 @@ class IssuableSeverity < ApplicationRecord
critical: 'Critical - S1'
}.freeze
+ SEVERITY_QUICK_ACTION_PARAMS = {
+ unknown: %w(Unknown 0),
+ low: %w(Low S4 4),
+ medium: %w(Medium S3 3),
+ high: %w(High S2 2),
+ critical: %w(Critical S1 1)
+ }.freeze
+
belongs_to :issue
validates :issue, presence: true, uniqueness: true
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d91d72e1fba..48e3fdd51e9 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -48,6 +48,7 @@ class Issue < ApplicationRecord
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
belongs_to :iteration, foreign_key: 'sprint_id'
+ belongs_to :work_item_type, class_name: 'WorkItem::Type', inverse_of: :work_items
belongs_to :moved_to, class_name: 'Issue'
has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id
@@ -76,6 +77,7 @@ class Issue < ApplicationRecord
has_one :issuable_severity
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
+ 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 :prometheus_alerts, through: :prometheus_alert_events
@@ -86,12 +88,7 @@ class Issue < ApplicationRecord
validates :project, presence: true
validates :issue_type, presence: true
- enum issue_type: {
- issue: 0,
- incident: 1,
- test_case: 2, ## EE-only
- requirement: 3 ## EE-only
- }
+ enum issue_type: WorkItem::Type.base_types
alias_method :issuing_parent, :project
@@ -134,6 +131,15 @@ class Issue < ApplicationRecord
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
+ scope :without_hidden, -> {
+ if Feature.enabled?(:ban_user_feature_flag)
+ where(id: joins('LEFT JOIN banned_users ON banned_users.user_id = issues.author_id WHERE banned_users.user_id IS NULL')
+ .select('issues.id'))
+ else
+ all
+ end
+ }
+
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
@@ -317,6 +323,21 @@ class Issue < ApplicationRecord
)
end
+ def self.to_branch_name(*args)
+ branch_name = args.map(&:to_s).each_with_index.map do |arg, i|
+ arg.parameterize(preserve_case: i == 0).presence
+ end.compact.join('-')
+
+ if branch_name.length > 100
+ truncated_string = branch_name[0, 100]
+ # Delete everything dangling after the last hyphen so as not to risk
+ # existence of unintended words in the branch name due to mid-word split.
+ branch_name = truncated_string.sub(/-[^-]*\Z/, '')
+ end
+
+ branch_name
+ end
+
# Temporary disable moving null elements because of performance problems
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
def check_repositioning_allowed!
@@ -384,16 +405,7 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
- branch_name = "#{iid}-#{title.parameterize}"
-
- if branch_name.length > 100
- truncated_string = branch_name[0, 100]
- # Delete everything dangling after the last hyphen so as not to risk
- # existence of unintended words in the branch name due to mid-word split.
- branch_name = truncated_string[0, truncated_string.rindex("-")]
- end
-
- branch_name
+ self.class.to_branch_name(iid, title)
end
end
@@ -437,10 +449,10 @@ class Issue < ApplicationRecord
user, project.external_authorization_classification_label)
end
- def check_for_spam?
+ def check_for_spam?(user:)
# content created via support bots is always checked for spam, EVEN if
# the issue is not publicly visible and/or confidential
- return true if author.support_bot? && spammable_attribute_changed?
+ return true if user.support_bot? && spammable_attribute_changed?
# Only check for spam on issues which are publicly visible (and thus indexed in search engines)
return false unless publicly_visible?
@@ -549,6 +561,8 @@ class Issue < ApplicationRecord
true
elsif confidential? && !assignee_or_author?(user)
project.team.member?(user, Gitlab::Access::REPORTER)
+ elsif hidden?
+ false
else
project.public? ||
project.internal? && !user.external? ||
@@ -556,6 +570,10 @@ class Issue < ApplicationRecord
end
end
+ def hidden?
+ author&.banned?
+ end
+
private
def spammable_attribute_changed?
@@ -583,7 +601,7 @@ class Issue < ApplicationRecord
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
- project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
+ project.public? && !confidential? && !hidden? && !::Gitlab::ExternalAuthorization.enabled?
end
def expire_etag_cache
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 7480800abc3..759d44fb29e 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -11,6 +11,7 @@ class JiraConnectInstallation < ApplicationRecord
validates :client_key, presence: true, uniqueness: true
validates :shared_secret, presence: true
validates :base_url, presence: true, public_url: true
+ validates :instance_url, public_url: true, allow_blank: true
scope :for_project, -> (project) {
distinct
diff --git a/app/models/label.rb b/app/models/label.rb
index 1a07620f944..a46d6bc5c0f 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -9,10 +9,6 @@ class Label < ApplicationRecord
include Sortable
include FromUnion
include Presentable
- include IgnorableColumns
-
- # TODO: Project#create_labels can remove column exception when this column is dropped from all envs
- ignore_column :remove_on_close, remove_with: '14.1', remove_after: '2021-06-22'
cache_markdown_field :description, pipeline: :single_line
diff --git a/app/models/member.rb b/app/models/member.rb
index 14c886e3ab8..397e60be3a8 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -12,6 +12,7 @@ class Member < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include FromUnion
include UpdateHighestRole
+ include RestrictedSignup
AVATAR_SIZE = 40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@@ -42,6 +43,7 @@ class Member < ApplicationRecord
scope: [:source_type, :source_id],
allow_nil: true
}
+ validate :signup_email_valid?, on: :create, if: ->(member) { member.invite_email.present? }
validates :user_id,
uniqueness: {
message: _('project bots cannot be added to other groups / projects')
@@ -166,7 +168,7 @@ class Member < ApplicationRecord
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
- before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
+ before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? && !member.invite_accepted_at? }
after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request?, unless: :importing?
@@ -175,7 +177,9 @@ class Member < ApplicationRecord
after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
after_destroy :destroy_notification_setting
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
- after_commit :refresh_member_authorized_projects
+ after_save :log_invitation_token_cleanup
+
+ after_commit :refresh_member_authorized_projects, unless: :importing?
default_value_for :notification_level, NotificationSetting.levels[:global]
@@ -391,11 +395,6 @@ class Member < ApplicationRecord
# error or not doing any meaningful work.
# rubocop: disable CodeReuse/ServiceClass
def refresh_member_authorized_projects
- # If user/source is being destroyed, project access are going to be
- # destroyed eventually because of DB foreign keys, so we shouldn't bother
- # with refreshing after each member is destroyed through association
- return if destroyed_by_association.present?
-
UserProjectAccessChangedService.new(user_id).execute
end
# rubocop: enable CodeReuse/ServiceClass
@@ -436,6 +435,12 @@ class Member < ApplicationRecord
end
end
+ def signup_email_valid?
+ error = validate_admin_signup_restrictions(invite_email)
+
+ errors.add(:user, error) if error
+ end
+
def update_highest_role?
return unless user_id.present?
@@ -449,6 +454,13 @@ class Member < ApplicationRecord
def project_bot?
user&.project_bot?
end
+
+ def log_invitation_token_cleanup
+ return true unless Gitlab.com? && invite? && invite_accepted_at?
+
+ error = StandardError.new("Invitation token is present but invite was already accepted!")
+ Gitlab::ErrorTracking.track_exception(error, attributes.slice(%w["invite_accepted_at created_at source_type source_id user_id id"]))
+ end
end
Member.prepend_mod_with('Member')
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index cf5906a4cbf..a13133c90e9 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class GroupMember < Member
+ extend ::Gitlab::Utils::Override
include FromUnion
include CreatedAtFilterable
@@ -28,8 +29,6 @@ class GroupMember < Member
attr_accessor :last_owner, :last_blocked_owner
- self.enumerate_columns_in_select_statements = true
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -51,6 +50,19 @@ class GroupMember < Member
{ group: group }
end
+ override :refresh_member_authorized_projects
+ def refresh_member_authorized_projects
+ # Here, `destroyed_by_association` will be present if the
+ # GroupMember is being destroyed due to the `dependent: :destroy`
+ # callback on Group. In this case, there is no need to refresh the
+ # authorizations, because whenever a Group is being destroyed,
+ # its projects are also destroyed, so the removal of project_authorizations
+ # will happen behind the scenes via DB foreign keys anyway.
+ return if destroyed_by_association.present?
+
+ super
+ end
+
private
def access_level_inclusion
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 5040879e177..b45c0b6a0cc 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ProjectMember < Member
+ extend ::Gitlab::Utils::Override
SOURCE_TYPE = 'Project'
belongs_to :project, foreign_key: 'source_id'
@@ -19,11 +20,6 @@ class ProjectMember < Member
.where(projects: { namespace_id: groups.select(:id) })
end
- scope :without_project_bots, -> do
- left_join_users
- .merge(User.without_project_bot)
- end
-
class << self
# Add users to projects with passed access option
#
@@ -48,7 +44,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- Members::Projects::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -94,6 +90,22 @@ class ProjectMember < Member
{ project: project }
end
+ override :refresh_member_authorized_projects
+ def refresh_member_authorized_projects
+ return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh)
+ return unless user
+
+ # rubocop:disable CodeReuse/ServiceClass
+ AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
+
+ # Until we compare the inconsistency rates of the new, specialized service and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ UserProjectAccessChangedService.new(user_id)
+ .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY)
+ # rubocop:enable CodeReuse/ServiceClass
+ end
+
private
def send_invite
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7ca83d1d68c..a090ac87cc9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -329,16 +329,16 @@ class MergeRequest < ApplicationRecord
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
- scope :order_merged_at, ->(direction) do
+ scope :order_by_metric, ->(metric, direction) do
reverse_direction = { 'ASC' => 'DESC', 'DESC' => 'ASC' }
reversed_direction = reverse_direction[direction] || raise("Unknown sort direction was given: #{direction}")
order = Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'merge_request_metrics_merged_at',
- column_expression: MergeRequest::Metrics.arel_table[:merged_at],
- order_expression: Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', direction),
- reversed_order_expression: Gitlab::Database.nulls_first_order('merge_request_metrics.merged_at', reversed_direction),
+ attribute_name: "merge_request_metrics_#{metric}",
+ column_expression: MergeRequest::Metrics.arel_table[metric],
+ order_expression: Gitlab::Database.nulls_last_order("merge_request_metrics.#{metric}", direction),
+ reversed_order_expression: Gitlab::Database.nulls_first_order("merge_request_metrics.#{metric}", reversed_direction),
order_direction: direction,
nullable: :nulls_last,
distinct: false,
@@ -353,8 +353,10 @@ class MergeRequest < ApplicationRecord
order.apply_cursor_conditions(join_metrics).order(order)
end
- scope :order_merged_at_asc, -> { order_merged_at('ASC') }
- scope :order_merged_at_desc, -> { order_merged_at('DESC') }
+ scope :order_merged_at_asc, -> { order_by_metric(:merged_at, 'ASC') }
+ scope :order_merged_at_desc, -> { order_by_metric(:merged_at, 'DESC') }
+ scope :order_closed_at_asc, -> { order_by_metric(:latest_closed_at, 'ASC') }
+ scope :order_closed_at_desc, -> { order_by_metric(:latest_closed_at, 'DESC') }
scope :preload_source_project, -> { preload(:source_project) }
scope :preload_target_project, -> { preload(:target_project) }
scope :preload_routables, -> do
@@ -452,7 +454,9 @@ class MergeRequest < ApplicationRecord
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'merged_at', 'merged_at_asc' then order_merged_at_asc
+ when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'merged_at_desc' then order_merged_at_desc
+ when 'closed_at_desc' then order_closed_at_desc
else
super
end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index 0f2a7515462..09824ed4468 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -26,7 +26,7 @@ class MergeRequestContextCommit < ApplicationRecord
# create MergeRequestContextCommit by given commit sha and it's diff file record
def self.bulk_insert(rows, **args)
- Gitlab::Database.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index 8abedd26b06..b9efebe3af2 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -14,7 +14,7 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
def path
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d2ea663551d..bea75927b2c 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -395,10 +395,10 @@ class MergeRequestDiff < ApplicationRecord
if comparison
if diff_options[:paths].blank? && !without_files?
# Return the empty MergeRequestDiffBatch for an out of bound batch request
- break diffs_batch if diffs_batch.diff_file_paths.blank?
+ break diffs_batch if diffs_batch.diff_paths.blank?
diff_options.merge!(
- paths: diffs_batch.diff_file_paths,
+ paths: diffs_batch.diff_paths,
pagination_data: diffs_batch.pagination_data
)
end
@@ -515,7 +515,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
save!
end
@@ -535,7 +535,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
update!(stored_externally: false)
end
@@ -595,7 +595,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
# Faster inserts
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
end
def build_external_diff_tempfile(rows)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 466d28301c0..d9a1784cdda 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -63,7 +63,7 @@ class MergeRequestDiffCommit < ApplicationRecord
)
end
- Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
def self.prepare_commits_for_bulk_insert(commits)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2168d57693e..0e2842c3c11 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -61,10 +61,38 @@ class Milestone < ApplicationRecord
end
def self.reference_pattern
+ if Feature.enabled?(:milestone_reference_pattern, default_enabled: :yaml)
+ new_reference_pattern
+ else
+ old_reference_pattern
+ end
+ end
+
+ def self.new_reference_pattern
+ # NOTE: The iid pattern only matches when all characters on the expression
+ # are digits, so it will match %2 but not %2.1 because that's probably a
+ # milestone name and we want it to be matched as such.
+ @new_reference_pattern ||= %r{
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
+ (?:
+ (?<milestone_iid>
+ \d+(?!\S\w)\b # Integer-based milestone iid, or
+ ) |
+ (?<milestone_name>
+ [^"\s\<]+\b | # String-based single-word milestone title, or
+ "[^"]+" # String-based multi-word milestone surrounded in quotes
+ )
+ )
+ }x
+ end
+
+ # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
+ def self.old_reference_pattern
# NOTE: The iid pattern only matches when all characters on the expression
# are digits, so it will match %2 but not %2.1 because that's probably a
# milestone name and we want it to be matched as such.
- @reference_pattern ||= %r{
+ @old_reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5524fec5324..261639a4ec1 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -24,6 +24,7 @@ class Namespace < ApplicationRecord
NUMBER_OF_ANCESTORS_ALLOWED = 20
SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
+ URL_MAX_LENGTH = 255
cache_markdown_field :description, pipeline: :description
@@ -33,6 +34,7 @@ class Namespace < ApplicationRecord
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
+ has_many :pending_builds, class_name: 'Ci::PendingBuild'
has_one :onboarding_progress
# This should _not_ be `inverse_of: :namespace`, because that would also set
@@ -58,7 +60,7 @@ class Namespace < ApplicationRecord
validates :description, length: { maximum: 255 }
validates :path,
presence: true,
- length: { maximum: 255 },
+ length: { maximum: URL_MAX_LENGTH },
namespace_path: true
# Introduce minimal path length of 2 characters.
@@ -464,10 +466,34 @@ class Namespace < ApplicationRecord
end
def refresh_access_of_projects_invited_groups
- Group
- .joins(project_group_links: :project)
- .where(projects: { namespace_id: id })
- .find_each(&:refresh_members_authorized_projects)
+ if Feature.enabled?(:specialized_worker_for_group_lock_update_auth_recalculation)
+ Project
+ .where(namespace_id: id)
+ .joins(:project_group_links)
+ .distinct
+ .find_each do |project|
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
+ end
+
+ # Until we compare the inconsistency rates of the new specialized worker and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ Group
+ .joins(project_group_links: :project)
+ .where(projects: { namespace_id: id })
+ .distinct
+ .find_each do |group|
+ group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
+ end
+ else
+ Group
+ .joins(project_group_links: :project)
+ .where(projects: { namespace_id: id })
+ .find_each(&:refresh_members_authorized_projects)
+ end
end
def nesting_level_allowed
@@ -503,7 +529,7 @@ class Namespace < ApplicationRecord
def write_projects_repository_config
all_projects.find_each do |project|
- project.write_repository_config
+ project.set_full_path
project.track_project_repository
end
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index fc890bf687c..4a39bfebda0 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -11,6 +11,9 @@ class NamespaceSetting < ApplicationRecord
validate :allow_mfa_for_group
validate :allow_resource_access_token_creation_for_group
+ before_save :set_prevent_sharing_groups_outside_hierarchy, if: -> { user_cap_enabled? }
+ after_save :disable_project_sharing!, if: -> { user_cap_enabled? }
+
before_validation :normalize_default_branch_name
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
@@ -19,10 +22,20 @@ class NamespaceSetting < ApplicationRecord
self.primary_key = :namespace_id
+ def prevent_sharing_groups_outside_hierarchy
+ return super if namespace.root?
+
+ namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
+ end
+
private
def normalize_default_branch_name
- self.default_branch_name = nil if default_branch_name.blank?
+ self.default_branch_name = if default_branch_name.blank?
+ nil
+ else
+ Sanitize.fragment(self.default_branch_name)
+ end
end
def default_branch_name_content
@@ -44,6 +57,18 @@ class NamespaceSetting < ApplicationRecord
errors.add(:resource_access_token_creation_allowed, _('is not allowed since the group is not top-level group.'))
end
end
+
+ def set_prevent_sharing_groups_outside_hierarchy
+ self.prevent_sharing_groups_outside_hierarchy = true
+ end
+
+ def disable_project_sharing!
+ namespace.update_attribute(:share_with_group_lock, true)
+ end
+
+ def user_cap_enabled?
+ new_user_signups_cap.present? && namespace.root?
+ end
end
NamespaceSetting.prepend_mod_with('NamespaceSetting')
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 3d78f384634..33e8c3e5172 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -37,6 +37,7 @@ module Namespaces
module Traversal
module Linear
extend ActiveSupport::Concern
+ include LinearScopes
UnboundedSearch = Class.new(StandardError)
@@ -44,14 +45,6 @@ module Namespaces
before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
-
- scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
- # When filtering namespaces by the traversal_ids column to compile a
- # list of namespace IDs, it's much faster to reference the ID in
- # traversal_ids than the primary key ID column.
- # WARNING This scope must be used behind a linear query feature flag
- # such as `use_traversal_ids`.
- scope :as_ids, -> { select('traversal_ids[array_length(traversal_ids, 1)] AS id') }
end
def sync_traversal_ids?
@@ -59,7 +52,7 @@ module Namespaces
end
def use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
traversal_ids.present?
end
@@ -164,20 +157,14 @@ module Namespaces
Namespace.lock.select(:id).where(id: roots).order(id: :asc).load
end
- # Make sure we drop the STI `type = 'Group'` condition for better performance.
- # Logically equivalent so long as hierarchies remain homogeneous.
- def without_sti_condition
- self.class.unscope(where: :type)
- end
-
# Search this namespace's lineage. Bound inclusively by top node.
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
- skope = without_sti_condition
+ skope = self.class.without_sti_condition
if top
- skope = skope.traversal_ids_contains("{#{top.id}}")
+ skope = skope.where("traversal_ids @> ('{?}')", top.id)
end
if bottom
@@ -190,7 +177,13 @@ module Namespaces
if hierarchy_order
depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
- .order(depth: hierarchy_order)
+ # The SELECT includes an extra depth attribute. We wrap the SQL in a
+ # standard SELECT to avoid mismatched attribute errors when trying to
+ # chain future ActiveRelation commands, and retain the ordering.
+ skope = self.class
+ .without_sti_condition
+ .from(skope, self.class.table_name)
+ .order(depth: hierarchy_order)
end
skope
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
new file mode 100644
index 00000000000..90fae8ef35d
--- /dev/null
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module LinearScopes
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # When filtering namespaces by the traversal_ids column to compile a
+ # list of namespace IDs, it can be faster to reference the ID in
+ # traversal_ids than the primary key ID column.
+ def as_ids
+ return super unless use_traversal_ids?
+
+ select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
+ end
+
+ def self_and_descendants(include_self: true)
+ return super unless use_traversal_ids?
+
+ records = self_and_descendants_with_duplicates(include_self: include_self)
+
+ distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
+
+ # Produce a query of the form: SELECT * FROM namespaces;
+ #
+ # When we have queries that break this SELECT * format we can run in to errors.
+ # For example `SELECT DISTINCT on(...)` will fail when we chain a `.count` c
+ unscoped.without_sti_condition.from(distinct, :namespaces)
+ end
+
+ def self_and_descendant_ids(include_self: true)
+ return super unless use_traversal_ids?
+
+ self_and_descendants_with_duplicates(include_self: include_self)
+ .select('DISTINCT namespaces.id')
+ end
+
+ # Make sure we drop the STI `type = 'Group'` condition for better performance.
+ # Logically equivalent so long as hierarchies remain homogeneous.
+ def without_sti_condition
+ unscope(where: :type)
+ end
+
+ private
+
+ def use_traversal_ids?
+ Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ end
+
+ def self_and_descendants_with_duplicates(include_self: true)
+ base_ids = select(:id)
+
+ records = unscoped
+ .without_sti_condition
+ .from("namespaces, (#{base_ids.to_sql}) base")
+ .where('namespaces.traversal_ids @> ARRAY[base.id]')
+
+ if include_self
+ records
+ else
+ records.where('namespaces.id <> base.id')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
index d9e8743aa50..c1ada715d6d 100644
--- a/app/models/namespaces/traversal/recursive.rb
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -4,6 +4,7 @@ module Namespaces
module Traversal
module Recursive
extend ActiveSupport::Concern
+ include RecursiveScopes
def root_ancestor
return self if parent.nil?
diff --git a/app/models/namespaces/traversal/recursive_scopes.rb b/app/models/namespaces/traversal/recursive_scopes.rb
new file mode 100644
index 00000000000..be49d5d9d55
--- /dev/null
+++ b/app/models/namespaces/traversal/recursive_scopes.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module RecursiveScopes
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def as_ids
+ select('id')
+ end
+
+ def descendant_ids
+ recursive_descendants.as_ids
+ end
+ alias_method :recursive_descendant_ids, :descendant_ids
+
+ def self_and_descendants(include_self: true)
+ base = if include_self
+ unscoped.where(id: all.as_ids)
+ else
+ unscoped.where(parent_id: all.as_ids)
+ end
+
+ Gitlab::ObjectHierarchy.new(base).base_and_descendants
+ end
+ alias_method :recursive_self_and_descendants, :self_and_descendants
+
+ def self_and_descendant_ids(include_self: true)
+ self_and_descendants(include_self: include_self).as_ids
+ end
+ alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
+ end
+ end
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2ad6df85e5f..34ffd7c91af 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -5,6 +5,8 @@
# A note of this type is never resolvable.
class Note < ApplicationRecord
extend ActiveModel::Naming
+ extend Gitlab::Utils::Override
+
include Gitlab::Utils::StrongMemoize
include Participable
include Mentionable
@@ -576,6 +578,29 @@ class Note < ApplicationRecord
review.present? || !author.can_trigger_notifications?
end
+ def post_processed_cache_key
+ cache_key_items = [cache_key, author.cache_key]
+ cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
+
+ cache_key_items.join(':')
+ end
+
+ override :user_mention_class
+ def user_mention_class
+ return if noteable.blank?
+
+ noteable.user_mention_class
+ end
+
+ override :user_mention_identifier
+ def user_mention_identifier
+ return if noteable.blank?
+
+ noteable.user_mention_identifier.merge({
+ note_id: id
+ })
+ end
+
private
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 4323f89865a..2e45753c182 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -16,7 +16,7 @@ class NotificationSetting < ApplicationRecord
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
- validate :owns_notification_email, if: :notification_email_changed?
+ validate :notification_email_verified, if: :notification_email_changed?
scope :for_groups, -> { where(source_type: 'Namespace') }
@@ -110,11 +110,11 @@ class NotificationSetting < ApplicationRecord
has_attribute?(event) && !!read_attribute(event)
end
- def owns_notification_email
+ def notification_email_verified
return if user.temp_oauth_email?
return if notification_email.empty?
- errors.add(:notification_email, _("is not an email you own")) unless user.verified_emails.include?(notification_email)
+ errors.add(:notification_email, _("must be an email you have verified")) unless user.verified_emails.include?(notification_email)
end
end
diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb
index c70e10c72d5..ed9400dde8f 100644
--- a/app/models/operations/feature_flags/strategy.rb
+++ b/app/models/operations/feature_flags/strategy.rb
@@ -16,7 +16,7 @@ module Operations
STRATEGY_USERWITHID => ['userIds'].freeze
}.freeze
USERID_MAX_LENGTH = 256
- STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze
+ STICKINESS_SETTINGS = %w[default userId sessionId random].freeze
self.table_name = 'operations_strategies'
diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb
index e20f1b8244a..2daafe0ebcf 100644
--- a/app/models/packages/debian.rb
+++ b/app/models/packages/debian.rb
@@ -6,6 +6,8 @@ module Packages
COMPONENT_REGEX = DISTRIBUTION_REGEX.freeze
ARCHITECTURE_REGEX = %r{[a-z0-9][-a-z0-9]*}.freeze
+ LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
+
def self.table_name_prefix
'packages_debian_'
end
diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb
index a1eb7120117..bb2c33594e5 100644
--- a/app/models/packages/event.rb
+++ b/app/models/packages/event.rb
@@ -4,7 +4,7 @@ class Packages::Event < ApplicationRecord
belongs_to :package, optional: true
UNIQUE_EVENTS_ALLOWED = %i[push_package delete_package pull_package pull_symbol_package push_symbol_package].freeze
- EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze
+ EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001, dependency_proxy: 1002).freeze
EVENT_PREFIX = "i_package"
@@ -23,7 +23,11 @@ class Packages::Event < ApplicationRecord
list_tags: 9,
cli_metadata: 10,
pull_symbol_package: 11,
- push_symbol_package: 12
+ push_symbol_package: 12,
+ pull_manifest: 13,
+ pull_manifest_from_cache: 14,
+ pull_blob: 15,
+ pull_blob_from_cache: 16
}
enum originator_type: { user: 0, deploy_token: 1, guest: 2 }
diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb
new file mode 100644
index 00000000000..e49199d911c
--- /dev/null
+++ b/app/models/packages/npm.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+module Packages
+ module Npm
+ # from "@scope/package-name" return "scope" or nil
+ def self.scope_of(package_name)
+ return unless package_name
+ return unless package_name.starts_with?('@')
+ return unless package_name.include?('/')
+
+ package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index d2e4f46898c..4ea127fc222 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -62,7 +62,7 @@ class Packages::Package < ApplicationRecord
validate :valid_conan_package_recipe, if: :conan?
validate :valid_composer_global_name, if: :composer?
- validate :package_already_taken, if: :npm?
+ validate :npm_package_already_taken, if: :npm?
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
@@ -320,14 +320,22 @@ class Packages::Package < ApplicationRecord
end
end
- def package_already_taken
+ def npm_package_already_taken
return unless project
+ return unless follows_npm_naming_convention?
- if project.package_already_taken?(name)
+ if project.package_already_taken?(name, version, package_type: :npm)
errors.add(:base, _('Package already exists'))
end
end
+ # https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
+ def follows_npm_naming_convention?
+ return false unless project&.root_namespace&.path
+
+ project.root_namespace.path == ::Packages::Npm.scope_of(name)
+ end
+
def unique_debian_package_name
return unless debian_publication&.distribution
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 799242a639a..8aa19397086 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -5,11 +5,14 @@ class Packages::PackageFile < ApplicationRecord
delegate :project, :project_id, to: :package
delegate :conan_file_type, to: :conan_file_metadatum
- delegate :file_type, :component, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
+ delegate :file_type, :dsc?, :component, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
delegate :channel, :metadata, to: :helm_file_metadatum, prefix: :helm
belongs_to :package
+ # used to move the linked file within object storage
+ attribute :new_file_path, default: nil
+
has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum'
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
has_many :pipelines, through: :package_file_build_infos
@@ -33,6 +36,8 @@ class Packages::PackageFile < ApplicationRecord
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :with_format, ->(format) { where(::Packages::PackageFile.arel_table[:file_name].matches("%.#{format}")) }
+
+ scope :preload_package, -> { preload(:package) }
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) }
@@ -78,6 +83,12 @@ class Packages::PackageFile < ApplicationRecord
before_save :update_size_from_file
+ # if a new_file_path is provided, we need
+ # * disable the remove_previously_stored_file callback so that carrierwave doesn't take care of the file
+ # * enable a new after_commit callback that will move the file in object storage
+ skip_callback :commit, :after, :remove_previously_stored_file, if: :execute_move_in_object_storage?
+ after_commit :move_in_object_storage, if: :execute_move_in_object_storage?
+
def download_path
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
end
@@ -87,6 +98,17 @@ class Packages::PackageFile < ApplicationRecord
def update_size_from_file
self.size ||= file.size
end
+
+ def execute_move_in_object_storage?
+ !file.file_storage? && new_file_path?
+ end
+
+ def move_in_object_storage
+ carrierwave_file = file.file
+
+ carrierwave_file.copy_to(new_file_path)
+ carrierwave_file.delete
+ end
end
Packages::PackageFile.prepend_mod_with('Packages::PackageFile')
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 732ed0b7bb3..1778e927dd1 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -47,6 +47,10 @@ class PersonalAccessToken < ApplicationRecord
!revoked? && !expired?
end
+ def expired_but_not_enforced?
+ false
+ end
+
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
redis_key = redis_shared_state_key(user_id)
diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb
new file mode 100644
index 00000000000..76b299ff9d4
--- /dev/null
+++ b/app/models/postgresql/detached_partition.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Postgresql
+ class DetachedPartition < ApplicationRecord
+ scope :ready_to_drop, -> { where('drop_after < ?', Time.current) }
+ end
+end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index 77b42c34ad9..1a4d3bd5794 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -39,5 +39,55 @@ module Postgresql
false
end
end
+
+ def self.count
+ connection
+ .execute("SELECT COUNT(*) FROM pg_replication_slots;")
+ .first
+ .fetch('count')
+ .to_i
+ end
+
+ def self.unused_slots_count
+ connection
+ .execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 'f';")
+ .first
+ .fetch('count')
+ .to_i
+ end
+
+ def self.used_slots_count
+ connection
+ .execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 't';")
+ .first
+ .fetch('count')
+ .to_i
+ end
+
+ # array of slots and the retained_bytes
+ # https://www.skillslogic.com/blog/databases/checking-postgres-replication-lag
+ # http://bdr-project.org/docs/stable/monitoring-peers.html
+ def self.slots_retained_bytes
+ connection.execute(<<-SQL.squish).to_a
+ SELECT slot_name, database,
+ active, pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)
+ AS retained_bytes
+ FROM pg_replication_slots;
+ SQL
+ end
+
+ # returns the max number WAL space (in bytes) being used across the replication slots
+ def self.max_retained_wal
+ connection.execute(<<-SQL.squish).first.fetch('coalesce').to_i
+ SELECT COALESCE(MAX(pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)), 0)
+ FROM pg_replication_slots;
+ SQL
+ end
+
+ def self.max_replication_slots
+ connection.execute(<<-SQL.squish).first&.fetch('setting').to_i
+ SELECT setting FROM pg_settings WHERE name = 'max_replication_slots';
+ SQL
+ end
end
end
diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
index c0ed56057ae..3764e9dcb16 100644
--- a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -10,9 +10,13 @@ module Preloaders
end
def execute
+ # Use reselect to override the existing select to prevent
+ # the error `subquery has too many columns`
+ # NotificationsController passes in an Array so we need to check the type
+ project_ids = @projects.is_a?(ActiveRecord::Relation) ? @projects.reselect(:id) : @projects
access_levels = @user
.project_authorizations
- .where(project_id: @projects)
+ .where(project_id: project_ids)
.group(:project_id)
.maximum(:access_level)
diff --git a/app/models/project.rb b/app/models/project.rb
index c5522737b87..81b04e1316c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -43,8 +43,13 @@ class Project < ApplicationRecord
extend Gitlab::ConfigHelper
+ ignore_columns :container_registry_enabled, remove_after: '2021-09-22', remove_with: '14.4'
+
BoardLimitExceeded = Class.new(StandardError)
+ ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4'
+ ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4'
+
STATISTICS_ATTRIBUTE = 'repositories_count'
UNKNOWN_IMPORT_URL = 'http://unknown.git'
# Hashed Storage versions handle rolling out new storage to project and dependents models:
@@ -73,7 +78,6 @@ class Project < ApplicationRecord
default_value_for :packages_enabled, true
default_value_for :archived, false
default_value_for :resolve_outdated_diff_discussions, false
- default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) do
Repository.pick_storage_shard
end
@@ -95,9 +99,6 @@ class Project < ApplicationRecord
before_save :ensure_runners_token
- # https://api.rubyonrails.org/v6.0.3.4/classes/ActiveRecord/AttributeMethods/Dirty.html#method-i-will_save_change_to_attribute-3F
- before_update :set_container_registry_access_level, if: :will_save_change_to_container_registry_enabled?
-
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
@@ -318,7 +319,6 @@ class Project < ApplicationRecord
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
- has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
has_many :job_artifacts, class_name: 'Ci::JobArtifact'
@@ -378,6 +378,7 @@ class Project < ApplicationRecord
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error'
+ has_many :error_tracking_client_keys, inverse_of: :project, class_name: 'ErrorTracking::ClientKey'
has_many :timelogs
@@ -436,7 +437,7 @@ class Project < ApplicationRecord
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
- :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?,
+ :allow_merge_on_skipped_pipeline=, :has_confluence?,
to: :project_setting
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
@@ -538,10 +539,8 @@ class Project < ApplicationRecord
scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
scope :archived, -> { where(archived: true) }
scope :non_archived, -> { where(archived: false) }
- scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
- scope :with_active_jira_integrations, -> { joins(:integrations).merge(::Integrations::Jira.active) }
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) }
@@ -549,7 +548,9 @@ class Project < ApplicationRecord
scope :with_namespace, -> { includes(:namespace) }
scope :with_import_state, -> { includes(:import_state) }
scope :include_project_feature, -> { includes(:project_feature) }
- scope :with_integration, ->(integration) { joins(integration).eager_load(integration) }
+ scope :include_integration, -> (integration_association_name) { includes(integration_association_name) }
+ scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) }
+ scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
@@ -913,7 +914,13 @@ class Project < ApplicationRecord
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
- alias_method :ancestors, :ancestors_upto
+ def ancestors(hierarchy_order: nil)
+ if Feature.enabled?(:linear_project_ancestors, self, default_enabled: :yaml)
+ group&.self_and_ancestors(hierarchy_order: hierarchy_order) || Group.none
+ else
+ ancestors_upto(hierarchy_order: hierarchy_order)
+ end
+ end
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)
@@ -1180,6 +1187,15 @@ class Project < ApplicationRecord
import_type == 'gitea'
end
+ def github_import?
+ import_type == 'github'
+ end
+
+ def github_enterprise_import?
+ github_import? &&
+ URI.parse(import_url).host != URI.parse(Octokit::Default::API_ENDPOINT).host
+ end
+
def has_remote_mirror?
remote_mirror_available? && remote_mirrors.enabled.exists?
end
@@ -1411,14 +1427,13 @@ class Project < ApplicationRecord
def find_or_initialize_integration(name)
return if disabled_integrations.include?(name)
- find_integration(integrations, name) || build_from_instance_or_template(name) || build_integration(name)
+ find_integration(integrations, name) || build_from_instance(name) || build_integration(name)
end
# rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
- # TODO: remove_on_close exception can be removed after the column is dropped from all envs
- params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type', 'remove_on_close')
+ params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
@@ -1876,11 +1891,11 @@ class Project < ApplicationRecord
.update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
end
- def write_repository_config(gl_full_path: full_path)
+ def set_full_path(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
- repository.raw_repository.write_config(full_path: gl_full_path)
+ repository.raw_repository.set_full_path(full_path: gl_full_path)
rescue Gitlab::Git::Repository::NoRepository => e
Gitlab::AppLogger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
@@ -1892,6 +1907,7 @@ class Project < ApplicationRecord
DetectRepositoryLanguagesWorker.perform_async(id)
ProjectCacheWorker.perform_async(self.id, [], [:repository_size])
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(id)
# The import assigns iid values on its own, e.g. by re-using GitHub ids.
# Flush existing InternalId records for this project for consistency reasons.
@@ -1904,7 +1920,7 @@ class Project < ApplicationRecord
after_create_default_branch
join_pool_repository
refresh_markdown_cache!
- write_repository_config
+ set_full_path
end
def update_project_counter_caches
@@ -2030,6 +2046,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
.append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase)
+ .append(key: 'CI_PROJECT_CLASSIFICATION_LABEL', value: external_authorization_classification_label)
.append(key: 'CI_DEFAULT_BRANCH', value: default_branch)
.append(key: 'CI_CONFIG_PATH', value: ci_config_path_or_default)
end
@@ -2557,12 +2574,15 @@ class Project < ApplicationRecord
[project&.id, root_group&.id]
end
- def package_already_taken?(package_name)
- namespace.root_ancestor.all_projects
- .joins(:packages)
- .where.not(id: id)
- .merge(Packages::Package.default_scoped.with_name(package_name))
- .exists?
+ def package_already_taken?(package_name, package_version, package_type:)
+ Packages::Package.with_name(package_name)
+ .with_version(package_version)
+ .with_package_type(package_type)
+ .for_projects(
+ root_ancestor.all_projects
+ .id_not_in(id)
+ .select(:id)
+ ).exists?
end
def default_branch_or_main
@@ -2651,40 +2671,22 @@ class Project < ApplicationRecord
private
- def set_container_registry_access_level
- # changes_to_save = { 'container_registry_enabled' => [value_before_update, value_after_update] }
- value = changes_to_save['container_registry_enabled'][1]
-
- access_level =
- if value
- ProjectFeature::ENABLED
- else
- ProjectFeature::DISABLED
- end
-
- project_feature.update!(container_registry_access_level: access_level)
- end
-
def find_integration(integrations, name)
integrations.find { _1.to_param == name }
end
- def build_from_instance_or_template(name)
+ def build_from_instance(name)
instance = find_integration(integration_instances, name)
- return Integration.build_from_integration(instance, project_id: id) if instance
- template = find_integration(integration_templates, name)
- return Integration.build_from_integration(template, project_id: id) if template
+ return unless instance
+
+ Integration.build_from_integration(instance, project_id: id)
end
def build_integration(name)
Integration.integration_name_to_model(name).new(project_id: id)
end
- def integration_templates
- @integration_templates ||= Integration.for_template
- end
-
def integration_instances
@integration_instances ||= Integration.for_instance
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index f6e889396c6..aea8abecd74 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -2,6 +2,7 @@
class ProjectFeature < ApplicationRecord
include Featurable
+ extend Gitlab::ConfigHelper
# When updating this array, make sure to update rubocop/cop/gitlab/feature_available_usage.rb as well.
FEATURES = %i[
@@ -48,12 +49,7 @@ class ProjectFeature < ApplicationRecord
end
end
- before_create :set_container_registry_access_level
-
- # Default scopes force us to unscope here since a service may need to check
- # permissions for a project in pending_delete
- # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
- belongs_to :project, -> { unscope(where: :pending_delete) }
+ belongs_to :project
validates :project, presence: true
@@ -80,6 +76,14 @@ class ProjectFeature < ApplicationRecord
end
end
+ default_value_for(:container_registry_access_level) do |feature|
+ if gitlab_config_features.container_registry
+ ENABLED
+ else
+ DISABLED
+ end
+ end
+
def public_pages?
return true unless Gitlab.config.pages.access_control
@@ -94,15 +98,6 @@ class ProjectFeature < ApplicationRecord
private
- def set_container_registry_access_level
- self.container_registry_access_level =
- if project&.read_attribute(:container_registry_enabled)
- ENABLED
- else
- DISABLED
- end
- end
-
# Validates builds and merge requests access level
# which cannot be higher than repository access level
def repository_children_level
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 24d892290a6..b2559636f32 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
+ include IgnorableColumns
+
+ ignore_column :allow_editing_commit_messages, remove_with: '14.4', remove_after: '2021-09-10'
+
belongs_to :project, inverse_of: :project_setting
enum squash_option: {
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 4586aa2b4b4..4ae3bc01a01 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -42,7 +42,7 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -52,7 +52,7 @@ class ProjectTeam
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.new(project, # rubocop:todo CodeReuse/ServiceClass
+ Members::Projects::CreatorService.new(project, # rubocop:disable CodeReuse/ServiceClass
user,
access_level,
current_user: current_user,
@@ -78,6 +78,10 @@ class ProjectTeam
members.where(id: member_user_ids)
end
+ def members_with_access_levels(access_levels = [])
+ fetch_members(access_levels)
+ end
+
def guests
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
diff --git a/app/models/projects/ci_feature_usage.rb b/app/models/projects/ci_feature_usage.rb
new file mode 100644
index 00000000000..a10426e50c9
--- /dev/null
+++ b/app/models/projects/ci_feature_usage.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Projects
+ class CiFeatureUsage < ApplicationRecord
+ self.table_name = 'project_ci_feature_usages'
+
+ belongs_to :project
+
+ validates :project, :feature, presence: true
+
+ enum feature: {
+ code_coverage: 1,
+ security_report: 2
+ }
+
+ def self.insert_usage(project_id:, feature:, default_branch:)
+ insert(
+ {
+ project_id: project_id,
+ feature: feature,
+ default_branch: default_branch
+ },
+ unique_by: 'index_project_ci_feature_usages_unique_columns'
+ )
+ end
+ end
+end
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 84e0a43670b..17a9ad7db66 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -49,8 +49,12 @@ class ReleaseHighlight
end
def self.file_paths
- @file_paths ||= Rails.cache.fetch(self.cache_key('file_paths'), expires_in: CACHE_DURATION) do
- Dir.glob(FILES_PATH).sort.reverse
+ @file_paths ||= self.relative_file_paths.map { |path| path.prepend(Rails.root.to_s) }
+ end
+
+ def self.relative_file_paths
+ Rails.cache.fetch(self.cache_key('file_paths'), expires_in: CACHE_DURATION) do
+ Dir.glob(FILES_PATH).sort.reverse.map { |path| path.delete_prefix(Rails.root.to_s) }
end
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index a700f104150..7f41f0907d5 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -22,14 +22,9 @@ class RemoteMirror < ApplicationRecord
validates :url, presence: true, public_url: { schemes: %w(ssh git http https), allow_blank: true, enforce_user: true }
- before_save :set_new_remote_name, if: :mirror_url_changed?
-
after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
- after_save :refresh_remote, if: :saved_change_to_mirror_url?
after_update :reset_fields, if: :saved_change_to_mirror_url?
- after_commit :remove_remote, on: :destroy
-
before_validation :store_credentials
scope :enabled, -> { where(enabled: true) }
@@ -88,10 +83,6 @@ class RemoteMirror < ApplicationRecord
end
end
- def remote_name
- super || fallback_remote_name
- end
-
def update_failed?
update_status == 'failed'
end
@@ -100,11 +91,10 @@ class RemoteMirror < ApplicationRecord
update_status == 'started'
end
- def update_repository(inmemory_remote:)
+ def update_repository
Gitlab::Git::RemoteMirror.new(
project.repository.raw,
- remote_name,
- inmemory_remote ? remote_url : nil,
+ remote_url,
**options_for_update
).update
end
@@ -227,15 +217,6 @@ class RemoteMirror < ApplicationRecord
Gitlab::UrlSanitizer.new(read_attribute(:url)).full_url
end
- def ensure_remote!
- return unless project
- return unless remote_name && remote_url
-
- # If this fails or the remote already exists, we won't know due to
- # https://gitlab.com/gitlab-org/gitaly/issues/1317
- project.repository.add_remote(remote_name, remote_url)
- end
-
def after_sent_notification
update_column(:error_notification_sent, true)
end
@@ -280,12 +261,6 @@ class RemoteMirror < ApplicationRecord
super
end
- def fallback_remote_name
- return unless id
-
- "remote_mirror_#{id}"
- end
-
def recently_scheduled?
return false unless self.last_update_started_at
@@ -308,29 +283,6 @@ class RemoteMirror < ApplicationRecord
project.update(remote_mirror_available_overridden: enabled)
end
- def set_new_remote_name
- self.remote_name = "remote_mirror_#{SecureRandom.hex}"
- end
-
- def refresh_remote
- return unless project
-
- # Before adding a new remote we have to delete the data from
- # the previous remote name
- prev_remote_name = remote_name_before_last_save || fallback_remote_name
- run_after_commit do
- project.repository.async_remove_remote(prev_remote_name)
- end
-
- project.repository.add_remote(remote_name, remote_url)
- end
-
- def remove_remote
- return unless project # could be pending to delete so don't need to touch the git repository
-
- project.repository.async_remove_remote(remote_name)
- end
-
def mirror_url_changed?
url_changed? || attribute_changed?(:credentials)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a77aaf02e06..0164d6fed93 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -168,8 +168,8 @@ class Repository
end
# Returns a list of commits that are not present in any reference
- def new_commits(newrev)
- commits = raw.new_commits(newrev)
+ def new_commits(newrev, allow_quarantine: false)
+ commits = raw.new_commits(newrev, allow_quarantine: allow_quarantine)
::Commit.decorate(commits, container)
end
@@ -502,8 +502,8 @@ class Repository
end
end
- def blob_at(sha, path)
- Blob.decorate(raw_repository.blob_at(sha, path), container)
+ def blob_at(sha, path, limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ Blob.decorate(raw_repository.blob_at(sha, path, limit: limit), container)
rescue Gitlab::Git::Repository::NoRepository
nil
end
@@ -656,7 +656,7 @@ class Repository
end
end
- def tree(sha = :head, path = nil, recursive: false)
+ def tree(sha = :head, path = nil, recursive: false, pagination_params: nil)
if sha == :head
return unless head_commit
@@ -667,7 +667,7 @@ class Repository
end
end
- Tree.new(self, sha, path, recursive: recursive)
+ Tree.new(self, sha, path, recursive: recursive, pagination_params: pagination_params)
end
def blob_at_branch(branch_name, path)
@@ -938,33 +938,8 @@ class Repository
end
end
- def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true)
- return fetch_remote(remote_name, url: url, refmap: refmap, forced: forced, prune: prune) if Feature.enabled?(:fetch_remote_params, project, default_enabled: :yaml)
-
- unless remote_name
- remote_name = "tmp-#{SecureRandom.hex}"
- tmp_remote_name = true
- end
-
- add_remote(remote_name, url, mirror_refmap: refmap)
- fetch_remote(remote_name, forced: forced, prune: prune)
- ensure
- async_remove_remote(remote_name) if tmp_remote_name
- end
-
- def async_remove_remote(remote_name)
- return unless remote_name
- return unless project
-
- job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
-
- if job_id
- Gitlab::AppLogger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
- else
- Gitlab::AppLogger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
- end
-
- job_id
+ def fetch_as_mirror(url, forced: false, refmap: :all_refs, prune: true, http_authorization_header: "")
+ fetch_remote(url, refmap: refmap, forced: forced, prune: prune, http_authorization_header: http_authorization_header)
end
def fetch_source_branch!(source_repository, source_branch, local_ref)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 68957dd6b22..dd76f2c3c84 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -246,7 +246,7 @@ class Snippet < ApplicationRecord
notes.includes(:author)
end
- def check_for_spam?
+ def check_for_spam?(user:)
visibility_level_changed?(to: Snippet::PUBLIC) ||
(public? && (title_changed? || content_changed?))
end
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 8aeeae1330c..8c3b85ac4c3 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -20,7 +20,6 @@ module Terraform
foreign_key: :terraform_state_id,
inverse_of: :terraform_state
- scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index 3f0e827cf61..7c394736560 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -19,6 +19,14 @@ class Timelog < ApplicationRecord
joins(:project).where(projects: { namespace: group.self_and_descendants })
end
+ scope :in_project, -> (project) do
+ where(project: project)
+ end
+
+ scope :for_user, -> (user) do
+ where(user: user)
+ end
+
scope :at_or_after, -> (start_time) do
where('spent_at >= ?', start_time)
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index cd385872171..fd416ebdedc 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -4,9 +4,9 @@ class Tree
include Gitlab::MarkupHelper
include Gitlab::Utils::StrongMemoize
- attr_accessor :repository, :sha, :path, :entries
+ attr_accessor :repository, :sha, :path, :entries, :cursor
- def initialize(repository, sha, path = '/', recursive: false)
+ def initialize(repository, sha, path = '/', recursive: false, pagination_params: nil)
path = '/' if path.blank?
@repository = repository
@@ -14,7 +14,7 @@ class Tree
@path = path
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
+ @entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, pagination_params)
end
def readme_path
diff --git a/app/models/user.rb b/app/models/user.rb
index 80b8c9173d1..cb0f15c04cb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -26,6 +26,7 @@ class User < ApplicationRecord
include UpdateHighestRole
include HasUserType
include Gitlab::Auth::Otp::Fortinet
+ include RestrictedSignup
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -205,11 +206,14 @@ class User < ApplicationRecord
has_one :user_canonical_email
has_one :credit_card_validation, class_name: '::Users::CreditCardValidation'
has_one :atlassian_identity, class_name: 'Atlassian::Identity'
+ has_one :banned_user, class_name: '::Users::BannedUser'
has_many :reviews, foreign_key: :author_id, inverse_of: :author
has_many :in_product_marketing_emails, class_name: '::Users::InProductMarketingEmail'
+ has_many :timelogs
+
#
# Validations
#
@@ -220,7 +224,7 @@ class User < ApplicationRecord
validates :email, confirmation: true
validates :notification_email, presence: true
validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email }
- validates :public_email, presence: true, uniqueness: true, devise_email: true, allow_blank: true
+ validates :public_email, uniqueness: true, devise_email: true, allow_blank: true
validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email }
validates :projects_limit,
presence: true,
@@ -231,11 +235,10 @@ class User < ApplicationRecord
validate :namespace_move_dir_allowed, if: :username_changed?
validate :unique_email, if: :email_changed?
- validate :owns_notification_email, if: :notification_email_changed?
- validate :owns_public_email, if: :public_email_changed?
- validate :owns_commit_email, if: :commit_email_changed?
- validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
- validate :check_email_restrictions, on: :create, if: ->(user) { !user.created_by_id }
+ validate :notification_email_verified, if: :notification_email_changed?
+ validate :public_email_verified, if: :public_email_changed?
+ validate :commit_email_verified, if: :commit_email_changed?
+ validate :signup_email_valid?, on: :create, if: ->(user) { !user.created_by_id }
validate :check_username_format, if: :username_changed?
validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids,
@@ -245,7 +248,6 @@ class User < ApplicationRecord
message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
before_validation :sanitize_attrs
- before_validation :set_notification_email, if: :new_record?
before_validation :set_public_email, if: :public_email_changed?
before_validation :set_commit_email, if: :commit_email_changed?
before_save :default_private_profile_to_false
@@ -270,11 +272,6 @@ class User < ApplicationRecord
update_emails_with_primary_email(previous_confirmed_at, previous_email)
update_invalid_gpg_signatures
-
- if previous_email == notification_email
- self.notification_email = email
- save
- end
end
end
@@ -315,6 +312,7 @@ class User < ApplicationRecord
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
+ delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -326,7 +324,6 @@ class User < ApplicationRecord
transition deactivated: :blocked
transition ldap_blocked: :blocked
transition blocked_pending_approval: :blocked
- transition banned: :blocked
end
event :ldap_block do
@@ -380,6 +377,14 @@ class User < ApplicationRecord
NotificationService.new.user_deactivated(user.name, user.notification_email)
end
# rubocop: enable CodeReuse/ServiceClass
+
+ after_transition active: :banned do |user|
+ user.create_banned_user
+ end
+
+ after_transition banned: :active do |user|
+ user.banned_user&.destroy
+ end
end
# Scopes
@@ -917,22 +922,22 @@ class User < ApplicationRecord
end
end
- def owns_notification_email
- return if new_record? || temp_oauth_email?
+ def notification_email_verified
+ return if read_attribute(:notification_email).blank? || temp_oauth_email?
- errors.add(:notification_email, _("is not an email you own")) unless verified_emails.include?(notification_email)
+ errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
end
- def owns_public_email
+ def public_email_verified
return if public_email.blank?
- errors.add(:public_email, _("is not an email you own")) unless verified_emails.include?(public_email)
+ errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
end
- def owns_commit_email
+ def commit_email_verified
return if read_attribute(:commit_email).blank?
- errors.add(:commit_email, _("is not an email you own")) unless verified_emails.include?(commit_email)
+ errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
end
# Define commit_email-related attribute methods explicitly instead of relying
@@ -959,6 +964,11 @@ class User < ApplicationRecord
has_attribute?(:commit_email) && super
end
+ def notification_email
+ # The notification email is the same as the primary email if undefined
+ super.presence || self.email
+ end
+
def private_commit_email
Gitlab::PrivateCommitEmail.for_user(self)
end
@@ -1005,6 +1015,8 @@ class User < ApplicationRecord
# Returns a relation of groups the user has access to, including their parent
# and child groups (recursively).
def all_expanded_groups
+ return groups if groups.empty?
+
Gitlab::ObjectHierarchy.new(groups).all_objects
end
@@ -1576,10 +1588,11 @@ class User < ApplicationRecord
.order('routes.path')
end
- def namespaces
- namespace_ids = groups.pluck(:id)
- namespace_ids.push(namespace.id)
- Namespace.where(id: namespace_ids)
+ def namespaces(owned_only: false)
+ user_groups = owned_only ? owned_groups : groups
+ personal_namespace = Namespace.where(id: namespace.id)
+
+ Namespace.from_union([user_groups, personal_namespace])
end
def oauth_authorized_tokens
@@ -2008,8 +2021,8 @@ class User < ApplicationRecord
def authorized_groups_without_shared_membership
Group.from_union([
- groups,
- authorized_projects.joins(:namespace).select('namespaces.*')
+ groups.select(Namespace.arel_table[Arel.star]),
+ authorized_projects.joins(:namespace).select(Namespace.arel_table[Arel.star])
])
end
@@ -2058,51 +2071,10 @@ class User < ApplicationRecord
end
end
- def signup_domain_valid?
- valid = true
- error = nil
-
- if Gitlab::CurrentSettings.domain_denylist_enabled?
- blocked_domains = Gitlab::CurrentSettings.domain_denylist
- if domain_matches?(blocked_domains, email)
- error = 'is not from an allowed domain.'
- valid = false
- end
- end
-
- allowed_domains = Gitlab::CurrentSettings.domain_allowlist
- unless allowed_domains.blank?
- if domain_matches?(allowed_domains, email)
- valid = true
- else
- error = "domain is not authorized for sign-up"
- valid = false
- end
- end
-
- errors.add(:email, error) unless valid
-
- valid
- end
-
- def domain_matches?(email_domains, email)
- signup_domain = Mail::Address.new(email).domain
- email_domains.any? do |domain|
- escaped = Regexp.escape(domain).gsub('\*', '.*?')
- regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
- signup_domain =~ regexp
- end
- end
+ def signup_email_valid?
+ error = validate_admin_signup_restrictions(email)
- def check_email_restrictions
- return unless Gitlab::CurrentSettings.email_restrictions_enabled?
-
- restrictions = Gitlab::CurrentSettings.email_restrictions
- return if restrictions.blank?
-
- if Gitlab::UntrustedRegexp.new(restrictions).match?(email)
- errors.add(:email, _('is not allowed. Try again with a different email address, or contact your GitLab admin.'))
- end
+ errors.add(:email, error) if error
end
def check_username_format
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 854992dcd1e..1172b2ee5e8 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -16,7 +16,6 @@ class UserCallout < ApplicationRecord
tabs_position_highlight: 10,
threat_monitoring_info: 11, # EE-only
account_recovery_regular_check: 12, # EE-only
- service_templates_deprecated_callout: 14,
web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
@@ -35,7 +34,9 @@ class UserCallout < ApplicationRecord
cloud_licensing_subscription_activation_banner: 33, # EE-only
trial_status_reminder_d14: 34, # EE-only
trial_status_reminder_d3: 35, # EE-only
- security_configuration_devops_alert: 36 # EE-only
+ security_configuration_devops_alert: 36, # EE-only
+ profile_personal_access_token_expiry: 37, # EE-only
+ terraform_notification_dismissed: 38
}
validates :user, presence: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 47537e5885f..b3cca1e0cc0 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -7,6 +7,7 @@ class UserDetail < ApplicationRecord
belongs_to :user
validates :pronouns, length: { maximum: 50 }
+ validates :pronunciation, length: { maximum: 255 }
validates :job_title, length: { maximum: 200 }
validates :bio, length: { maximum: 255 }, allow_blank: true
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 4c8cc5fc83a..1c7515894fe 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -24,16 +24,8 @@ class UserInteractedProject < ApplicationRecord
}
cached_exists?(**attributes) do
- transaction(requires_new: true) do
- where(attributes).select(1).first || create!(attributes)
- true # not caching the whole record here for now
- rescue ActiveRecord::RecordNotUnique
- # Note, above queries are not atomic and prone
- # to race conditions (similar like #find_or_create!).
- # In the case where we hit this, the record we want
- # already exists - shortcut and return.
- true
- end
+ where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w(project_id user_id))
+ true
end
end
diff --git a/app/models/users/banned_user.rb b/app/models/users/banned_user.rb
new file mode 100644
index 00000000000..c52b6d4b728
--- /dev/null
+++ b/app/models/users/banned_user.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Users
+ class BannedUser < ApplicationRecord
+ self.primary_key = :user_id
+
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :user_id, uniqueness: { message: _("banned user already exists") }
+ end
+end
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index 3e5e7b259d8..8fe52ac7ecc 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -19,7 +19,10 @@ module Users
verify: 1,
trial: 2,
team: 3,
- experience: 4
+ experience: 4,
+ team_short: 5,
+ trial_short: 6,
+ admin_verify: 7
}, _suffix: true
scope :without_track_and_series, -> (track, series) do
diff --git a/app/models/work_item/type.rb b/app/models/work_item/type.rb
new file mode 100644
index 00000000000..16cb7a8be45
--- /dev/null
+++ b/app/models/work_item/type.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# Note: initial thinking behind `icon_name` is for it to do triple duty:
+# 1. one of our svg icon names, such as `external-link` or a new one `bug`
+# 2. if it's an absolute url, then url to a user uploaded icon/image
+# 3. an emoji, with the format of `:smile:`
+class WorkItem::Type < ApplicationRecord
+ self.table_name = 'work_item_types'
+
+ include CacheMarkdownField
+
+ cache_markdown_field :description, pipeline: :single_line
+
+ enum base_type: {
+ issue: 0,
+ incident: 1,
+ test_case: 2, ## EE-only
+ requirement: 3 ## EE-only
+ }
+
+ belongs_to :namespace, optional: true
+ has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type
+
+ before_validation :strip_whitespace
+
+ # TODO: review validation rules
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336919
+ validates :name, presence: true
+ validates :name, uniqueness: { case_sensitive: false, scope: [:namespace_id] }
+ validates :name, length: { maximum: 255 }
+ validates :icon_name, length: { maximum: 255 }
+
+ private
+
+ def strip_whitespace
+ name&.strip!
+ end
+end