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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /app/models
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/alert_management/alert.rb80
-rw-r--r--app/models/alert_management/http_integration.rb41
-rw-r--r--app/models/analytics/instance_statistics/measurement.rb22
-rw-r--r--app/models/application_record.rb14
-rw-r--r--app/models/application_setting.rb16
-rw-r--r--app/models/application_setting/term.rb2
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/models/audit_event.rb18
-rw-r--r--app/models/authentication_event.rb10
-rw-r--r--app/models/blob_viewer/balsamiq.rb2
-rw-r--r--app/models/blob_viewer/markup.rb10
-rw-r--r--app/models/blob_viewer/pdf.rb2
-rw-r--r--app/models/blob_viewer/sketch.rb2
-rw-r--r--app/models/bulk_import.rb16
-rw-r--r--app/models/bulk_imports/configuration.rb20
-rw-r--r--app/models/bulk_imports/entity.rb43
-rw-r--r--app/models/ci/bridge.rb30
-rw-r--r--app/models/ci/build.rb45
-rw-r--r--app/models/ci/build_pending_state.rb6
-rw-r--r--app/models/ci/build_trace_chunk.rb104
-rw-r--r--app/models/ci/build_trace_chunks/database.rb2
-rw-r--r--app/models/ci/deleted_object.rb37
-rw-r--r--app/models/ci/job_artifact.rb32
-rw-r--r--app/models/ci/pipeline.rb46
-rw-r--r--app/models/ci_platform_metric.rb2
-rw-r--r--app/models/clusters/agent.rb1
-rw-r--r--app/models/clusters/applications/fluentd.rb6
-rw-r--r--app/models/clusters/applications/ingress.rb7
-rw-r--r--app/models/clusters/applications/prometheus.rb8
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb3
-rw-r--r--app/models/commit.rb32
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/approvable_base.rb24
-rw-r--r--app/models/concerns/avatarable.rb15
-rw-r--r--app/models/concerns/checksummable.rb8
-rw-r--r--app/models/concerns/counter_attribute.rb29
-rw-r--r--app/models/concerns/has_repository.rb10
-rw-r--r--app/models/concerns/has_user_type.rb6
-rw-r--r--app/models/concerns/integration.rb4
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/issue_available_features.rb23
-rw-r--r--app/models/concerns/mentionable.rb16
-rw-r--r--app/models/concerns/presentable.rb4
-rw-r--r--app/models/concerns/reactive_caching.rb5
-rw-r--r--app/models/concerns/reactive_service.rb1
-rw-r--r--app/models/concerns/referable.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb43
-rw-r--r--app/models/concerns/shardable.rb4
-rw-r--r--app/models/concerns/timebox.rb26
-rw-r--r--app/models/concerns/update_project_statistics.rb5
-rw-r--r--app/models/container_expiration_policy.rb11
-rw-r--r--app/models/container_repository.rb8
-rw-r--r--app/models/data_list.rb10
-rw-r--r--app/models/deploy_token.rb14
-rw-r--r--app/models/deployment.rb34
-rw-r--r--app/models/deployment_merge_request.rb21
-rw-r--r--app/models/design_management/design.rb10
-rw-r--r--app/models/design_management/design_at_version.rb4
-rw-r--r--app/models/design_management/design_collection.rb1
-rw-r--r--app/models/diff_viewer/rich.rb2
-rw-r--r--app/models/environment.rb11
-rw-r--r--app/models/environment_status.rb8
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/global_label.rb32
-rw-r--r--app/models/group.rb120
-rw-r--r--app/models/group_import_state.rb3
-rw-r--r--app/models/incident_management/project_incident_management_setting.rb2
-rw-r--r--app/models/issuable_severity.rb7
-rw-r--r--app/models/issue.rb18
-rw-r--r--app/models/issue_assignee.rb1
-rw-r--r--app/models/issue_email_participant.rb13
-rw-r--r--app/models/iteration.rb16
-rw-r--r--app/models/member.rb13
-rw-r--r--app/models/merge_request.rb36
-rw-r--r--app/models/merge_request_context_commit.rb4
-rw-r--r--app/models/merge_request_diff.rb25
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/milestone_release.rb4
-rw-r--r--app/models/namespace.rb57
-rw-r--r--app/models/namespace_setting.rb19
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_reason.rb2
-rw-r--r--app/models/notification_recipient.rb2
-rw-r--r--app/models/notification_setting.rb1
-rw-r--r--app/models/operations/feature_flags/strategy.rb33
-rw-r--r--app/models/packages/event.rb25
-rw-r--r--app/models/packages/package.rb14
-rw-r--r--app/models/pages_deployment.rb14
-rw-r--r--app/models/postgresql/replication_slot.rb4
-rw-r--r--app/models/preloaders/merge_request_diff_preloader.rb27
-rw-r--r--app/models/project.rb57
-rw-r--r--app/models/project_pages_metadatum.rb1
-rw-r--r--app/models/project_repository_storage_move.rb10
-rw-r--r--app/models/project_services/chat_message/deployment_message.rb10
-rw-r--r--app/models/project_services/chat_message/issue_message.rb10
-rw-r--r--app/models/project_services/confluence_service.rb2
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_services/packagist_service.rb2
-rw-r--r--app/models/project_statistics.rb31
-rw-r--r--app/models/project_tracing_setting.rb15
-rw-r--r--app/models/project_wiki.rb3
-rw-r--r--app/models/prometheus_alert.rb2
-rw-r--r--app/models/prometheus_metric.rb2
-rw-r--r--app/models/release.rb17
-rw-r--r--app/models/repository.rb24
-rw-r--r--app/models/resource_label_event.rb11
-rw-r--r--app/models/resource_state_event.rb25
-rw-r--r--app/models/resource_timebox_event.rb15
-rw-r--r--app/models/resource_weight_event.rb7
-rw-r--r--app/models/service.rb29
-rw-r--r--app/models/service_list.rb12
-rw-r--r--app/models/snippet.rb8
-rw-r--r--app/models/snippet_input_action_collection.rb6
-rw-r--r--app/models/snippet_repository.rb2
-rw-r--r--app/models/snippet_statistics.rb2
-rw-r--r--app/models/system_note_metadata.rb4
-rw-r--r--app/models/terraform/state.rb68
-rw-r--r--app/models/terraform/state_version.rb6
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/u2f_registration.rb22
-rw-r--r--app/models/user.rb50
-rw-r--r--app/models/user_callout.rb2
-rw-r--r--app/models/user_interacted_project.rb2
-rw-r--r--app/models/user_preference.rb3
-rw-r--r--app/models/vulnerability.rb10
-rw-r--r--app/models/wiki.rb46
-rw-r--r--app/models/wiki_directory.rb39
-rw-r--r--app/models/wiki_page.rb23
129 files changed, 1677 insertions, 462 deletions
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index e9b89af45c6..61cc15a522e 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -20,18 +20,7 @@ module AlertManagement
resolved: 2,
ignored: 3
}.freeze
-
- STATUS_EVENTS = {
- triggered: :trigger,
- acknowledged: :acknowledge,
- resolved: :resolve,
- ignored: :ignore
- }.freeze
-
- OPEN_STATUSES = [
- :triggered,
- :acknowledged
- ].freeze
+ private_constant :STATUSES
belongs_to :project
belongs_to :issue, optional: true
@@ -49,12 +38,16 @@ module AlertManagement
sha_attribute :fingerprint
+ TITLE_MAX_LENGTH = 200
+ DESCRIPTION_MAX_LENGTH = 1_000
+ SERVICE_MAX_LENGTH = 100
+ TOOL_MAX_LENGTH = 100
HOSTS_MAX_LENGTH = 255
- validates :title, length: { maximum: 200 }, presence: true
- validates :description, length: { maximum: 1_000 }
- validates :service, length: { maximum: 100 }
- validates :monitoring_tool, length: { maximum: 100 }
+ validates :title, length: { maximum: TITLE_MAX_LENGTH }, presence: true
+ validates :description, length: { maximum: DESCRIPTION_MAX_LENGTH }
+ validates :service, length: { maximum: SERVICE_MAX_LENGTH }
+ validates :monitoring_tool, length: { maximum: TOOL_MAX_LENGTH }
validates :project, presence: true
validates :events, presence: true
validates :severity, presence: true
@@ -65,7 +58,7 @@ module AlertManagement
conditions: -> { not_resolved },
message: -> (object, data) { _('Cannot have multiple unresolved alerts') }
}, unless: :resolved?
- validate :hosts_length
+ validate :hosts_format
enum severity: {
critical: 0,
@@ -121,12 +114,13 @@ module AlertManagement
delegate :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
- scope :for_status, -> (status) { where(status: status) }
+ 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)) }
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
- scope :open, -> { with_status(OPEN_STATUSES) }
- scope :not_resolved, -> { where.not(status: STATUSES[:resolved]) }
+ scope :open, -> { with_status(open_statuses) }
+ scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
@@ -142,13 +136,33 @@ module AlertManagement
# 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 :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
- scope :counts_by_status, -> { group(:status).count }
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)
@@ -190,8 +204,25 @@ module AlertManagement
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
+ def self.open_statuses
+ [:triggered, :acknowledged]
+ end
+
+ def self.open_status?(status)
+ open_statuses.include?(status)
+ 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::AlertParams::MONITORING_TOOLS[:prometheus]
+ monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
def register_new_event!
@@ -224,10 +255,11 @@ module AlertManagement
Gitlab::DataBuilder::Alert.build(self)
end
- def hosts_length
+ def hosts_format
return unless hosts
errors.add(:hosts, "hosts array is over #{HOSTS_MAX_LENGTH} chars") if hosts.join.length > HOSTS_MAX_LENGTH
+ errors.add(:hosts, "hosts array cannot be nested") if hosts.flatten != hosts
end
end
end
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
new file mode 100644
index 00000000000..7f954e1d384
--- /dev/null
+++ b/app/models/alert_management/http_integration.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class HttpIntegration < ApplicationRecord
+ belongs_to :project, inverse_of: :alert_management_http_integrations
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm'
+
+ validates :project, presence: true
+ validates :active, inclusion: { in: [true, false] }
+
+ validates :token, presence: true
+ validates :name, presence: true, length: { maximum: 255 }
+ validates :endpoint_identifier, presence: true, length: { maximum: 255 }
+ validates :endpoint_identifier, uniqueness: { scope: [:project_id, :active] }, if: :active?
+
+ before_validation :prevent_token_assignment
+ before_validation :ensure_token
+
+ private
+
+ def prevent_token_assignment
+ if token.present? && token_changed?
+ self.token = nil
+ self.encrypted_token = encrypted_token_was
+ self.encrypted_token_iv = encrypted_token_iv_was
+ end
+ end
+
+ def ensure_token
+ self.token = generate_token if token.blank?
+ end
+
+ def generate_token
+ SecureRandom.hex
+ end
+ end
+end
diff --git a/app/models/analytics/instance_statistics/measurement.rb b/app/models/analytics/instance_statistics/measurement.rb
index eaaf9e999b3..76cc1111e90 100644
--- a/app/models/analytics/instance_statistics/measurement.rb
+++ b/app/models/analytics/instance_statistics/measurement.rb
@@ -3,13 +3,19 @@
module Analytics
module InstanceStatistics
class Measurement < ApplicationRecord
+ EXPERIMENTAL_IDENTIFIERS = %i[pipelines_succeeded pipelines_failed pipelines_canceled pipelines_skipped].freeze
+
enum identifier: {
projects: 1,
users: 2,
issues: 3,
merge_requests: 4,
groups: 5,
- pipelines: 6
+ pipelines: 6,
+ pipelines_succeeded: 7,
+ pipelines_failed: 8,
+ pipelines_canceled: 9,
+ pipelines_skipped: 10
}
IDENTIFIER_QUERY_MAPPING = {
@@ -18,7 +24,11 @@ module Analytics
identifiers[:issues] => -> { Issue },
identifiers[:merge_requests] => -> { MergeRequest },
identifiers[:groups] => -> { Group },
- identifiers[:pipelines] => -> { Ci::Pipeline }
+ identifiers[:pipelines] => -> { Ci::Pipeline },
+ identifiers[:pipelines_succeeded] => -> { Ci::Pipeline.success },
+ identifiers[:pipelines_failed] => -> { Ci::Pipeline.failed },
+ identifiers[:pipelines_canceled] => -> { Ci::Pipeline.canceled },
+ identifiers[:pipelines_skipped] => -> { Ci::Pipeline.skipped }
}.freeze
validates :recorded_at, :identifier, :count, presence: true
@@ -26,6 +36,14 @@ module Analytics
scope :order_by_latest, -> { order(recorded_at: :desc) }
scope :with_identifier, -> (identifier) { where(identifier: identifier) }
+
+ def self.measurement_identifier_values
+ if Feature.enabled?(:store_ci_pipeline_counts_by_status, default_enabled: true)
+ identifiers.values
+ else
+ identifiers.values - EXPERIMENTAL_IDENTIFIERS.map { |identifier| identifiers[identifier] }
+ end
+ end
end
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 6ffb9b7642a..3542bb90dc0 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -52,6 +52,16 @@ class ApplicationRecord < ActiveRecord::Base
end
end
+ # Start a new transaction with a shorter-than-usual statement timeout. This is
+ # currently one third of the default 15-second timeout
+ def self.with_fast_statement_timeout
+ transaction(requires_new: true) do
+ connection.exec_query("SET LOCAL statement_timeout = 5000")
+
+ yield
+ end
+ end
+
def self.safe_find_or_create_by(*args, &block)
safe_ensure_unique(retries: 1) do
find_or_create_by(*args, &block)
@@ -61,4 +71,8 @@ class ApplicationRecord < ActiveRecord::Base
def self.underscore
Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
end
+
+ def self.where_exists(query)
+ where('EXISTS (?)', query.select(1))
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e9a3dcf39df..d034630a085 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -11,6 +11,7 @@ class ApplicationSetting < ApplicationRecord
ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22'
ignore_column :snowplow_iglu_registry_url, remove_with: '13.6', remove_after: '2020-11-22'
+ INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
@@ -91,11 +92,16 @@ class ApplicationSetting < ApplicationRecord
addressable_url: true,
if: :help_page_support_url_column_exists?
+ validates :help_page_documentation_base_url,
+ length: { maximum: 255, message: _("is too long (maximum is %{count} characters)") },
+ allow_blank: true,
+ addressable_url: true
+
validates :after_sign_out_path,
allow_blank: true,
addressable_url: true
- validates :admin_notification_email,
+ validates :abuse_notification_email,
devise_email: true,
allow_blank: true
@@ -432,6 +438,14 @@ class ApplicationSetting < ApplicationRecord
!!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
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)
+ end
+
+ users_count >= INSTANCE_REVIEW_MIN_USERS
+ end
+
def self.create_from_defaults
check_schema!
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
index 723540c9b91..bab036f5697 100644
--- a/app/models/application_setting/term.rb
+++ b/app/models/application_setting/term.rb
@@ -14,6 +14,8 @@ class ApplicationSetting
end
def accepted_by_user?(user)
+ return true if user.project_bot?
+
user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists?
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 7a869d16a31..8a7bd5a7ad9 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -82,6 +82,7 @@ module ApplicationSettingImplementation
group_import_limit: 6,
help_page_hide_commercial_content: false,
help_page_text: nil,
+ help_page_documentation_base_url: nil,
hide_third_party_offers: false,
housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true,
@@ -119,6 +120,7 @@ module ApplicationSettingImplementation
repository_checks_enabled: true,
repository_storages_weighted: { default: 100 },
repository_storages: ['default'],
+ require_admin_approval_after_user_signup: false,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: 0,
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index f46803be057..34f03e769a0 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -4,8 +4,15 @@ class AuditEvent < ApplicationRecord
include CreatedAtFilterable
include IgnorableColumns
include BulkInsertSafe
+ include EachBatch
- PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details, :target_type].freeze
+ PARALLEL_PERSISTENCE_COLUMNS = [
+ :author_name,
+ :entity_path,
+ :target_details,
+ :target_type,
+ :target_id
+ ].freeze
ignore_column :type, remove_with: '13.6', remove_after: '2020-11-22'
@@ -16,6 +23,7 @@ class AuditEvent < ApplicationRecord
validates :author_id, presence: true
validates :entity_id, presence: true
validates :entity_type, presence: true
+ validates :ip_address, ip_address: true
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
@@ -47,7 +55,9 @@ class AuditEvent < ApplicationRecord
end
def initialize_details
- self.details = {} if details.nil?
+ return unless self.has_attribute?(:details)
+
+ self.details = {} if details&.nil?
end
def author_name
@@ -59,8 +69,8 @@ class AuditEvent < ApplicationRecord
end
def lazy_author
- BatchLoader.for(author_id).batch(default_value: default_author_value) do |author_ids, loader|
- User.where(id: author_ids).find_each do |user|
+ BatchLoader.for(author_id).batch(default_value: default_author_value, replace_methods: false) do |author_ids, loader|
+ User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
loader.call(user.id, user)
end
end
diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb
index 1ac3c5fbd9c..ac6e08caf50 100644
--- a/app/models/authentication_event.rb
+++ b/app/models/authentication_event.rb
@@ -1,12 +1,22 @@
# frozen_string_literal: true
class AuthenticationEvent < ApplicationRecord
+ include UsageStatistics
+
belongs_to :user, optional: true
validates :provider, :user_name, :result, presence: true
+ validates :ip_address, ip_address: true
enum result: {
failed: 0,
success: 1
}
+
+ scope :for_provider, ->(provider) { where(provider: provider) }
+ scope :ldap, -> { where('provider LIKE ?', 'ldap%')}
+
+ def self.providers
+ distinct.pluck(:provider)
+ end
end
diff --git a/app/models/blob_viewer/balsamiq.rb b/app/models/blob_viewer/balsamiq.rb
index 1af6c5474d7..6ab73730222 100644
--- a/app/models/blob_viewer/balsamiq.rb
+++ b/app/models/blob_viewer/balsamiq.rb
@@ -8,7 +8,7 @@ module BlobViewer
self.partial_name = 'balsamiq'
self.extensions = %w(bmpr)
self.binary = true
- self.switcher_icon = 'file-image-o'
+ self.switcher_icon = 'doc-image'
self.switcher_title = 'preview'
end
end
diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb
index f525180048e..37a8e01d0f1 100644
--- a/app/models/blob_viewer/markup.rb
+++ b/app/models/blob_viewer/markup.rb
@@ -9,5 +9,15 @@ module BlobViewer
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
self.file_types = %i(readme)
self.binary = false
+
+ def banzai_render_context
+ {}.tap do |h|
+ h[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup)
+
+ if Feature.enabled?(:cached_markdown_blob, blob.project, default_enabled: true)
+ h[:cache_key] = ['blob', blob.id, 'commit', blob.commit_id]
+ end
+ end
+ end
end
end
diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb
index 2cf7752585c..e3542b91d5c 100644
--- a/app/models/blob_viewer/pdf.rb
+++ b/app/models/blob_viewer/pdf.rb
@@ -8,7 +8,7 @@ module BlobViewer
self.partial_name = 'pdf'
self.extensions = %w(pdf)
self.binary = true
- self.switcher_icon = 'file-pdf-o'
+ self.switcher_icon = 'document'
self.switcher_title = 'PDF'
end
end
diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb
index 659ab11f30b..90bc9be29f4 100644
--- a/app/models/blob_viewer/sketch.rb
+++ b/app/models/blob_viewer/sketch.rb
@@ -8,7 +8,7 @@ module BlobViewer
self.partial_name = 'sketch'
self.extensions = %w(sketch)
self.binary = true
- self.switcher_icon = 'file-image-o'
+ self.switcher_icon = 'doc-image'
self.switcher_title = 'preview'
end
end
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
new file mode 100644
index 00000000000..cabff86a9f9
--- /dev/null
+++ b/app/models/bulk_import.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class BulkImport < ApplicationRecord
+ belongs_to :user, optional: false
+
+ has_one :configuration, class_name: 'BulkImports::Configuration'
+ has_many :entities, class_name: 'BulkImports::Entity'
+
+ validates :source_type, :status, presence: true
+
+ enum source_type: { gitlab: 0 }
+
+ state_machine :status, initial: :created do
+ state :created, value: 0
+ end
+end
diff --git a/app/models/bulk_imports/configuration.rb b/app/models/bulk_imports/configuration.rb
new file mode 100644
index 00000000000..8c3aff6f749
--- /dev/null
+++ b/app/models/bulk_imports/configuration.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class BulkImports::Configuration < ApplicationRecord
+ self.table_name = 'bulk_import_configurations'
+
+ belongs_to :bulk_import, inverse_of: :configuration, optional: false
+
+ validates :url, :access_token, length: { maximum: 255 }, presence: true
+ validates :url, public_url: { schemes: %w[http https], enforce_sanitization: true, ascii_only: true },
+ allow_nil: true
+
+ attr_encrypted :url,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ attr_encrypted :access_token,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
new file mode 100644
index 00000000000..2d0bba7bccc
--- /dev/null
+++ b/app/models/bulk_imports/entity.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class BulkImports::Entity < ApplicationRecord
+ self.table_name = 'bulk_import_entities'
+
+ belongs_to :bulk_import, optional: false
+ belongs_to :parent, class_name: 'BulkImports::Entity', optional: true
+
+ belongs_to :project, optional: true
+ belongs_to :group, foreign_key: :namespace_id, optional: true
+
+ validates :project, absence: true, if: :group
+ validates :group, absence: true, if: :project
+ validates :source_type, :source_full_path, :destination_name,
+ :destination_namespace, presence: true
+
+ validate :validate_parent_is_a_group, if: :parent
+ validate :validate_imported_entity_type
+
+ enum source_type: { group_entity: 0, project_entity: 1 }
+
+ state_machine :status, initial: :created do
+ state :created, value: 0
+ end
+
+ private
+
+ def validate_parent_is_a_group
+ unless parent.group_entity?
+ errors.add(:parent, s_('BulkImport|must be a group'))
+ end
+ end
+
+ def validate_imported_entity_type
+ if group.present? && project_entity?
+ errors.add(:group, s_('BulkImport|expected an associated Project but has an associated Group'))
+ end
+
+ if project.present? && group_entity?
+ errors.add(:project, s_('BulkImport|expected an associated Group but has an associated Project'))
+ end
+ end
+end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 1697067f633..2e725e0baff 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -27,7 +27,7 @@ module Ci
# rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do
- after_transition created: :pending do |bridge|
+ after_transition [:created, :manual] => :pending do |bridge|
next unless bridge.downstream_project
bridge.run_after_commit do
@@ -46,6 +46,10 @@ module Ci
event :scheduled do
transition all => :scheduled
end
+
+ event :actionize do
+ transition created: :manual
+ end
end
def self.retry(bridge, current_user)
@@ -126,9 +130,27 @@ module Ci
false
end
+ def playable?
+ return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
+
+ action? && !archived? && manual?
+ end
+
def action?
- false
+ return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
+
+ %w[manual].include?(self.when)
+ end
+
+ # rubocop: disable CodeReuse/ServiceClass
+ # We don't need it but we are taking `job_variables_attributes` parameter
+ # to make it consistent with `Ci::Build#play` method.
+ def play(current_user, job_variables_attributes = nil)
+ Ci::PlayBridgeService
+ .new(project, current_user)
+ .execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
def artifacts?
false
@@ -185,6 +207,10 @@ module Ci
[]
end
+ def target_revision_ref
+ downstream_pipeline_params.dig(:target_revision, :ref)
+ end
+
private
def cross_project_params
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 99580a52e96..9ff70ece947 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -327,6 +327,8 @@ module Ci
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
+ build.run_status_commit_hooks!
+
BuildFinishedWorker.perform_async(id)
end
end
@@ -524,7 +526,6 @@ module Ci
.concat(job_jwt_variables)
.concat(scoped_variables)
.concat(job_variables)
- .concat(environment_changed_page_variables)
.concat(persisted_environment_variables)
.to_runner_variables
end
@@ -561,15 +562,6 @@ module Ci
end
end
- def environment_changed_page_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless environment_status && Feature.enabled?(:modifed_path_ci_variables, project)
-
- variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: environment_status.changed_paths.join(','))
- variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: environment_status.changed_urls.join(','))
- end
- end
-
def deploy_token_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless gitlab_deploy_token
@@ -780,6 +772,11 @@ module Ci
end
end
+ def has_expired_locked_archive_artifacts?
+ locked_artifacts? &&
+ artifacts_expire_at.present? && artifacts_expire_at < Time.current
+ end
+
def has_expiring_archive_artifacts?
has_expiring_artifacts? && job_artifacts_archive.present?
end
@@ -901,7 +898,11 @@ module Ci
def collect_test_reports!(test_reports)
test_reports.get_suite(group_name).tap do |test_suite|
each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
- Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite, job: self)
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
+ blob,
+ test_suite,
+ job: self
+ )
end
end
end
@@ -963,8 +964,30 @@ module Ci
pending_state.try(:delete)
end
+ def run_on_status_commit(&block)
+ status_commit_hooks.push(block)
+ end
+
+ def max_test_cases_per_report
+ # NOTE: This is temporary and will be replaced later by a value
+ # that would come from an actual application limit.
+ ::Gitlab.com? ? 500_000 : 0
+ end
+
+ protected
+
+ def run_status_commit_hooks!
+ status_commit_hooks.reverse_each do |hook|
+ instance_eval(&hook)
+ end
+ end
+
private
+ def status_commit_hooks
+ @status_commit_hooks ||= []
+ end
+
def auto_retry
strong_memoize(:auto_retry) do
Gitlab::Ci::Build::AutoRetry.new(self)
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 45f323adec2..299c67f441d 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -9,4 +9,10 @@ class Ci::BuildPendingState < ApplicationRecord
enum failure_reason: CommitStatus.failure_reasons
validates :build, presence: true
+
+ def crc32
+ trace_checksum.try do |checksum|
+ checksum.to_s.split('crc32:').last.to_i(16)
+ end
+ end
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 444742062d9..6926ccd9438 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -3,9 +3,11 @@
module Ci
class BuildTraceChunk < ApplicationRecord
extend ::Gitlab::Ci::Model
+ include ::Comparable
include ::FastDestroyAll
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
+ include ::Gitlab::OptimisticLocking
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
@@ -29,6 +31,7 @@ module Ci
}
scope :live, -> { redis }
+ scope :persisted, -> { not_redis.order(:chunk_index) }
class << self
def all_stores
@@ -63,12 +66,24 @@ module Ci
get_store_class(store).delete_keys(value)
end
end
+
+ ##
+ # Sometimes we do not want to read raw data. This method makes it easier
+ # to find attributes that are just metadata excluding raw data.
+ #
+ def metadata_attributes
+ attribute_names - %w[raw_data]
+ end
end
def data
@data ||= get_data.to_s
end
+ def crc32
+ checksum.to_i
+ end
+
def truncate(offset = 0)
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
return if offset == size # Skip the following process as it doesn't affect anything
@@ -102,22 +117,47 @@ module Ci
(start_offset...end_offset)
end
- def persist_data!
- in_lock(*lock_params) { unsafe_persist_data! }
- end
-
def schedule_to_persist!
- return if persisted?
+ return if flushed?
Ci::BuildTraceChunkFlushWorker.perform_async(id)
end
- def persisted?
- !redis?
- end
+ ##
+ # It is possible that we run into two concurrent migrations. It might
+ # happen that a chunk gets migrated after being loaded by another worker
+ # but before the worker acquires a lock to perform the migration.
+ #
+ # We are using Redis locking to ensure that we perform this operation
+ # inside an exclusive lock, but this does not prevent us from running into
+ # race conditions related to updating a model representation in the
+ # database. Optimistic locking is another mechanism that help here.
+ #
+ # We are using optimistic locking combined with Redis locking to ensure
+ # that a chunk gets migrated properly.
+ #
+ # We are catching an exception related to an exclusive lock not being
+ # acquired because it is creating a lot of noise, and is a result of
+ # duplicated workers running in parallel for the same build trace chunk.
+ #
+ def persist_data!
+ in_lock(*lock_params) do # exclusive Redis lock is acquired first
+ raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
- def live?
- redis?
+ self.reset.then do |chunk| # we ensure having latest lock_version
+ chunk.unsafe_persist_data! # we migrate the data and update data store
+ end
+ end
+ rescue FailedToObtainLockError
+ metrics.increment_trace_operation(operation: :stalled)
+ rescue ActiveRecord::StaleObjectError
+ raise FailedToPersistDataError, <<~MSG
+ Data migration race condition detected
+
+ store: #{data_store}
+ build: #{build.id}
+ index: #{chunk_index}
+ MSG
end
##
@@ -126,11 +166,28 @@ module Ci
# no chunk with higher index in the database.
#
def final?
- build.pending_state.present? &&
- build.trace_chunks.maximum(:chunk_index).to_i == chunk_index
+ build.pending_state.present? && chunks_max_index == chunk_index
end
- private
+ def flushed?
+ !redis?
+ end
+
+ def migrated?
+ flushed?
+ end
+
+ def live?
+ redis?
+ end
+
+ def <=>(other)
+ return unless self.build_id == other.build_id
+
+ self.chunk_index <=> other.chunk_index
+ end
+
+ protected
def get_data
# Redis / database return UTF-8 encoded string by default
@@ -145,12 +202,19 @@ module Ci
current_size = current_data&.bytesize.to_i
unless current_size == CHUNK_SIZE || final?
- raise FailedToPersistDataError, 'Data is not fulfilled in a bucket'
+ raise FailedToPersistDataError, <<~MSG
+ data is not fulfilled in a bucket
+
+ size: #{current_size}
+ state: #{pending_state?}
+ max: #{chunks_max_index}
+ index: #{chunk_index}
+ MSG
end
self.raw_data = nil
self.data_store = new_store
- self.checksum = crc32(current_data)
+ self.checksum = self.class.crc32(current_data)
##
# We need to so persist data then save a new store identifier before we
@@ -199,10 +263,20 @@ module Ci
size == CHUNK_SIZE
end
+ private
+
+ def pending_state?
+ build.pending_state.present?
+ end
+
def current_store
self.class.get_store_class(data_store)
end
+ def chunks_max_index
+ build.trace_chunks.maximum(:chunk_index).to_i
+ end
+
def lock_params
["trace_write:#{build_id}:chunks:#{chunk_index}",
{ ttl: WRITE_LOCK_TTL,
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
index ea8072099c6..7448afba4c2 100644
--- a/app/models/ci/build_trace_chunks/database.rb
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -17,6 +17,8 @@ module Ci
def data(model)
model.raw_data
+ rescue ActiveModel::MissingAttributeError
+ model.reset.raw_data
end
def set_data(model, new_data)
diff --git a/app/models/ci/deleted_object.rb b/app/models/ci/deleted_object.rb
new file mode 100644
index 00000000000..e74946eda16
--- /dev/null
+++ b/app/models/ci/deleted_object.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Ci
+ class DeletedObject < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ mount_uploader :file, DeletedObjectUploader
+
+ scope :ready_for_destruction, ->(limit) do
+ where('pick_up_at < ?', Time.current).limit(limit)
+ end
+
+ scope :lock_for_destruction, ->(limit) do
+ ready_for_destruction(limit)
+ .select(:id)
+ .order(:pick_up_at)
+ .lock('FOR UPDATE SKIP LOCKED')
+ end
+
+ def self.bulk_import(artifacts)
+ attributes = artifacts.each.with_object([]) do |artifact, accumulator|
+ record = artifact.to_deleted_object_attrs
+ accumulator << record if record[:store_dir] && record[:file]
+ end
+
+ self.insert_all(attributes) if attributes.any?
+ end
+
+ def delete_file_from_storage
+ file.remove!
+ true
+ rescue => exception
+ Gitlab::ErrorTracking.track_exception(exception)
+ false
+ end
+ end
+end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 8bbb92e319f..02e17afdab0 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -46,7 +46,8 @@ module Ci
terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json',
requirements: 'requirements.json',
- coverage_fuzzing: 'gl-coverage-fuzzing.json'
+ coverage_fuzzing: 'gl-coverage-fuzzing.json',
+ api_fuzzing: 'gl-api-fuzzing-report.json'
}.freeze
INTERNAL_TYPES = {
@@ -65,11 +66,8 @@ module Ci
cluster_applications: :gzip,
lsif: :zip,
- # All these file formats use `raw` as we need to store them uncompressed
- # for Frontend to fetch the files and do analysis
- # When they will be only used by backend, they can be `gzipped`.
- accessibility: :raw,
- codequality: :raw,
+ # Security reports and license scanning reports are raw artifacts
+ # because they used to be fetched by the frontend, but this is not the case anymore.
sast: :raw,
secret_detection: :raw,
dependency_scanning: :raw,
@@ -77,16 +75,24 @@ module Ci
dast: :raw,
license_management: :raw,
license_scanning: :raw,
+
+ # All these file formats use `raw` as we need to store them uncompressed
+ # for Frontend to fetch the files and do analysis
+ # When they will be only used by backend, they can be `gzipped`.
+ accessibility: :raw,
+ codequality: :raw,
performance: :raw,
browser_performance: :raw,
load_performance: :raw,
terraform: :raw,
requirements: :raw,
- coverage_fuzzing: :raw
+ coverage_fuzzing: :raw,
+ api_fuzzing: :raw
}.freeze
DOWNLOADABLE_TYPES = %w[
accessibility
+ api_fuzzing
archive
cobertura
codequality
@@ -194,7 +200,8 @@ module Ci
requirements: 22, ## EE-specific
coverage_fuzzing: 23, ## EE-specific
browser_performance: 24, ## EE-specific
- load_performance: 25 ## EE-specific
+ load_performance: 25, ## EE-specific
+ api_fuzzing: 26 ## EE-specific
}
# `file_location` indicates where actual files are stored.
@@ -283,6 +290,15 @@ module Ci
max_size&.megabytes.to_i
end
+ def to_deleted_object_attrs
+ {
+ file_store: file_store,
+ store_dir: file.store_dir.to_s,
+ file: file_identifier,
+ pick_up_at: expire_at || Time.current
+ }
+ end
+
private
def set_size
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 47eba685afe..684b6387ab1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -27,6 +27,13 @@ module Ci
sha_attribute :source_sha
sha_attribute :target_sha
+ # Ci::CreatePipelineService returns Ci::Pipeline so this is the only place
+ # where we can pass additional information from the service. This accessor
+ # is used for storing the processed CI YAML contents for linting purposes.
+ # There is an open issue to address this:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/259010
+ attr_accessor :merged_yaml
+
belongs_to :project, inverse_of: :all_pipelines
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
@@ -42,6 +49,7 @@ module Ci
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :latest_statuses, -> { latest }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
@@ -577,11 +585,11 @@ module Ci
end
def retried
- @retried ||= (statuses.order(id: :desc) - statuses.latest)
+ @retried ||= (statuses.order(id: :desc) - latest_statuses)
end
def coverage
- coverage_array = statuses.latest.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
@@ -821,16 +829,28 @@ module Ci
end
def same_family_pipeline_ids
- if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project)
- ::Gitlab::Ci::PipelineObjectHierarchy.new(
- base_and_ancestors(same_project: true), options: { same_project: true }
- ).base_and_descendants.select(:id)
- else
- # If pipeline is a child of another pipeline, include the parent
- # and the siblings, otherwise return only itself and children.
- parent = parent_pipeline || self
- [parent.id] + parent.child_pipelines.pluck(:id)
- end
+ ::Gitlab::Ci::PipelineObjectHierarchy.new(
+ base_and_ancestors(same_project: true), options: { same_project: true }
+ ).base_and_descendants.select(:id)
+ end
+
+ def build_with_artifacts_in_self_and_descendants(name)
+ builds_in_self_and_descendants
+ .ordered_by_pipeline # find job in hierarchical order
+ .with_downloadable_artifacts
+ .find_by_name(name)
+ end
+
+ def builds_in_self_and_descendants
+ Ci::Build.latest.where(pipeline: self_and_descendants)
+ end
+
+ # Without using `unscoped`, caller scope is also included into the query.
+ # Using `unscoped` here will be redundant after Rails 6.1
+ def self_and_descendants
+ ::Gitlab::Ci::PipelineObjectHierarchy
+ .new(self.class.unscoped.where(id: id), options: { same_project: true })
+ .base_and_descendants
end
def bridge_triggered?
@@ -875,7 +895,7 @@ module Ci
end
def builds_with_coverage
- builds.with_coverage
+ builds.latest.with_coverage
end
def has_reports?(reports_scope)
diff --git a/app/models/ci_platform_metric.rb b/app/models/ci_platform_metric.rb
index 5e6e3eddce9..ac4ab391bbf 100644
--- a/app/models/ci_platform_metric.rb
+++ b/app/models/ci_platform_metric.rb
@@ -14,7 +14,7 @@ class CiPlatformMetric < ApplicationRecord
numericality: { only_integer: true, greater_than: 0 }
CI_VARIABLE_KEY = 'AUTO_DEVOPS_PLATFORM_TARGET'
- ALLOWED_TARGETS = %w[ECS FARGATE].freeze
+ ALLOWED_TARGETS = %w[ECS FARGATE EC2].freeze
def self.insert_auto_devops_platform_targets!
recorded_at = Time.zone.now
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index 874415e7bf4..5feb3b0a1e6 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -8,6 +8,7 @@ module Clusters
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
+ scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
validates :name,
diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb
index 3fd6e870edc..c608d37be77 100644
--- a/app/models/clusters/applications/fluentd.rb
+++ b/app/models/clusters/applications/fluentd.rb
@@ -22,7 +22,11 @@ module Clusters
validate :has_at_least_one_log_enabled?
def chart
- 'stable/fluentd'
+ 'fluentd/fluentd'
+ end
+
+ def repository
+ 'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
end
def install_command
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 1d08f38a2f1..d5412714858 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -46,7 +46,11 @@ module Clusters
end
def chart
- 'stable/nginx-ingress'
+ "#{name}/nginx-ingress"
+ end
+
+ def repository
+ 'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
end
def values
@@ -60,6 +64,7 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
+ repository: repository,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index dd6a4144608..7679296699f 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -51,7 +51,11 @@ module Clusters
end
def chart
- 'stable/prometheus'
+ "#{name}/prometheus"
+ end
+
+ def repository
+ 'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
end
def service_name
@@ -65,6 +69,7 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
+ repository: repository,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
@@ -76,6 +81,7 @@ module Clusters
def patch_command(values)
::Gitlab::Kubernetes::Helm::PatchCommand.new(
name: name,
+ repository: repository,
version: version,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index f0b3c11ba1d..d07ea7b71dc 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.20.2'
+ VERSION = '0.21.1'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 7af78960e35..b85a902d58b 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -11,6 +11,7 @@ module Clusters
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
self.table_name = 'cluster_platforms_kubernetes'
+ self.reactive_cache_work_type = :external_dependency
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
@@ -101,7 +102,7 @@ module Clusters
def terminals(environment, data)
pods = filter_by_project_environment(data[:pods], environment.project.full_path_slug, environment.slug)
terminals = pods.flat_map { |pod| terminals_for_pod(api_url, environment.deployment_namespace, pod) }.compact
- terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
+ terminals.each { |terminal| add_terminal_auth(terminal, **terminal_auth) }
end
def kubeclient
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5e0fceb23a4..83400c9e533 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -29,12 +29,6 @@ class Commit
delegate :repository, to: :container
delegate :project, to: :repository, allow_nil: true
- DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
-
- # Commits above this size will not be rendered in HTML
- DIFF_HARD_LIMIT_FILES = 1000
- DIFF_HARD_LIMIT_LINES = 50000
-
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze
@@ -80,10 +74,30 @@ class Commit
sha[0..MIN_SHA_LENGTH]
end
- def max_diff_options
+ def diff_safe_lines(project: nil)
+ Gitlab::Git::DiffCollection.default_limits(project: project)[:max_lines]
+ end
+
+ def diff_hard_limit_files(project: nil)
+ if Feature.enabled?(:increased_diff_limits, project)
+ 2000
+ else
+ 1000
+ end
+ end
+
+ def diff_hard_limit_lines(project: nil)
+ if Feature.enabled?(:increased_diff_limits, project)
+ 75000
+ else
+ 50000
+ end
+ end
+
+ def max_diff_options(project: nil)
{
- max_files: DIFF_HARD_LIMIT_FILES,
- max_lines: DIFF_HARD_LIMIT_LINES
+ max_files: diff_hard_limit_files(project: project),
+ max_lines: diff_hard_limit_lines(project: project)
}
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2f0596c93cc..4498e08d754 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -48,6 +48,7 @@ class CommitStatus < ApplicationRecord
scope :ordered_by_stage, -> { order(stage_idx: :asc) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
+ scope :ordered_by_pipeline, -> { order(pipeline_id: :asc) }
scope :before_stage, -> (index) { where('stage_idx < ?', index) }
scope :for_stage, -> (index) { where(stage_idx: index) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
@@ -204,8 +205,13 @@ class CommitStatus < ApplicationRecord
# 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
- # 'rspec:linux: [aws, max memory]' => 'rspec:linux'
- common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
+ if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled?
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
+ common_name.gsub!(%r{: \[.*\]\s*\z}, '')
+ else
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]'
+ common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
+ end
common_name.strip!
common_name
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index d07c4ec43ac..c2d94b50f8d 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -2,10 +2,34 @@
module ApprovableBase
extend ActiveSupport::Concern
+ include FromUnion
included do
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approved_by_users, through: :approvals, source: :user
+
+ scope :without_approvals, -> { left_outer_joins(:approvals).where(approvals: { id: nil }) }
+ scope :with_approvals, -> { joins(:approvals) }
+ scope :approved_by_users_with_ids, -> (*user_ids) do
+ with_approvals
+ .merge(Approval.with_user)
+ .where(users: { id: user_ids })
+ .group(:id)
+ .having("COUNT(users.id) = ?", user_ids.size)
+ end
+ scope :approved_by_users_with_usernames, -> (*usernames) do
+ with_approvals
+ .merge(Approval.with_user)
+ .where(users: { username: usernames })
+ .group(:id)
+ .having("COUNT(users.id) = ?", usernames.size)
+ end
+ end
+
+ class_methods do
+ def select_from_union(relations)
+ where(id: from_union(relations))
+ end
end
def approved_by?(user)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 0dd55ab67b5..d342b526677 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -3,16 +3,11 @@
module Avatarable
extend ActiveSupport::Concern
- ALLOWED_IMAGE_SCALER_WIDTHS = [
- 400,
- 200,
- 64,
- 48,
- 40,
- 26,
- 20,
- 16
- ].freeze
+ USER_AVATAR_SIZES = [16, 20, 23, 24, 26, 32, 36, 38, 40, 48, 60, 64, 90, 96, 120, 160].freeze
+ PROJECT_AVATAR_SIZES = [15, 40, 48, 64, 88].freeze
+ GROUP_AVATAR_SIZES = [15, 37, 38, 39, 40, 64, 96].freeze
+
+ ALLOWED_IMAGE_SCALER_WIDTHS = (USER_AVATAR_SIZES | PROJECT_AVATAR_SIZES | GROUP_AVATAR_SIZES).freeze
included do
prepend ShadowMethods
diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb
index d6d17bfc604..056abafd0ce 100644
--- a/app/models/concerns/checksummable.rb
+++ b/app/models/concerns/checksummable.rb
@@ -3,11 +3,11 @@
module Checksummable
extend ActiveSupport::Concern
- def crc32(data)
- Zlib.crc32(data)
- end
-
class_methods do
+ def crc32(data)
+ Zlib.crc32(data)
+ end
+
def hexdigest(path)
::Digest::SHA256.file(path).hexdigest
end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index a5c7393e8f7..b468415c4c7 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -20,6 +20,14 @@
# To increment the counter we can use the method:
# delayed_increment_counter(:commit_count, 3)
#
+# It is possible to register callbacks to be executed after increments have
+# been flushed to the database. Callbacks are not executed if there are no increments
+# to flush.
+#
+# counter_attribute_after_flush do |statistic|
+# Namespaces::ScheduleAggregationWorker.perform_async(statistic.namespace_id)
+# end
+#
module CounterAttribute
extend ActiveSupport::Concern
extend AfterCommitQueue
@@ -48,6 +56,15 @@ module CounterAttribute
def counter_attributes
@counter_attributes ||= Set.new
end
+
+ def after_flush_callbacks
+ @after_flush_callbacks ||= []
+ end
+
+ # perform registered callbacks after increments have been flushed to the database
+ def counter_attribute_after_flush(&callback)
+ after_flush_callbacks << callback
+ end
end
# This method must only be called by FlushCounterIncrementsWorker
@@ -75,6 +92,8 @@ module CounterAttribute
unsafe_update_counters(id, attribute => increment_value)
redis_state { |redis| redis.del(flushed_key) }
end
+
+ execute_after_flush_callbacks
end
end
@@ -108,13 +127,13 @@ module CounterAttribute
counter_key(attribute) + ':lock'
end
- private
-
def counter_attribute_enabled?(attribute)
Feature.enabled?(:efficient_counter_attribute, project) &&
self.class.counter_attributes.include?(attribute)
end
+ private
+
def steal_increments(increment_key, flushed_key)
redis_state do |redis|
redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
@@ -129,6 +148,12 @@ module CounterAttribute
self.class.update_counters(id, increments)
end
+ def execute_after_flush_callbacks
+ self.class.after_flush_callbacks.each do |callback|
+ callback.call(self)
+ end
+ end
+
def redis_state(&block)
Gitlab::Redis::SharedState.with(&block)
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index d909b67d7ba..978a54bdee7 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -71,6 +71,10 @@ module HasRepository
raise NotImplementedError
end
+ def lfs_enabled?
+ false
+ end
+
def empty_repo?
repository.empty?
end
@@ -80,7 +84,11 @@ module HasRepository
end
def default_branch_from_preferences
- empty_repo? ? Gitlab::CurrentSettings.default_branch_name : nil
+ return unless empty_repo?
+
+ group_branch_default_name = group&.default_branch_name if respond_to?(:group)
+
+ group_branch_default_name || Gitlab::CurrentSettings.default_branch_name
end
def reload_default_branch
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 8a238dc736c..468387115e5 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -11,16 +11,18 @@ module HasUserType
service_user: 4,
ghost: 5,
project_bot: 6,
- migration_bot: 7
+ migration_bot: 7,
+ security_bot: 8
}.with_indifferent_access.freeze
- BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot].freeze
+ BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot].freeze
NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
included do
scope :humans, -> { where(user_type: :human) }
scope :bots, -> { where(user_type: BOT_USER_TYPES) }
+ scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) }
scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) }
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb
index 34ff5bb1195..9d446841a9f 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/integration.rb
@@ -16,7 +16,7 @@ module Integration
Project.where(id: custom_integration_project_ids)
end
- def ids_without_integration(integration, limit)
+ def without_integration(integration)
services = Service
.select('1')
.where('services.project_id = projects.id')
@@ -26,8 +26,6 @@ module Integration
.where('NOT EXISTS (?)', services)
.where(pending_delete: false)
.where(archived: false)
- .limit(limit)
- .pluck(:id)
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 888e1b384a2..7624a1a4e80 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -182,7 +182,7 @@ module Issuable
end
def supports_time_tracking?
- is_a?(TimeTrackable) && !incident?
+ is_a?(TimeTrackable)
end
def supports_severity?
@@ -203,15 +203,6 @@ module Issuable
issuable_severity&.severity || IssuableSeverity::DEFAULT
end
- def update_severity(severity)
- return unless incident?
-
- severity = severity.to_s.downcase
- severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity)
-
- (issuable_severity || build_issuable_severity(issue_id: id)).update(severity: severity)
- end
-
private
def description_max_length_for_new_records_is_valid
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
new file mode 100644
index 00000000000..6efb8103b7b
--- /dev/null
+++ b/app/models/concerns/issue_available_features.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Verifies features availability based on issue type.
+# This can be used, for example, for hiding UI elements or blocking specific
+# quick actions for particular issue types;
+module IssueAvailableFeatures
+ extend ActiveSupport::Concern
+
+ # EE only features are listed on EE::IssueAvailableFeatures
+ def available_features_for_issue_types
+ {}.with_indifferent_access
+ end
+
+ def issue_type_supports?(feature)
+ unless available_features_for_issue_types.has_key?(feature)
+ raise ArgumentError, 'invalid feature'
+ end
+
+ available_features_for_issue_types[feature].include?(issue_type)
+ end
+end
+
+IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 7b4485376d4..b10e8547e86 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -81,13 +81,6 @@ module Mentionable
end
def store_mentions!
- # if store_mentioned_users_to_db feature flag is not enabled then consider storing operation as succeeded
- # because we wrap this method in transaction with with_transaction_returning_status, and we need the status to be
- # successful if mentionable.save is successful.
- #
- # This line will get removed when we remove the feature flag.
- return true unless store_mentioned_users_to_db_enabled?
-
refs = all_references(self.author)
references = {}
@@ -253,15 +246,6 @@ module Mentionable
def model_user_mention
user_mentions.where(note_id: nil).first_or_initialize
end
-
- # We need this method to be checking that store_mentioned_users_to_db feature flag is enabled at the group level
- # and not the project level as epics are defined at group level and we want to have epics store user mentions as well
- # for the test period.
- # During the test period the flag should be enabled at the group level.
- def store_mentioned_users_to_db_enabled?
- return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group, default_enabled: true) if self.respond_to?(:project)
- return Feature.enabled?(:store_mentioned_users_to_db, self.group, default_enabled: true) if self.respond_to?(:group)
- end
end
Mentionable.prepend_if_ee('EE::Mentionable')
diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb
index 06c300c2e41..1f05abff2f4 100644
--- a/app/models/concerns/presentable.rb
+++ b/app/models/concerns/presentable.rb
@@ -5,13 +5,13 @@ module Presentable
class_methods do
def present(attributes)
- all.map { |klass_object| klass_object.present(attributes) }
+ all.map { |klass_object| klass_object.present(**attributes) }
end
end
def present(**attributes)
Gitlab::View::Presenter::Factory
- .new(self, attributes)
+ .new(self, **attributes)
.fabricate!
end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 5f30fc0c36c..3470bdab5fb 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# The usage of the ReactiveCaching module is documented here:
-# https://docs.gitlab.com/ee/development/utilities.html#reactivecaching
+# https://docs.gitlab.com/ee/development/reactive_caching.md
module ReactiveCaching
extend ActiveSupport::Concern
@@ -9,7 +9,7 @@ module ReactiveCaching
ExceededReactiveCacheLimit = Class.new(StandardError)
WORK_TYPE = {
- default: ReactiveCachingWorker,
+ no_dependency: ReactiveCachingWorker,
external_dependency: ExternalServiceReactiveCachingWorker
}.freeze
@@ -30,7 +30,6 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_hard_limit = nil # this value should be set in megabytes. E.g: 1.megabyte
- self.reactive_cache_work_type = :default
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
index af69da24994..c444f238944 100644
--- a/app/models/concerns/reactive_service.rb
+++ b/app/models/concerns/reactive_service.rb
@@ -8,5 +8,6 @@ module ReactiveService
# Default cache key: class name + project_id
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
+ self.reactive_cache_work_type = :external_dependency
end
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 40edd3b3ead..9a17131c91c 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -85,7 +85,7 @@ module Referable
\/#{route.is_a?(Regexp) ? route : Regexp.escape(route)}
\/#{pattern}
(?<path>
- (\/[a-z0-9_=-]+)*
+ (\/[a-z0-9_=-]+)*\/*
)?
(?<query>
\?[a-z0-9_=-]+
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 3cbc174536c..7f559f0a7ed 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -102,33 +102,16 @@ module RelativePositioning
delta = at_end ? gap : -gap
indexed = (at_end ? objects : objects.reverse).each_with_index
- # Some classes are polymorphic, and not all siblings are in the same table.
- by_model = indexed.group_by { |pair| pair.first.class }
lower_bound, upper_bound = at_end ? [position, MAX_POSITION] : [MIN_POSITION, position]
- by_model.each do |model, pairs|
- model.transaction do
- pairs.each_slice(100) do |batch|
- # These are known to be integers, one from the DB, and the other
- # calculated by us, and thus safe to interpolate
- values = batch.map do |obj, i|
- desired_pos = position + delta * (i + 1)
- pos = desired_pos.clamp(lower_bound, upper_bound)
- obj.relative_position = pos
- "(#{obj.id}, #{pos})"
- end.join(', ')
-
- model.connection.exec_query(<<~SQL, "UPDATE #{model.table_name} positions")
- WITH cte(cte_id, new_pos) AS (
- SELECT *
- FROM (VALUES #{values}) as t (id, pos)
- )
- UPDATE #{model.table_name}
- SET relative_position = cte.new_pos
- FROM cte
- WHERE cte_id = id
- SQL
+ representative.model_class.transaction do
+ indexed.each_slice(100) do |batch|
+ mapping = batch.to_h.transform_values! do |i|
+ desired_pos = position + delta * (i + 1)
+ { relative_position: desired_pos.clamp(lower_bound, upper_bound) }
end
+
+ ::Gitlab::Database::BulkUpdate.execute([:relative_position], mapping, &:model_class)
end
end
@@ -200,4 +183,16 @@ module RelativePositioning
# Override if you want to be notified of failures to move
def could_not_move(exception)
end
+
+ # Override if the implementing class is not a simple application record, for
+ # example if the record is loaded from a union.
+ def reset_relative_position
+ reset.relative_position
+ end
+
+ # Override if the model class needs a more complicated computation (e.g. the
+ # object is a member of a union).
+ def model_class
+ self.class
+ end
end
diff --git a/app/models/concerns/shardable.rb b/app/models/concerns/shardable.rb
index 57cd77b44b4..c0883c08289 100644
--- a/app/models/concerns/shardable.rb
+++ b/app/models/concerns/shardable.rb
@@ -5,6 +5,10 @@ module Shardable
included do
belongs_to :shard
+
+ scope :for_repository_storage, -> (repository_storage) { joins(:shard).where(shards: { name: repository_storage }) }
+ scope :excluding_repository_storage, -> (repository_storage) { joins(:shard).where.not(shards: { name: repository_storage }) }
+
validates :shard, presence: true
end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 3e2cf9031d0..23fd73f2904 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -73,6 +73,32 @@ module Timebox
end
end
+ # A timebox is within the timeframe (start_date, end_date) if it overlaps
+ # with that timeframe:
+ #
+ # [ timeframe ]
+ # ----| ................ # Not overlapping
+ # |--| ................ # Not overlapping
+ # ------|............... # Overlapping
+ # -----------------------| # Overlapping
+ # ---------|............ # Overlapping
+ # |-----|............ # Overlapping
+ # |--------------| # Overlapping
+ # |--------------------| # Overlapping
+ # ...|-----|...... # Overlapping
+ # .........|-----| # Overlapping
+ # .........|--------- # Overlapping
+ # |-------------------- # Overlapping
+ # .........|--------| # Overlapping
+ # ...............|--| # Overlapping
+ # ............... |-| # Not Overlapping
+ # ............... |-- # Not Overlapping
+ #
+ # where: . = in timeframe
+ # ---| no start
+ # |--- no end
+ # |--| defined start and end
+ #
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index a7028e18451..586f1dbb65c 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -80,10 +80,7 @@ module UpdateProjectStatistics
run_after_commit do
ProjectStatistics.increment_statistic(
- project_id, self.class.project_statistics_name, delta)
-
- Namespaces::ScheduleAggregationWorker.perform_async(
- project.namespace_id)
+ project, self.class.project_statistics_name, delta)
end
end
end
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index b1dd720d908..641d244b665 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -3,6 +3,7 @@
class ContainerExpirationPolicy < ApplicationRecord
include Schedulable
include UsageStatistics
+ include EachBatch
belongs_to :project, inverse_of: :container_expiration_policy
@@ -19,6 +20,16 @@ class ContainerExpirationPolicy < ApplicationRecord
scope :active, -> { where(enabled: true) }
scope :preloaded, -> { preload(project: [:route]) }
+ def self.executable
+ runnable_schedules.where(
+ 'EXISTS (?)',
+ ContainerRepository.select(1)
+ .where(
+ 'container_repositories.project_id = container_expiration_policies.project_id'
+ )
+ )
+ end
+
def self.keep_n_options
{
1 => _('%{tags} tag per image name') % { tags: 1 },
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index b0f7edac2f3..d97b8776085 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -107,6 +107,14 @@ class ContainerRepository < ApplicationRecord
client.delete_repository_tag_by_name(self.path, name)
end
+ def reset_expiration_policy_started_at!
+ update!(expiration_policy_started_at: nil)
+ end
+
+ def start_expiration_policy!
+ update!(expiration_policy_started_at: Time.zone.now)
+ end
+
def self.build_from_path(path)
self.new(project: path.repository_project,
name: path.repository_name)
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
index 2cee3447886..adad8e3013e 100644
--- a/app/models/data_list.rb
+++ b/app/models/data_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class DataList
- def initialize(batch_ids, data_fields_hash, klass)
- @batch_ids = batch_ids
+ def initialize(batch, data_fields_hash, klass)
+ @batch = batch
@data_fields_hash = data_fields_hash
@klass = klass
end
@@ -13,15 +13,15 @@ class DataList
private
- attr_reader :batch_ids, :data_fields_hash, :klass
+ attr_reader :batch, :data_fields_hash, :klass
def columns
data_fields_hash.keys << 'service_id'
end
def values
- batch_ids.map do |row|
- data_fields_hash.values << row['id']
+ batch.map do |record|
+ data_fields_hash.values << record['id']
end
end
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 395260b5201..9355d73fae9 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -78,6 +78,20 @@ class DeployToken < ApplicationRecord
end
end
+ def group
+ strong_memoize(:group) do
+ groups.first
+ end
+ end
+
+ def accessible_projects
+ if project_type?
+ projects
+ elsif group_type?
+ group.all_projects
+ end
+ end
+
def holder
strong_memoize(:holder) do
if project_type?
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 3978620c74d..2d0d98136ec 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -46,6 +46,8 @@ class Deployment < ApplicationRecord
scope :older_than, -> (deployment) { where('id < ?', deployment.id) }
scope :with_deployable, -> { includes(:deployable).where('deployable_id IS NOT NULL') }
+ FINISHED_STATUSES = %i[success failed canceled].freeze
+
state_machine :status, initial: :created do
event :run do
transition created: :running
@@ -63,27 +65,41 @@ class Deployment < ApplicationRecord
transition any - [:canceled] => :canceled
end
- before_transition any => [:success, :failed, :canceled] do |deployment|
+ before_transition any => FINISHED_STATUSES do |deployment|
deployment.finished_at = Time.current
end
- after_transition any => :success do |deployment|
+ after_transition any => :running do |deployment|
+ next unless deployment.project.ci_forward_deployment_enabled?
+
deployment.run_after_commit do
- Deployments::SuccessWorker.perform_async(id)
+ Deployments::DropOlderDeploymentsWorker.perform_async(id)
end
end
- after_transition any => [:success, :failed, :canceled] do |deployment|
+ after_transition any => :running do |deployment|
deployment.run_after_commit do
- Deployments::FinishedWorker.perform_async(id)
+ next unless Feature.enabled?(:ci_send_deployment_hook_when_start, deployment.project)
+
+ Deployments::ExecuteHooksWorker.perform_async(id)
end
end
- after_transition any => :running do |deployment|
- next unless deployment.project.forward_deployment_enabled?
+ after_transition any => :success do |deployment|
+ deployment.run_after_commit do
+ Deployments::UpdateEnvironmentWorker.perform_async(id)
+ end
+ end
+
+ after_transition any => FINISHED_STATUSES do |deployment|
+ deployment.run_after_commit do
+ Deployments::LinkMergeRequestWorker.perform_async(id)
+ end
+ end
+ after_transition any => FINISHED_STATUSES do |deployment|
deployment.run_after_commit do
- Deployments::ForwardDeploymentWorker.perform_async(id)
+ Deployments::ExecuteHooksWorker.perform_async(id)
end
end
end
@@ -273,7 +289,7 @@ class Deployment < ApplicationRecord
SQL
end
- # Changes the status of a deployment and triggers the correspinding state
+ # Changes the status of a deployment and triggers the corresponding state
# machine events.
def update_status(status)
case status
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
index ff4d9f66202..b67f96906f5 100644
--- a/app/models/deployment_merge_request.rb
+++ b/app/models/deployment_merge_request.rb
@@ -3,4 +3,25 @@
class DeploymentMergeRequest < ApplicationRecord
belongs_to :deployment, optional: false
belongs_to :merge_request, optional: false
+
+ def self.join_deployments_for_merge_requests
+ joins(deployment: :environment)
+ .where('deployment_merge_requests.merge_request_id = merge_requests.id')
+ end
+
+ def self.by_deployment_id(id)
+ where('deployments.id = ?', id)
+ end
+
+ def self.deployed_to(name)
+ where('environments.name = ?', name)
+ end
+
+ def self.deployed_after(time)
+ where('deployments.finished_at > ?', time)
+ end
+
+ def self.deployed_before(time)
+ where('deployments.finished_at < ?', time)
+ end
end
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index 57bb250829d..62e4bd6cebc 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -167,6 +167,10 @@ module DesignManagement
end
end
+ def self.build_full_path(issue, design)
+ File.join(DesignManagement.designs_directory, "issue-#{issue.iid}", design.filename)
+ end
+
def to_ability_name
'design'
end
@@ -180,7 +184,7 @@ module DesignManagement
end
def full_path
- @full_path ||= File.join(DesignManagement.designs_directory, "issue-#{issue.iid}", filename)
+ @full_path ||= self.class.build_full_path(issue, self)
end
def diff_refs
@@ -224,6 +228,10 @@ module DesignManagement
!interloper.exists?
end
+ def notes_with_associations
+ notes.includes(:author)
+ end
+
private
def head_version
diff --git a/app/models/design_management/design_at_version.rb b/app/models/design_management/design_at_version.rb
index b4cafb93c2c..211211144f4 100644
--- a/app/models/design_management/design_at_version.rb
+++ b/app/models/design_management/design_at_version.rb
@@ -21,10 +21,6 @@ module DesignManagement
@design, @version = design, version
end
- def self.instantiate(attrs)
- new(attrs).tap { |obj| obj.validate! }
- end
-
# The ID, needed by GraphQL types and as part of the Lazy-fetch
# protocol, includes information about both the design and the version.
#
diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb
index c48b36588c9..6deba14a6ba 100644
--- a/app/models/design_management/design_collection.rb
+++ b/app/models/design_management/design_collection.rb
@@ -5,6 +5,7 @@ module DesignManagement
attr_reader :issue
delegate :designs, :project, to: :issue
+ delegate :empty?, to: :designs
state_machine :copy_state, initial: :ready, namespace: :copy do
after_transition any => any, do: :update_stored_copy_state!
diff --git a/app/models/diff_viewer/rich.rb b/app/models/diff_viewer/rich.rb
index 5caefa2031c..0d94d8f773b 100644
--- a/app/models/diff_viewer/rich.rb
+++ b/app/models/diff_viewer/rich.rb
@@ -6,7 +6,7 @@ module DiffViewer
included do
self.type = :rich
- self.switcher_icon = 'file-text-o'
+ self.switcher_icon = 'doc-text'
self.switcher_title = _('rendered diff')
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index cfdcb0499e6..66613869915 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -4,12 +4,15 @@ class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include ReactiveCaching
include FastDestroyAll::Helpers
+ include Presentable
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
self.reactive_cache_hard_limit = 10.megabytes
self.reactive_cache_work_type = :external_dependency
+ PRODUCTION_ENVIRONMENT_IDENTIFIERS = %w[prod production].freeze
+
belongs_to :project, required: true
use_fast_destroy :all_deployments
@@ -67,6 +70,7 @@ class Environment < ApplicationRecord
scope :order_by_last_deployed_at_desc, -> do
order(Gitlab::Database.nulls_last_order("(#{max_deployment_id_sql})", 'DESC'))
end
+ scope :order_by_name, -> { order('environments.name ASC') }
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
@@ -86,6 +90,7 @@ class Environment < ApplicationRecord
scope :with_rank, -> do
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
end
+ scope :for_id, -> (id) { where(id: id) }
state_machine :state, initial: :available do
event :start do
@@ -118,6 +123,10 @@ class Environment < ApplicationRecord
pluck(:name)
end
+ def self.pluck_unique_names
+ pluck('DISTINCT(environments.name)')
+ end
+
def self.find_or_create_by_name(name)
find_or_create_by(name: name)
end
@@ -211,7 +220,7 @@ class Environment < ApplicationRecord
end
def update_merge_request_metrics?
- folder_name == "production"
+ PRODUCTION_ENVIRONMENT_IDENTIFIERS.include?(folder_name.downcase)
end
def ref_path
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 46e41c22139..55ea4e2fe18 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -72,14 +72,6 @@ class EnvironmentStatus
.merge_request_diff_files.where(deleted_file: false)
end
- def changed_paths
- changes.map { |change| change[:path] }
- end
-
- def changed_urls
- changes.map { |change| change[:external_url] }
- end
-
def has_route_map?
project.route_map_for(sha).present?
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 92609144576..671def16151 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -242,6 +242,8 @@ class Event < ApplicationRecord
target if note?
end
+ # rubocop: disable Metrics/CyclomaticComplexity
+ # rubocop: disable Metrics/PerceivedComplexity
def action_name
if push_action?
push_action_name
@@ -267,10 +269,14 @@ class Event < ApplicationRecord
'updated'
elsif created_project_action?
created_project_action_name
+ elsif approved_action?
+ 'approved'
else
"opened"
end
end
+ # rubocop: enable Metrics/CyclomaticComplexity
+ # rubocop: enable Metrics/PerceivedComplexity
def target_iid
target.respond_to?(:iid) ? target.iid : target_id
@@ -323,14 +329,6 @@ class Event < ApplicationRecord
end
end
- def note_target_type
- if target.noteable_type.present?
- target.noteable_type.titleize
- else
- "Wall"
- end.downcase
- end
-
def body?
if push_action?
push_with_commits?
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
deleted file mode 100644
index 7c020dd3b3d..00000000000
--- a/app/models/global_label.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-class GlobalLabel
- include Presentable
-
- attr_accessor :title, :labels
- alias_attribute :name, :title
-
- delegate :color, :text_color, :description, :scoped_label?, to: :@first_label
-
- def for_display
- @first_label
- end
-
- def self.build_collection(labels)
- labels = labels.group_by(&:title)
-
- labels.map do |title, labels|
- new(title, labels)
- end
- end
-
- def initialize(title, labels)
- @title = title
- @labels = labels
- @first_label = labels.find { |lbl| lbl.description.present? } || labels.first
- end
-
- def present(attributes)
- super(attributes.merge(presenter_class: ::LabelPresenter))
- end
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index c0f145997cc..74f7efd253d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -15,11 +15,10 @@ class Group < Namespace
include WithUploads
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
+ include EachBatch
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
- UpdateSharedRunnersError = Class.new(StandardError)
-
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -77,6 +76,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
+ validate :two_factor_authentication_allowed
validates :variables, variable_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
@@ -140,6 +140,15 @@ class Group < Namespace
end
end
+ def without_integration(integration)
+ services = Service
+ .select('1')
+ .where('services.group_id = namespaces.id')
+ .where(type: integration.type)
+
+ where('NOT EXISTS (?)', services)
+ end
+
private
def public_to_user_arel(user)
@@ -348,6 +357,7 @@ class Group < Namespace
end
group_hierarchy_members = GroupMember.active_without_invites_and_requests
+ .non_minimal_access
.where(source_id: source_ids)
GroupMember.from_union([group_hierarchy_members,
@@ -528,57 +538,37 @@ class Group < Namespace
preloader.preload(self, shared_with_group_links: [shared_with_group: :route])
end
- def shared_runners_allowed?
- shared_runners_enabled? || allow_descendants_override_disabled_shared_runners?
- end
-
- def parent_allows_shared_runners?
- return true unless has_parent?
+ def update_shared_runners_setting!(state)
+ raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state)
- parent.shared_runners_allowed?
+ case state
+ when 'disabled_and_unoverridable' then disable_shared_runners! # also disallows override
+ when 'disabled_with_override' then disable_shared_runners_and_allow_override!
+ when 'enabled' then enable_shared_runners! # set both to true
+ end
end
- def parent_enabled_shared_runners?
- return true unless has_parent?
-
- parent.shared_runners_enabled?
+ def default_owner
+ owners.first || parent&.default_owner || owner
end
- def enable_shared_runners!
- raise UpdateSharedRunnersError, 'Shared Runners disabled for the parent group' unless parent_enabled_shared_runners?
-
- update_column(:shared_runners_enabled, true)
+ def default_branch_name
+ namespace_settings&.default_branch_name
end
- def disable_shared_runners!
- group_ids = self_and_descendants
- return if group_ids.empty?
-
- Group.by_id(group_ids).update_all(shared_runners_enabled: false)
-
- all_projects.update_all(shared_runners_enabled: false)
+ def access_level_roles
+ GroupMember.access_level_roles
end
- def allow_descendants_override_disabled_shared_runners!
- raise UpdateSharedRunnersError, 'Shared Runners enabled' if shared_runners_enabled?
- raise UpdateSharedRunnersError, 'Group level shared Runners not allowed' unless parent_allows_shared_runners?
-
- update_column(:allow_descendants_override_disabled_shared_runners, true)
+ def access_level_values
+ access_level_roles.values
end
- def disallow_descendants_override_disabled_shared_runners!
- raise UpdateSharedRunnersError, 'Shared Runners enabled' if shared_runners_enabled?
-
- group_ids = self_and_descendants
- return if group_ids.empty?
-
- Group.by_id(group_ids).update_all(allow_descendants_override_disabled_shared_runners: false)
-
- all_projects.update_all(shared_runners_enabled: false)
- end
+ def parent_allows_two_factor_authentication?
+ return true unless has_parent?
- def default_owner
- owners.first || parent&.default_owner || owner
+ ancestor_settings = ancestors.find_by(parent_id: nil).namespace_settings
+ ancestor_settings.allow_mfa_for_subgroups
end
private
@@ -611,6 +601,15 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
+ def two_factor_authentication_allowed
+ return unless has_parent?
+ return unless require_two_factor_authentication
+
+ return if parent_allows_two_factor_authentication?
+
+ errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
+ end
+
def members_from_self_and_ancestor_group_shares
group_group_link_table = GroupGroupLink.arel_table
group_member_table = GroupMember.arel_table
@@ -658,6 +657,45 @@ class Group < Namespace
.new(Group.where(id: group_ids))
.base_and_descendants
end
+
+ def disable_shared_runners!
+ update!(
+ shared_runners_enabled: false,
+ allow_descendants_override_disabled_shared_runners: false)
+
+ group_ids = descendants
+ unless group_ids.empty?
+ Group.by_id(group_ids).update_all(
+ shared_runners_enabled: false,
+ allow_descendants_override_disabled_shared_runners: false)
+ end
+
+ all_projects.update_all(shared_runners_enabled: false)
+ end
+
+ def disable_shared_runners_and_allow_override!
+ # enabled -> disabled_with_override
+ if shared_runners_enabled?
+ update!(
+ shared_runners_enabled: false,
+ allow_descendants_override_disabled_shared_runners: true)
+
+ group_ids = descendants
+ unless group_ids.empty?
+ Group.by_id(group_ids).update_all(shared_runners_enabled: false)
+ end
+
+ all_projects.update_all(shared_runners_enabled: false)
+
+ # disabled_and_unoverridable -> disabled_with_override
+ else
+ update!(allow_descendants_override_disabled_shared_runners: true)
+ end
+ end
+
+ def enable_shared_runners!
+ update!(shared_runners_enabled: true)
+ end
end
Group.prepend_if_ee('EE::Group')
diff --git a/app/models/group_import_state.rb b/app/models/group_import_state.rb
index d22c1ac5550..89602e40357 100644
--- a/app/models/group_import_state.rb
+++ b/app/models/group_import_state.rb
@@ -4,8 +4,9 @@ class GroupImportState < ApplicationRecord
self.primary_key = :group_id
belongs_to :group, inverse_of: :import_state
+ belongs_to :user, optional: false
- validates :group, :status, presence: true
+ validates :group, :status, :user, presence: true
validates :jid, presence: true, if: -> { started? || finished? }
state_machine :status, initial: :created do
diff --git a/app/models/incident_management/project_incident_management_setting.rb b/app/models/incident_management/project_incident_management_setting.rb
index c79acdb685f..4887265be88 100644
--- a/app/models/incident_management/project_incident_management_setting.rb
+++ b/app/models/incident_management/project_incident_management_setting.rb
@@ -51,3 +51,5 @@ module IncidentManagement
end
end
end
+
+IncidentManagement::ProjectIncidentManagementSetting.prepend_if_ee('EE::IncidentManagement::ProjectIncidentManagementSetting')
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
index d68b3dc48ee..35d03a544bd 100644
--- a/app/models/issuable_severity.rb
+++ b/app/models/issuable_severity.rb
@@ -2,6 +2,13 @@
class IssuableSeverity < ApplicationRecord
DEFAULT = 'unknown'
+ SEVERITY_LABELS = {
+ unknown: 'Unknown',
+ low: 'Low - S4',
+ medium: 'Medium - S3',
+ high: 'High - S2',
+ critical: 'Critical - S1'
+ }.freeze
belongs_to :issue
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5a5de371301..5291b7890b6 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -19,6 +19,8 @@ class Issue < ApplicationRecord
include WhereComposite
include StateEventable
include IdInOrdered
+ include Presentable
+ include IssueAvailableFeatures
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -54,6 +56,7 @@ class Issue < ApplicationRecord
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
+ has_many :issue_email_participants
has_many :assignees, class_name: "User", through: :issue_assignees
has_many :zoom_meetings
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -87,6 +90,7 @@ class Issue < ApplicationRecord
alias_method :issuing_parent, :project
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
+ scope :not_in_projects, ->(project_ids) { where.not(project_id: project_ids) }
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
@@ -101,6 +105,8 @@ class Issue < ApplicationRecord
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
+ scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
+ scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_web_entity_associations, -> { preload(:author, :project) }
@@ -122,6 +128,7 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
+ scope :inc_relations_for_view, -> { includes(author: :status) }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
@@ -145,6 +152,7 @@ class Issue < ApplicationRecord
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
+ after_create_commit :record_create_action, unless: :importing?
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -232,6 +240,8 @@ class Issue < ApplicationRecord
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
+ when 'severity_asc' then order_severity_asc.with_order_id_desc
+ when 'severity_desc' then order_severity_desc.with_order_id_desc
else
super
end
@@ -413,6 +423,10 @@ class Issue < ApplicationRecord
IssueLink.inverse_link_type(type)
end
+ def relocation_target
+ moved_to || duplicated_to
+ end
+
private
def ensure_metrics
@@ -420,6 +434,10 @@ class Issue < ApplicationRecord
metrics.record!
end
+ def record_create_action
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author)
+ end
+
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
index e57acbae546..7f3d552b3d9 100644
--- a/app/models/issue_assignee.rb
+++ b/app/models/issue_assignee.rb
@@ -8,6 +8,7 @@ class IssueAssignee < ApplicationRecord
scope :in_projects, ->(project_ids) { joins(:issue).where("issues.project_id in (?)", project_ids) }
scope :on_issues, ->(issue_ids) { where(issue_id: issue_ids) }
+ scope :for_assignee, ->(user) { where(assignee: user) }
end
IssueAssignee.prepend_if_ee('EE::IssueAssignee')
diff --git a/app/models/issue_email_participant.rb b/app/models/issue_email_participant.rb
new file mode 100644
index 00000000000..8eb9b6a8152
--- /dev/null
+++ b/app/models/issue_email_participant.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class IssueEmailParticipant < ApplicationRecord
+ belongs_to :issue
+
+ validates :email, presence: true, uniqueness: { scope: [:issue_id] }
+ validates :issue, presence: true
+ validate :validate_email_format
+
+ def validate_email_format
+ self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
+ end
+end
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index d223c80fca0..bd245de411c 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -94,13 +94,25 @@ class Iteration < ApplicationRecord
private
+ def parent_group
+ group || project.group
+ end
+
def start_or_due_dates_changed?
start_date_changed? || due_date_changed?
end
- # ensure dates do not overlap with other Iterations in the same group/project
+ # ensure dates do not overlap with other Iterations in the same group/project tree
def dates_do_not_overlap
- return unless resource_parent.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
+ iterations = if parent_group.present? && resource_parent.is_a?(Project)
+ Iteration.where(group: parent_group.self_and_ancestors).or(project.iterations)
+ elsif parent_group.present?
+ Iteration.where(group: parent_group.self_and_ancestors)
+ else
+ project.iterations
+ end
+
+ return unless iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations"))
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 7ea9caa45d3..498e03b2c1a 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -80,7 +80,10 @@ class Member < ApplicationRecord
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
- scope :not_accepted_invitations_by_user, -> (user) { invite.where(invite_accepted_at: nil, created_by: user) }
+ scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
+ scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
+ scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
+ scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
scope :has_access, -> { active.where('access_level > 0') }
@@ -372,6 +375,14 @@ class Member < ApplicationRecord
send_invite
end
+ def send_invitation_reminder(reminder_index)
+ return unless invite?
+
+ generate_invite_token! unless @raw_invite_token
+
+ run_after_commit_or_now { notification_service.invite_member_reminder(self, @raw_invite_token, reminder_index) }
+ end
+
def create_notification_setting
user.notification_settings.find_or_create_for(source)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 3fdc501644d..24541ba3218 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -31,6 +31,7 @@ class MergeRequest < ApplicationRecord
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
self.reactive_cache_lifetime = 10.minutes
+ self.reactive_cache_work_type = :no_dependency
SORTING_PREFERENCE_FIELD = :merge_requests_sort
@@ -121,6 +122,8 @@ class MergeRequest < ApplicationRecord
# when creating new merge request
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
+ participant :reviewers
+
# Keep states definition to be evaluated before the state_machine block to avoid spec failures.
# If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil.
def self.available_state_names
@@ -255,11 +258,7 @@ class MergeRequest < ApplicationRecord
scope :join_project, -> { joins(:target_project) }
scope :join_metrics, -> do
query = joins(:metrics)
-
- if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
- query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
- end
-
+ query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
query
end
scope :references_project, -> { references(:target_project) }
@@ -271,6 +270,8 @@ class MergeRequest < ApplicationRecord
metrics: [:latest_closed_by, :merged_by])
}
+ scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
+
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
end
@@ -629,7 +630,7 @@ class MergeRequest < ApplicationRecord
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
- merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size
+ merge_request_diff&.real_size || diff_stats&.real_size(project: project) || diffs.real_size
end
def modified_paths(past_merge_request_diff: nil, fallback_on_overflow: false)
@@ -928,7 +929,7 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def diffable_merge_ref?
- can_be_merged? && merge_ref_head.present?
+ merge_ref_head.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?)
end
# Returns boolean indicating the merge_status should be rechecked in order to
@@ -1301,6 +1302,14 @@ class MergeRequest < ApplicationRecord
unlock_mr
end
+ def update_and_mark_in_progress_merge_commit_sha(commit_id)
+ self.update(in_progress_merge_commit_sha: commit_id)
+ # Since another process checks for matching merge request, we need
+ # to make it possible to detect whether the query should go to the
+ # primary.
+ target_project.mark_primary_write_location
+ end
+
def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
@@ -1375,8 +1384,6 @@ class MergeRequest < ApplicationRecord
end
def has_coverage_reports?
- return false unless Feature.enabled?(:coverage_report_view, project, default_enabled: true)
-
actual_head_pipeline&.has_coverage_reports?
end
@@ -1511,6 +1518,7 @@ class MergeRequest < ApplicationRecord
metrics&.merged_at ||
merge_event&.created_at ||
+ resource_state_events.find_by(state: :merged)&.created_at ||
notes.system.reorder(nil).find_by(note: 'merged')&.created_at
end
end
@@ -1591,6 +1599,12 @@ class MergeRequest < ApplicationRecord
.find_by(sha: diff_base_sha, ref: target_branch)
end
+ def merge_base_pipeline
+ @merge_base_pipeline ||= project.ci_pipelines
+ .order(id: :desc)
+ .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
+ end
+
def discussions_rendered_on_frontend?
true
end
@@ -1680,6 +1694,10 @@ class MergeRequest < ApplicationRecord
Feature.enabled?(:merge_request_reviewers, project)
end
+ def allows_multiple_reviewers?
+ false
+ end
+
private
def with_rebase_lock
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index a2982a5dd73..59cc82cfaf5 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -22,8 +22,8 @@ class MergeRequestContextCommit < ApplicationRecord
end
# create MergeRequestContextCommit by given commit sha and it's diff file record
- def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert
+ def self.bulk_insert(rows, **args)
+ Gitlab::Database.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 880e3cc1ba5..24809141570 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -106,6 +106,17 @@ class MergeRequestDiff < ApplicationRecord
joins(merge_request: :metrics).where(condition)
end
+ scope :latest_diff_for_merge_requests, -> (merge_requests) do
+ inner_select = MergeRequestDiff
+ .default_scoped
+ .distinct
+ .select("FIRST_VALUE(id) OVER (PARTITION BY merge_request_id ORDER BY created_at DESC) as id")
+ .where(merge_request: merge_requests)
+
+ joins("INNER JOIN (#{inner_select.to_sql}) latest_diffs ON latest_diffs.id = merge_request_diffs.id")
+ .includes(:merge_request_diff_commits)
+ end
+
class << self
def ids_for_external_storage_migration(limit:)
return [] unless Gitlab.config.external_diffs.enabled
@@ -280,7 +291,13 @@ class MergeRequestDiff < ApplicationRecord
end
def commit_shas(limit: nil)
- merge_request_diff_commits.limit(limit).pluck(:sha)
+ if association(:merge_request_diff_commits).loaded?
+ sorted_diff_commits = merge_request_diff_commits.sort_by { |diff_commit| [diff_commit.id, diff_commit.relative_order] }
+ sorted_diff_commits = sorted_diff_commits.take(limit) if limit
+ sorted_diff_commits.map(&:sha)
+ else
+ merge_request_diff_commits.limit(limit).pluck(:sha)
+ end
end
def includes_any_commits?(shas)
@@ -509,6 +526,8 @@ class MergeRequestDiff < ApplicationRecord
end
def encode_in_base64?(diff_text)
+ return false if diff_text.nil?
+
(diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?) ||
diff_text.include?("\0")
end
@@ -536,7 +555,7 @@ class MergeRequestDiff < ApplicationRecord
rows.each do |row|
data = row.delete(:diff)
row[:external_diff_offset] = file.pos
- row[:external_diff_size] = data.bytesize
+ row[:external_diff_size] = data&.bytesize || 0
file.write(data)
end
@@ -651,7 +670,7 @@ class MergeRequestDiff < ApplicationRecord
if compare.commits.empty?
new_attributes[:state] = :empty
else
- diff_collection = compare.diffs(Commit.max_diff_options)
+ diff_collection = compare.diffs(Commit.max_diff_options(project: merge_request.project))
new_attributes[:real_size] = diff_collection.real_size
if diff_collection.any?
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 55326b9a282..0a315ba8db2 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -46,6 +46,10 @@ class Milestone < ApplicationRecord
state :active
end
+ def self.min_chars_for_partial_matching
+ 2
+ end
+
def self.reference_prefix
'%'
end
diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb
index 0a6165c8254..2f2bf91e436 100644
--- a/app/models/milestone_release.rb
+++ b/app/models/milestone_release.rb
@@ -11,6 +11,10 @@ class MilestoneRelease < ApplicationRecord
def same_project_between_milestone_and_release
return if milestone&.project_id == release&.project_id
+ return if milestone&.group_id
+
errors.add(:base, _('Release does not have the same project as the milestone'))
end
end
+
+MilestoneRelease.prepend_if_ee('EE::MilestoneRelease')
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 527fa9d52d0..fd31042c2f6 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -18,6 +18,8 @@ class Namespace < ApplicationRecord
# Android repo (15) + some extra backup.
NUMBER_OF_ANCESTORS_ALLOWED = 20
+ SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
+
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -59,6 +61,8 @@ class Namespace < ApplicationRecord
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
validate :nesting_level_allowed
+ validate :changing_shared_runners_enabled_is_allowed
+ validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed
validates_associated :runners
@@ -79,6 +83,7 @@ class Namespace < ApplicationRecord
scope :for_user, -> { where('type IS NULL') }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
+ scope :include_route, -> { includes(:route) }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
@@ -278,7 +283,11 @@ class Namespace < ApplicationRecord
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
- Project.inside_path(full_path)
+ if Feature.enabled?(:recursive_approach_for_all_projects)
+ Project.where(namespace: self_and_descendants)
+ else
+ Project.inside_path(full_path)
+ end
end
# Includes pipelines from this namespace and pipelines from all subgroups
@@ -378,6 +387,52 @@ class Namespace < ApplicationRecord
actual_plan.name
end
+ def changing_shared_runners_enabled_is_allowed
+ return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
+ return unless new_record? || changes.has_key?(:shared_runners_enabled)
+
+ if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
+ errors.add(:shared_runners_enabled, _('cannot be enabled because parent group has shared Runners disabled'))
+ end
+ end
+
+ def changing_allow_descendants_override_disabled_shared_runners_is_allowed
+ return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
+ return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
+
+ if shared_runners_enabled && !new_record?
+ errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled'))
+ end
+
+ if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
+ errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be enabled because parent group does not allow it'))
+ end
+ end
+
+ def shared_runners_setting
+ if shared_runners_enabled
+ 'enabled'
+ else
+ if allow_descendants_override_disabled_shared_runners
+ 'disabled_with_override'
+ else
+ 'disabled_and_unoverridable'
+ end
+ end
+ end
+
+ def shared_runners_setting_higher_than?(other_setting)
+ if other_setting == 'enabled'
+ false
+ elsif other_setting == 'disabled_with_override'
+ shared_runners_setting == 'enabled'
+ elsif other_setting == 'disabled_and_unoverridable'
+ shared_runners_setting == 'enabled' || shared_runners_setting == 'disabled_with_override'
+ else
+ raise ArgumentError
+ end
+ end
+
private
def all_projects_with_pages
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 53bfa3d979e..6f31208f28b 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -3,7 +3,26 @@
class NamespaceSetting < ApplicationRecord
belongs_to :namespace, inverse_of: :namespace_settings
+ validate :default_branch_name_content
+ validate :allow_mfa_for_group
+
+ NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
+
self.primary_key = :namespace_id
+
+ def default_branch_name_content
+ return if default_branch_name.nil?
+
+ if default_branch_name.blank?
+ errors.add(:default_branch_name, "can not be an empty string")
+ end
+ end
+
+ def allow_mfa_for_group
+ if namespace&.subgroup? && allow_mfa_for_subgroups == false
+ errors.add(:allow_mfa_for_subgroups, _('is not allowed since the group is not top-level group.'))
+ end
+ end
end
NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')
diff --git a/app/models/note.rb b/app/models/note.rb
index 812d77d5f86..954843505d4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -322,8 +322,6 @@ class Note < ApplicationRecord
end
def contributor?
- return false unless ::Feature.enabled?(:show_contributor_on_note, project)
-
project&.team&.contributor?(self.author_id)
end
diff --git a/app/models/notification_reason.rb b/app/models/notification_reason.rb
index a7967239417..c227626af9e 100644
--- a/app/models/notification_reason.rb
+++ b/app/models/notification_reason.rb
@@ -5,6 +5,7 @@
class NotificationReason
OWN_ACTIVITY = 'own_activity'
ASSIGNED = 'assigned'
+ REVIEW_REQUESTED = 'review_requested'
MENTIONED = 'mentioned'
SUBSCRIBED = 'subscribed'
@@ -12,6 +13,7 @@ class NotificationReason
REASON_PRIORITY = [
OWN_ACTIVITY,
ASSIGNED,
+ REVIEW_REQUESTED,
MENTIONED,
SUBSCRIBED
].freeze
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 6a6b2bb1b58..79a84231083 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -5,7 +5,7 @@ class NotificationRecipient
attr_reader :user, :type, :reason
- def initialize(user, type, **opts)
+ def initialize(user, type, opts = {})
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index c003a20f0fc..6066046a722 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -43,6 +43,7 @@ class NotificationSetting < ApplicationRecord
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
+ :change_reviewer_merge_request,
:merge_merge_request,
:failed_pipeline,
:fixed_pipeline,
diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb
index ff68af9741e..c70e10c72d5 100644
--- a/app/models/operations/feature_flags/strategy.rb
+++ b/app/models/operations/feature_flags/strategy.rb
@@ -6,14 +6,17 @@ module Operations
STRATEGY_DEFAULT = 'default'
STRATEGY_GITLABUSERLIST = 'gitlabUserList'
STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'
+ STRATEGY_FLEXIBLEROLLOUT = 'flexibleRollout'
STRATEGY_USERWITHID = 'userWithId'
STRATEGIES = {
STRATEGY_DEFAULT => [].freeze,
STRATEGY_GITLABUSERLIST => [].freeze,
STRATEGY_GRADUALROLLOUTUSERID => %w[groupId percentage].freeze,
+ STRATEGY_FLEXIBLEROLLOUT => %w[groupId rollout stickiness].freeze,
STRATEGY_USERWITHID => ['userIds'].freeze
}.freeze
USERID_MAX_LENGTH = 256
+ STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze
self.table_name = 'operations_strategies'
@@ -67,16 +70,25 @@ module Operations
case name
when STRATEGY_GRADUALROLLOUTUSERID
gradual_rollout_user_id_parameters_validation
+ when STRATEGY_FLEXIBLEROLLOUT
+ flexible_rollout_parameters_validation
when STRATEGY_USERWITHID
FeatureFlagUserXidsValidator.validate_user_xids(self, :parameters, parameters['userIds'], 'userIds')
end
end
+ def within_range?(value, min, max)
+ return false unless value.is_a?(String)
+ return false unless value.match?(/\A\d+\z/)
+
+ value.to_i.between?(min, max)
+ end
+
def gradual_rollout_user_id_parameters_validation
percentage = parameters['percentage']
group_id = parameters['groupId']
- unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/)
+ unless within_range?(percentage, 0, 100)
parameters_error('percentage must be a string between 0 and 100 inclusive')
end
@@ -85,6 +97,25 @@ module Operations
end
end
+ def flexible_rollout_parameters_validation
+ stickiness = parameters['stickiness']
+ group_id = parameters['groupId']
+ rollout = parameters['rollout']
+
+ unless STICKINESS_SETTINGS.include?(stickiness)
+ options = STICKINESS_SETTINGS.to_sentence(last_word_connector: ', or ')
+ parameters_error("stickiness parameter must be #{options}")
+ end
+
+ unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/)
+ parameters_error('groupId parameter is invalid')
+ end
+
+ unless within_range?(rollout, 0, 100)
+ parameters_error('rollout must be a string between 0 and 100 inclusive')
+ end
+ end
+
def parameters_error(message)
errors.add(:parameters, message)
false
diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb
new file mode 100644
index 00000000000..f1d0af64ccd
--- /dev/null
+++ b/app/models/packages/event.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Packages::Event < ApplicationRecord
+ belongs_to :package, optional: true
+
+ EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze
+
+ enum event_scope: EVENT_SCOPES
+
+ enum event_type: {
+ push_package: 0,
+ delete_package: 1,
+ pull_package: 2,
+ search_package: 3,
+ list_package: 4,
+ list_repositories: 5,
+ delete_repository: 6,
+ delete_tag: 7,
+ delete_tag_bulk: 8,
+ list_tags: 9,
+ cli_metadata: 10
+ }
+
+ enum originator_type: { user: 0, deploy_token: 1, guest: 2 }
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index bda11160957..a57d640ddc0 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -26,7 +26,7 @@ class Packages::Package < ApplicationRecord
validates :project, presence: true
validates :name, presence: true
- validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: :conan?
+ validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: -> { conan? || generic? }
validates :name,
uniqueness: { scope: %i[project_id version package_type] }, unless: :conan?
@@ -35,20 +35,24 @@ class Packages::Package < ApplicationRecord
validate :valid_npm_package_name, if: :npm?
validate :valid_composer_global_name, if: :composer?
validate :package_already_taken, if: :npm?
- validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? }
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 :version, format: { with: Gitlab::Regex.semver_regex }, if: :npm?
+ validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
+ validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang?
validates :version,
presence: true,
format: { with: Gitlab::Regex.generic_package_version_regex },
if: :generic?
- enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7 }
+ enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7, golang: 8, debian: 9 }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
+ scope :with_normalized_pypi_name, ->(name) { where("LOWER(regexp_replace(name, '[-_.]+', '-', 'g')) = ?", name.downcase) }
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
@@ -119,6 +123,10 @@ class Packages::Package < ApplicationRecord
.where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last!
end
+ def self.by_name_and_version!(name, version)
+ find_by!(name: name, version: version)
+ end
+
def self.pluck_names
pluck(:name)
end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index 78e0f185a11..cd952c32046 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -2,10 +2,24 @@
# PagesDeployment stores a zip archive containing GitLab Pages web-site
class PagesDeployment < ApplicationRecord
+ include FileStoreMounter
+
belongs_to :project, optional: false
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
validates :size, presence: true, numericality: { greater_than: 0, only_integer: true }
+
+ before_validation :set_size, if: :file_changed?
+
+ default_value_for(:file_store) { ::Pages::DeploymentUploader.default_store }
+
+ mount_file_store_uploader ::Pages::DeploymentUploader
+
+ private
+
+ def set_size
+ self.size = file.size
+ end
end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index a4370eda5ba..c96786423e5 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -22,8 +22,8 @@ module Postgresql
def self.lag_too_great?(max = 100.megabytes)
return false unless in_use?
- lag_function = "#{Gitlab::Database.pg_wal_lsn_diff}" \
- "(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)::bigint"
+ lag_function = "pg_wal_lsn_diff" \
+ "(pg_current_wal_insert_lsn(), restart_lsn)::bigint"
# We force the use of a transaction here so the query always goes to the
# primary, even when using the EE DB load balancer.
diff --git a/app/models/preloaders/merge_request_diff_preloader.rb b/app/models/preloaders/merge_request_diff_preloader.rb
new file mode 100644
index 00000000000..ee9995c497d
--- /dev/null
+++ b/app/models/preloaders/merge_request_diff_preloader.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Preloaders
+ # This class preloads the `merge_request_diff` association for the given merge request models.
+ #
+ # Usage:
+ # merge_requests = MergeRequest.where(...)
+ # Preloaders::MergeRequestDiffPreloader.new(merge_requests).preload_all
+ # merge_requests.first.merge_request_diff # won't fire any query
+ class MergeRequestDiffPreloader
+ def initialize(merge_requests)
+ @merge_requests = merge_requests
+ end
+
+ def preload_all
+ merge_request_diffs = MergeRequestDiff.latest_diff_for_merge_requests(@merge_requests)
+ cache = merge_request_diffs.index_by { |diff| diff.merge_request_id }
+
+ @merge_requests.each do |merge_request|
+ merge_request_diff = cache[merge_request.id]
+
+ merge_request.association(:merge_request_diff).target = merge_request_diff
+ merge_request.association(:merge_request_diff).loaded!
+ end
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 4db0eaa0442..dbedd6d120c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -33,7 +33,9 @@ class Project < ApplicationRecord
include FromUnion
include IgnorableColumns
include Integration
+ include EachBatch
extend Gitlab::Cache::RequestCache
+ extend Gitlab::Utils::Override
extend Gitlab::ConfigHelper
@@ -198,6 +200,7 @@ class Project < ApplicationRecord
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :export_jobs, class_name: 'ProjectExportJob'
has_one :project_repository, inverse_of: :project
+ has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
@@ -268,6 +271,7 @@ class Project < ApplicationRecord
has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :project
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
+ has_many :alert_management_http_integrations, class_name: 'AlertManagement::HttpIntegration', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -294,6 +298,7 @@ class Project < ApplicationRecord
# bulk that doesn't involve loading the rows into memory. As a result we're
# 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
@@ -336,6 +341,8 @@ class Project < ApplicationRecord
has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :reviews, inverse_of: :project
+ has_many :terraform_states, class_name: 'Terraform::State', inverse_of: :project
+
# GitLab Pages
has_many :pages_domains
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
@@ -361,6 +368,7 @@ class Project < ApplicationRecord
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+ accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :incident_management_setting, update_only: true
accepts_nested_attributes_for :error_tracking_setting, update_only: true
accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
@@ -392,7 +400,7 @@ class Project < ApplicationRecord
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
- delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings
+ delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci
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?,
@@ -432,6 +440,7 @@ class Project < ApplicationRecord
validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
+ validate :changing_shared_runners_enabled_is_allowed
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
@@ -560,6 +569,7 @@ class Project < ApplicationRecord
}
scope :imported_from, -> (type) { where(import_type: type) }
+ scope :with_tracing_enabled, -> { joins(:tracing_setting) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -595,7 +605,7 @@ class Project < ApplicationRecord
return public_to_user unless user
if user.is_a?(DeployToken)
- user.projects
+ user.accessible_projects
else
where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects(min_access_level: min_access_level),
@@ -667,8 +677,6 @@ class Project < ApplicationRecord
scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
scope :for_group, -> (group) { where(group: group) }
scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) }
- scope :for_repository_storage, -> (repository_storage) { where(repository_storage: repository_storage) }
- scope :excluding_repository_storage, -> (repository_storage) { where.not(repository_storage: repository_storage) }
class << self
# Searches for a list of projects based on the query given in `query`.
@@ -842,6 +850,7 @@ class Project < ApplicationRecord
end
end
+ override :lfs_enabled?
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
@@ -942,7 +951,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
return unless latest_pipeline
- latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
+ latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
end
def latest_successful_build_for_sha(job_name, sha)
@@ -951,7 +960,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_sha(sha)
return unless latest_pipeline
- latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
+ latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)
@@ -991,9 +1000,6 @@ class Project < ApplicationRecord
job_id =
if forked?
RepositoryForkWorker.perform_async(id)
- elsif gitlab_project_import?
- # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-foss/issues/26189 is solved.
- RepositoryImportWorker.set(retry: false).perform_async(self.id)
else
RepositoryImportWorker.perform_async(self.id)
end
@@ -1186,6 +1192,15 @@ class Project < ApplicationRecord
end
end
+ def changing_shared_runners_enabled_is_allowed
+ return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
+ return unless new_record? || changes.has_key?(:shared_runners_enabled)
+
+ if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
+ errors.add(:shared_runners_enabled, _('cannot be enabled because parent group does not allow it'))
+ end
+ end
+
def to_param
if persisted? && errors.include?(:path)
path_was
@@ -1325,7 +1340,8 @@ class Project < ApplicationRecord
end
def find_or_initialize_services
- available_services_names = Service.available_services_names - disabled_services
+ available_services_names =
+ Service.available_services_names + Service.project_specific_services_names - disabled_services
available_services_names.map do |service_name|
find_or_initialize_service(service_name)
@@ -2292,6 +2308,10 @@ class Project < ApplicationRecord
[]
end
+ def mark_primary_write_location
+ # Overriden in EE
+ end
+
def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute)
end
@@ -2495,12 +2515,25 @@ class Project < ApplicationRecord
ci_config_path.presence || Ci::Pipeline::DEFAULT_CONFIG_PATH
end
+ def ci_config_for(sha)
+ repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
+ end
+
def enabled_group_deploy_keys
return GroupDeployKey.none unless group
GroupDeployKey.for_groups(group.self_and_ancestors_ids)
end
+ def feature_flags_client_token
+ instance = operations_feature_flags_client || create_operations_feature_flags_client!
+ instance.token
+ end
+
+ def tracing_external_url
+ tracing_setting&.external_url
+ end
+
private
def find_service(services, name)
@@ -2509,10 +2542,10 @@ class Project < ApplicationRecord
def build_from_instance_or_template(name)
instance = find_service(services_instances, name)
- return Service.build_from_integration(id, instance) if instance
+ return Service.build_from_integration(instance, project_id: id) if instance
template = find_service(services_templates, name)
- return Service.build_from_integration(id, template) if template
+ return Service.build_from_integration(template, project_id: id) if template
end
def services_templates
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index 8a1db4a9acf..bd1919fe7ed 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -5,6 +5,7 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :project, inverse_of: :pages_metadatum
belongs_to :artifacts_archive, class_name: 'Ci::JobArtifact'
+ belongs_to :pages_deployment
scope :deployed, -> { where(deployed: true) }
end
diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb
index 2b74d9ccd88..76f428fe925 100644
--- a/app/models/project_repository_storage_move.rb
+++ b/app/models/project_repository_storage_move.rb
@@ -20,6 +20,10 @@ class ProjectRepositoryStorageMove < ApplicationRecord
inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
validate :project_repository_writable, on: :create
+ default_value_for(:destination_storage_name, allows_nil: false) do
+ pick_repository_storage
+ end
+
state_machine initial: :initial do
event :schedule do
transition initial: :scheduled
@@ -77,6 +81,12 @@ class ProjectRepositoryStorageMove < ApplicationRecord
scope :order_created_at_desc, -> { order(created_at: :desc) }
scope :with_projects, -> { includes(project: :route) }
+ class << self
+ def pick_repository_storage
+ Project.pick_repository_storage
+ end
+ end
+
private
def project_repository_writable
diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb
index dae3a56116e..5deb757e60f 100644
--- a/app/models/project_services/chat_message/deployment_message.rb
+++ b/app/models/project_services/chat_message/deployment_message.rb
@@ -38,7 +38,11 @@ module ChatMessage
private
def message
- "Deploy to #{environment} #{humanized_status}"
+ if running?
+ "Starting deploy to #{environment}"
+ else
+ "Deploy to #{environment} #{humanized_status}"
+ end
end
def color
@@ -73,5 +77,9 @@ module ChatMessage
def humanized_status
status == 'success' ? 'succeeded' : status
end
+
+ def running?
+ status == 'running'
+ end
end
end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index 0cdcfcf0237..c8e90b66bae 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -41,15 +41,11 @@ module ChatMessage
private
def message
- if opened_issue?
- "[#{project_link}] Issue #{state} by #{user_combined_name}"
- else
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
- end
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
end
def opened_issue?
- action == "open"
+ action == 'open'
end
def description_message
@@ -57,7 +53,7 @@ module ChatMessage
title: issue_title,
title_link: issue_url,
text: format(description),
- color: "#C95823"
+ color: '#C95823'
}]
end
diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb
index dd44a0d1d56..6db446fc04c 100644
--- a/app/models/project_services/confluence_service.rb
+++ b/app/models/project_services/confluence_service.rb
@@ -27,7 +27,7 @@ class ConfluenceService < Service
end
def description
- s_('ConfluenceService|Connect a Confluence Cloud Workspace to your GitLab project')
+ s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
end
def detailed_description
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 4e4955b45d8..5a49f780d46 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -42,7 +42,7 @@ class DroneCiService < CiService
def commit_status_path(sha, ref)
Gitlab::Utils.append_path(
drone_url,
- "gitlab/#{project.full_path}/commits/#{sha}?branch=#{URI.encode(ref.to_s)}&access_token=#{token}")
+ "gitlab/#{project.full_path}/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}&access_token=#{token}")
end
def commit_status(sha, ref)
@@ -75,7 +75,7 @@ class DroneCiService < CiService
def build_page(sha, ref)
Gitlab::Utils.append_path(
drone_url,
- "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{URI.encode(ref.to_s)}")
+ "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
def title
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
index 35dbedd1341..21f0a2b2463 100644
--- a/app/models/project_services/packagist_service.rb
+++ b/app/models/project_services/packagist_service.rb
@@ -16,7 +16,7 @@ class PackagistService < Service
end
def description
- 'Update your project on Packagist, the main Composer repository'
+ s_('Integrations|Update your projects on Packagist, the main Composer repository')
end
def self.to_param
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 67ab2c0ce8a..0d2f89fb18d 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -2,6 +2,7 @@
class ProjectStatistics < ApplicationRecord
include AfterCommitQueue
+ include CounterAttribute
belongs_to :project
belongs_to :namespace
@@ -9,6 +10,13 @@ class ProjectStatistics < ApplicationRecord
default_value_for :wiki_size, 0
default_value_for :snippets_size, 0
+ counter_attribute :build_artifacts_size
+ counter_attribute :storage_size
+
+ counter_attribute_after_flush do |project_statistic|
+ Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id)
+ end
+
before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze
@@ -29,6 +37,8 @@ class ProjectStatistics < ApplicationRecord
end
def refresh!(only: [])
+ return if Gitlab::Database.read_only?
+
COLUMNS_TO_REFRESH.each do |column, generator|
if only.empty? || only.include?(column)
public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
@@ -96,12 +106,27 @@ class ProjectStatistics < ApplicationRecord
# Additional columns are updated depending on key => [columns], which allows
# to update statistics which are and also those which aren't included in storage_size
# or any other additional summary column in the future.
- def self.increment_statistic(project_id, key, amount)
+ def self.increment_statistic(project, key, amount)
raise ArgumentError, "Cannot increment attribute: #{key}" unless INCREMENTABLE_COLUMNS.key?(key)
return if amount == 0
- where(project_id: project_id)
- .columns_to_increment(key, amount)
+ project.statistics.try do |project_statistics|
+ if project_statistics.counter_attribute_enabled?(key)
+ statistics_to_increment = [key] + INCREMENTABLE_COLUMNS[key].to_a
+ statistics_to_increment.each do |statistic|
+ project_statistics.delayed_increment_counter(statistic, amount)
+ end
+ else
+ legacy_increment_statistic(project, key, amount)
+ end
+ end
+ end
+
+ def self.legacy_increment_statistic(project, key, amount)
+ where(project_id: project.id).columns_to_increment(key, amount)
+
+ Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker
+ project.namespace_id)
end
def self.columns_to_increment(key, amount)
diff --git a/app/models/project_tracing_setting.rb b/app/models/project_tracing_setting.rb
new file mode 100644
index 00000000000..93fa80aed67
--- /dev/null
+++ b/app/models/project_tracing_setting.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ProjectTracingSetting < ApplicationRecord
+ belongs_to :project
+
+ validates :external_url, length: { maximum: 255 }, public_url: true
+
+ before_validation :sanitize_external_url
+
+ private
+
+ def sanitize_external_url
+ self.external_url = Rails::Html::FullSanitizer.new.sanitize(self.external_url)
+ end
+end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index bd570cf7ead..91fb3d4e4ba 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
class ProjectWiki < Wiki
+ self.container_class = Project
alias_method :project, :container
# Project wikis are tied to the main project storage
- delegate :storage, :repository_storage, :hashed_storage?, to: :container
+ delegate :storage, :repository_storage, :hashed_storage?, :lfs_enabled?, to: :container
override :disk_path
def disk_path(*args, &block)
diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb
index f0441d4a3cb..684f50d5f58 100644
--- a/app/models/prometheus_alert.rb
+++ b/app/models/prometheus_alert.rb
@@ -4,6 +4,7 @@ class PrometheusAlert < ApplicationRecord
include Sortable
include UsageStatistics
include Presentable
+ include EachBatch
OPERATORS_MAP = {
lt: "<",
@@ -35,6 +36,7 @@ class PrometheusAlert < ApplicationRecord
scope :for_metric, -> (metric) { where(prometheus_metric: metric) }
scope :for_project, -> (project) { where(project_id: project) }
scope :for_environment, -> (environment) { where(environment_id: environment) }
+ scope :get_environment_id, -> { select(:environment_id).pluck(:environment_id) }
def self.distinct_projects
sub_query = self.group(:project_id).select(1)
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index 9ddf66cd388..590eda62c11 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class PrometheusMetric < ApplicationRecord
+ include EachBatch
+
belongs_to :project, validate: true, inverse_of: :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :prometheus_metric
diff --git a/app/models/release.rb b/app/models/release.rb
index 4c9d89105d7..f2162a0f674 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -30,6 +30,12 @@ class Release < ApplicationRecord
scope :with_project_and_namespace, -> { includes(project: :namespace) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
+ # Sorting
+ scope :order_created, -> { reorder('created_at ASC') }
+ scope :order_created_desc, -> { reorder('created_at DESC') }
+ scope :order_released, -> { reorder('released_at ASC') }
+ scope :order_released_desc, -> { reorder('released_at DESC') }
+
delegate :repository, to: :project
MAX_NUMBER_TO_DISPLAY = 3
@@ -92,6 +98,17 @@ class Release < ApplicationRecord
def set_released_at
self.released_at ||= created_at
end
+
+ def self.sort_by_attribute(method)
+ case method.to_s
+ when 'created_at_asc' then order_created
+ when 'created_at_desc' then order_created_desc
+ when 'released_at_asc' then order_released
+ when 'released_at_desc' then order_released_desc
+ else
+ order_created_desc
+ end
+ end
end
Release.prepend_if_ee('EE::Release')
diff --git a/app/models/repository.rb b/app/models/repository.rb
index ef17e010ba8..d4fd202b966 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -26,6 +26,7 @@ class Repository
delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, to: :raw_repository
+ delegate :lfs_enabled?, to: :container
CreateTreeError = Class.new(StandardError)
AmbiguousRefError = Class.new(StandardError)
@@ -853,16 +854,16 @@ class Repository
def merge(user, source_sha, merge_request, message)
with_cache_hooks do
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
- merge_request.update(in_progress_merge_commit_sha: commit_id)
+ merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
nil # Return value does not matter.
end
end
end
- def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref)
+ def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref, allow_conflicts = false)
branch = merge_request.target_branch
- raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
+ raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref, allow_conflicts)
end
def delete_refs(*ref_names)
@@ -873,7 +874,7 @@ class Repository
their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil?
- merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
+ merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id)
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
@@ -1142,21 +1143,10 @@ class Repository
end
def project
- if repo_type.snippet?
- container.project
- elsif container.is_a?(Project)
- container
- end
- end
-
- # TODO: pass this in directly to `Blob` rather than delegating it to here
- #
- # https://gitlab.com/gitlab-org/gitlab/-/issues/201886
- def lfs_enabled?
if container.is_a?(Project)
- container.lfs_enabled?
+ container
else
- false # LFS is not supported for snippet or group repositories
+ container.try(:project)
end
end
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index cc96698be09..18e2944a9ca 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -15,6 +15,7 @@ class ResourceLabelEvent < ResourceEvent
validate :exactly_one_issuable
after_save :expire_etag_cache
+ after_save :usage_metrics
after_destroy :expire_etag_cache
enum action: {
@@ -113,6 +114,16 @@ class ResourceLabelEvent < ResourceEvent
def discussion_id_key
[self.class.name, created_at, user_id]
end
+
+ def for_issue?
+ issue_id.present?
+ end
+
+ def usage_metrics
+ return unless for_issue?
+
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(author: user)
+ end
end
ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent')
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 1ce4e14d289..6475633868a 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -11,6 +11,8 @@ class ResourceStateEvent < ResourceEvent
# state is used for issue and merge request states.
enum state: Issue.available_states.merge(MergeRequest.available_states).merge(reopened: 5)
+ after_save :usage_metrics
+
def self.issuable_attrs
%i(issue merge_request).freeze
end
@@ -18,6 +20,29 @@ class ResourceStateEvent < ResourceEvent
def issuable
issue || merge_request
end
+
+ def for_issue?
+ issue_id.present?
+ end
+
+ private
+
+ def usage_metrics
+ return unless for_issue?
+
+ case state
+ when 'closed'
+ issue_usage_counter.track_issue_closed_action(author: user)
+ when 'reopened'
+ issue_usage_counter.track_issue_reopened_action(author: user)
+ else
+ # no-op, nothing to do, not a state we're tracking
+ end
+ end
+
+ def issue_usage_counter
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter
+ end
end
ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index 44f48915425..dbb2b428c7b 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -13,6 +13,8 @@ class ResourceTimeboxEvent < ResourceEvent
remove: 2
}
+ after_save :usage_metrics
+
def self.issuable_attrs
%i(issue merge_request).freeze
end
@@ -20,4 +22,17 @@ class ResourceTimeboxEvent < ResourceEvent
def issuable
issue || merge_request
end
+
+ private
+
+ def usage_metrics
+ case self
+ when ResourceMilestoneEvent
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user)
+ when ResourceIterationEvent
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_iteration_changed_action(author: user)
+ else
+ # no-op
+ end
+ end
end
diff --git a/app/models/resource_weight_event.rb b/app/models/resource_weight_event.rb
deleted file mode 100644
index bbabd54325e..00000000000
--- a/app/models/resource_weight_event.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class ResourceWeightEvent < ResourceEvent
- validates :issue, presence: true
-
- include IssueResourceEvent
-end
diff --git a/app/models/service.rb b/app/models/service.rb
index e63e06bf46f..764f417362f 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -7,9 +7,7 @@ class Service < ApplicationRecord
include Importable
include ProjectServicesLoggable
include DataFields
- include IgnorableColumns
-
- ignore_columns %i[default], remove_with: '13.5', remove_after: '2020-10-22'
+ include FromUnion
SERVICE_NAMES = %w[
alerts asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
@@ -65,6 +63,7 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) }
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 :for_group, -> (group) { where(group_id: group, type: available_services_types) }
scope :for_template, -> { where(template: true, type: available_services_types) }
scope :for_instance, -> { where(instance: true, type: available_services_types) }
@@ -209,6 +208,10 @@ class Service < ApplicationRecord
DEV_SERVICE_NAMES
end
+ def self.project_specific_services_names
+ []
+ end
+
def self.available_services_types
available_services_names.map { |service_name| "#{service_name}_service".camelize }
end
@@ -217,7 +220,7 @@ class Service < ApplicationRecord
services_names.map { |service_name| "#{service_name}_service".camelize }
end
- def self.build_from_integration(project_id, integration)
+ def self.build_from_integration(integration, project_id: nil, group_id: nil)
service = integration.dup
if integration.supports_data_fields?
@@ -227,8 +230,9 @@ class Service < ApplicationRecord
service.template = false
service.instance = false
- service.inherit_from_id = integration.id if integration.instance?
service.project_id = project_id
+ service.group_id = group_id
+ service.inherit_from_id = integration.id if integration.instance? || integration.group
service.active = false if service.invalid?
service
end
@@ -245,7 +249,7 @@ class Service < ApplicationRecord
group_ids = scope.ancestors.select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
- where(type: type, group_id: group_ids)
+ where(type: type, group_id: group_ids, inherit_from_id: nil)
.order(Arel.sql("array_position(#{array}::bigint[], services.group_id)"))
.first
end
@@ -256,6 +260,19 @@ class Service < ApplicationRecord
end
private_class_method :instance_level_integration
+ def self.create_from_active_default_integrations(scope, association, with_templates: false)
+ group_ids = scope.ancestors.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[], services.group_id), instance DESC")).group_by(&:type).each do |type, records|
+ build_from_integration(records.first, association => scope.id).save!
+ end
+ end
+
def activated?
active
end
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
index 9cbc5e68059..5eca5f2bda1 100644
--- a/app/models/service_list.rb
+++ b/app/models/service_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class ServiceList
- def initialize(batch_ids, service_hash, association)
- @batch_ids = batch_ids
+ def initialize(batch, service_hash, association)
+ @batch = batch
@service_hash = service_hash
@association = association
end
@@ -13,15 +13,15 @@ class ServiceList
private
- attr_reader :batch_ids, :service_hash, :association
+ attr_reader :batch, :service_hash, :association
def columns
- (service_hash.keys << "#{association}_id")
+ service_hash.keys << "#{association}_id"
end
def values
- batch_ids.map do |id|
- (service_hash.values << id)
+ batch.select(:id).map do |record|
+ service_hash.values << record.id
end
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 1cf3097861c..d71853e11cf 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -19,7 +19,6 @@ class Snippet < ApplicationRecord
extend ::Gitlab::Utils::Override
MAX_FILE_COUNT = 10
- MAX_SINGLE_FILE_COUNT = 1
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -175,8 +174,8 @@ class Snippet < ApplicationRecord
Snippet.find_by(id: id, project: project)
end
- def self.max_file_limit(user)
- Feature.enabled?(:snippet_multiple_files, user) ? MAX_FILE_COUNT : MAX_SINGLE_FILE_COUNT
+ def self.max_file_limit
+ MAX_FILE_COUNT
end
def initialize(attributes = {})
@@ -283,7 +282,8 @@ class Snippet < ApplicationRecord
strong_memoize(:repository_size_checker) do
::Gitlab::RepositorySizeChecker.new(
current_size_proc: -> { repository.size.megabytes },
- limit: Gitlab::CurrentSettings.snippet_size_limit
+ limit: Gitlab::CurrentSettings.snippet_size_limit,
+ namespace: nil
)
end
end
diff --git a/app/models/snippet_input_action_collection.rb b/app/models/snippet_input_action_collection.rb
index 38313e3a980..1e886e98083 100644
--- a/app/models/snippet_input_action_collection.rb
+++ b/app/models/snippet_input_action_collection.rb
@@ -8,7 +8,11 @@ class SnippetInputActionCollection
delegate :empty?, :any?, :[], to: :actions
def initialize(actions = [], allowed_actions: nil)
- @actions = actions.map { |action| SnippetInputAction.new(action.merge(allowed_actions: allowed_actions)) }
+ @actions = actions.map do |action|
+ params = action.merge(allowed_actions: allowed_actions)
+
+ SnippetInputAction.new(**params)
+ end
end
def to_commit_actions
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 2cfb201191d..fa25a6f8441 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -12,7 +12,7 @@ class SnippetRepository < ApplicationRecord
belongs_to :snippet, inverse_of: :snippet_repository
- delegate :repository, to: :snippet
+ delegate :repository, :repository_storage, to: :snippet
class << self
def find_snippet(disk_path)
diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb
index 8545296d076..6fb6f0ef713 100644
--- a/app/models/snippet_statistics.rb
+++ b/app/models/snippet_statistics.rb
@@ -34,6 +34,8 @@ class SnippetStatistics < ApplicationRecord
end
def refresh!
+ return if Gitlab::Database.read_only?
+
update_commit_count
update_repository_size
update_file_count
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 961212d0295..0ddf2c5fbcd 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -18,9 +18,9 @@ class SystemNoteMetadata < ApplicationRecord
commit description merge confidential visible label assignee cross_reference
designs_added designs_modified designs_removed designs_discussion_added
title time_tracking branch milestone discussion task moved
- opened closed merged duplicate locked unlocked outdated
+ opened closed merged duplicate locked unlocked outdated reviewer
tag due_date pinned_embed cherry_pick health_status approved unapproved
- status alert_issue_added relate unrelate new_alert_added
+ status alert_issue_added relate unrelate new_alert_added severity
].freeze
validates :note, presence: true
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 419fffcb666..9d88db27449 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -4,6 +4,12 @@ module Terraform
class State < ApplicationRecord
include UsageStatistics
include FileStoreMounter
+ include IgnorableColumns
+ # These columns are being removed since geo replication falls to the versioned state
+ # Tracking in https://gitlab.com/gitlab-org/gitlab/-/issues/258262
+ ignore_columns %i[verification_failure verification_retry_at verified_at verification_retry_count verification_checksum],
+ remove_with: '13.7',
+ remove_after: '2020-12-22'
HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32
@@ -15,6 +21,7 @@ module Terraform
has_one :latest_version, -> { ordered_by_version_desc }, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
+ scope :ordered_by_name, -> { order(:name) }
validates :project_id, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
@@ -30,11 +37,11 @@ module Terraform
end
def latest_file
- versioning_enabled ? latest_version&.file : file
- end
-
- def local?
- file_store == ObjectStorage::Store::LOCAL
+ if versioning_enabled?
+ latest_version&.file
+ else
+ latest_version&.file || file
+ end
end
def locked?
@@ -43,15 +50,56 @@ module Terraform
def update_file!(data, version:)
if versioning_enabled?
- new_version = versions.build(version: version)
- new_version.assign_attributes(created_by_user: locked_by_user, file: data)
- new_version.save!
+ create_new_version!(data: data, version: version)
+ elsif latest_version.present?
+ migrate_legacy_version!(data: data, version: version)
else
self.file = data
save!
end
end
+
+ private
+
+ ##
+ # If a Terraform state was created before versioning support was
+ # introduced, it will have a single version record whose file
+ # uses a legacy naming scheme in object storage. To update
+ # these states and versions to use the new behaviour, we must do
+ # the following when creating the next version:
+ #
+ # * Read the current, non-versioned file from the old location.
+ # * Update the :versioning_enabled flag, which determines the
+ # naming scheme
+ # * Resave the existing file with the updated name and location,
+ # using a version number one prior to the new version
+ # * Create the new version as normal
+ #
+ # This migration only needs to happen once for each state, from
+ # then on the state will behave as if it was always versioned.
+ #
+ # The code can be removed in the next major version (14.0), after
+ # which any states that haven't been migrated will need to be
+ # recreated: https://gitlab.com/gitlab-org/gitlab/-/issues/258960
+ def migrate_legacy_version!(data:, version:)
+ current_file = latest_version.file.read
+ current_version = parse_serial(current_file) || version - 1
+
+ update!(versioning_enabled: true)
+
+ reload_latest_version.update!(version: current_version, file: CarrierWaveStringFile.new(current_file))
+ create_new_version!(data: data, version: version)
+ end
+
+ def create_new_version!(data:, version:)
+ new_version = versions.build(version: version, created_by_user: locked_by_user)
+ new_version.assign_attributes(file: data)
+ new_version.save!
+ end
+
+ def parse_serial(file)
+ Gitlab::Json.parse(file)["serial"]
+ rescue JSON::ParserError
+ end
end
end
-
-Terraform::State.prepend_if_ee('EE::Terraform::State')
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index d5e315d18a1..eff44485401 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -14,5 +14,11 @@ module Terraform
mount_file_store_uploader VersionedStateUploader
delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
+
+ def local?
+ file_store == ObjectStorage::Store::LOCAL
+ end
end
end
+
+Terraform::StateVersion.prepend_if_ee('EE::Terraform::StateVersion')
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 6c8e085762d..0d893b25253 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -227,7 +227,7 @@ class Todo < ApplicationRecord
end
def self_assigned?
- assigned? && self_added?
+ self_added? && (assigned? || review_requested?)
end
private
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
index 81415eb383b..1a389081913 100644
--- a/app/models/u2f_registration.rb
+++ b/app/models/u2f_registration.rb
@@ -4,6 +4,19 @@
class U2fRegistration < ApplicationRecord
belongs_to :user
+ after_commit :schedule_webauthn_migration, on: :create
+ after_commit :update_webauthn_registration, on: :update, if: :counter_changed?
+
+ def schedule_webauthn_migration
+ BackgroundMigrationWorker.perform_async('MigrateU2fWebauthn', [id, id])
+ end
+
+ def update_webauthn_registration
+ # When we update the sign count of this registration
+ # we need to update the sign count of the corresponding webauthn registration
+ # as well if it exists already
+ WebauthnRegistration.find_by_credential_xid(webauthn_credential_xid)&.update_attribute(:counter, counter)
+ end
def self.register(user, app_id, params, challenges)
u2f = U2F::U2F.new(app_id)
@@ -40,4 +53,13 @@ class U2fRegistration < ApplicationRecord
rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error
false
end
+
+ private
+
+ def webauthn_credential_xid
+ # To find the corresponding webauthn registration, we use that
+ # the key handle of the u2f reg corresponds to the credential xid of the webauthn reg
+ # (with some base64 back and forth)
+ Base64.strict_encode64(Base64.urlsafe_decode64(key_handle))
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 0a784b30d8f..ef77e207215 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -64,14 +64,7 @@ class User < ApplicationRecord
# and should be added after Devise modules are initialized.
include AsyncDeviseEmail
- BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
- "administrator if you think this is an error."
- LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \
- "administrator if you think this is an error."
-
- MINIMUM_INACTIVE_DAYS = 180
-
- ignore_column :bio, remove_with: '13.4', remove_after: '2020-09-22'
+ MINIMUM_INACTIVE_DAYS = 90
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
@@ -134,6 +127,8 @@ class User < ApplicationRecord
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
source: :group
+ has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
+ has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
# Projects
has_many :groups_projects, through: :groups, source: :projects
@@ -172,6 +167,8 @@ class User < ApplicationRecord
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, class_name: "MergeRequest", through: :merge_request_assignees, source: :merge_request
+ has_many :bulk_imports
+
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :term_agreements
@@ -298,6 +295,7 @@ class User < ApplicationRecord
transition active: :blocked
transition deactivated: :blocked
transition ldap_blocked: :blocked
+ transition blocked_pending_approval: :blocked
end
event :ldap_block do
@@ -309,13 +307,18 @@ class User < ApplicationRecord
transition deactivated: :active
transition blocked: :active
transition ldap_blocked: :active
+ transition blocked_pending_approval: :active
+ end
+
+ event :block_pending_approval do
+ transition active: :blocked_pending_approval
end
event :deactivate do
transition active: :deactivated
end
- state :blocked, :ldap_blocked do
+ state :blocked, :ldap_blocked, :blocked_pending_approval do
def blocked?
true
end
@@ -339,6 +342,7 @@ class User < ApplicationRecord
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
+ scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) }
scope :external, -> { where(external: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :active, -> { with_state(:active).non_internal }
@@ -381,11 +385,14 @@ class User < ApplicationRecord
super && can?(:log_in)
end
+ # The messages for these keys are defined in `devise.en.yml`
def inactive_message
- if blocked?
- BLOCKED_MESSAGE
+ if blocked_pending_approval?
+ :blocked_pending_approval
+ elsif blocked?
+ :blocked
elsif internal?
- LOGIN_FORBIDDEN
+ :forbidden
else
super
end
@@ -535,6 +542,8 @@ class User < ApplicationRecord
admins
when 'blocked'
blocked
+ when 'blocked_pending_approval'
+ blocked_pending_approval
when 'two_factor_disabled'
without_two_factor
when 'two_factor_enabled'
@@ -687,6 +696,17 @@ class User < ApplicationRecord
end
end
+ def security_bot
+ email_pattern = "security-bot%s@#{Settings.gitlab.host}"
+
+ unique_internal(where(user_type: :security_bot), 'GitLab-Security-Bot', email_pattern) do |u|
+ u.bio = 'System bot that monitors detected vulnerabilities for solutions and creates merge requests with the fixes.'
+ u.name = 'GitLab Security Bot'
+ u.website_url = Gitlab::Routing.url_helpers.help_page_url('user/application_security/security_bot/index.md')
+ u.avatar = bot_avatar(image: 'security-bot.png')
+ end
+ end
+
def support_bot
email_pattern = "support%s@#{Settings.gitlab.host}"
@@ -773,7 +793,7 @@ class User < ApplicationRecord
end
def two_factor_otp_enabled?
- otp_required_for_login?
+ otp_required_for_login? || Feature.enabled?(:forti_authenticator, self)
end
def two_factor_u2f_enabled?
@@ -1676,6 +1696,8 @@ class User < ApplicationRecord
end
def terms_accepted?
+ return true if project_bot?
+
accepted_term_id.present?
end
@@ -1706,7 +1728,7 @@ class User < ApplicationRecord
end
def can_be_deactivated?
- active? && no_recent_activity?
+ active? && no_recent_activity? && !internal?
end
def last_active_at
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 0ba319aa444..e39ff8712fc 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -19,7 +19,7 @@ class UserCallout < ApplicationRecord
webhooks_moved: 13,
service_templates_deprecated: 14,
admin_integrations_moved: 15,
- web_ide_alert_dismissed: 16,
+ web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
personal_access_token_expiry: 21, # EE-only
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 1c615777018..7e7a387d3d4 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -21,7 +21,7 @@ class UserInteractedProject < ApplicationRecord
user_id: event.author_id
}
- cached_exists?(attributes) do
+ 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
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index d3b3a46bf74..c05bc80415a 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord
belongs_to :user
+ scope :with_user, -> { joins(:user) }
+ scope :gitpod_enabled, -> { where(gitpod_enabled: true) }
+
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
validates :tab_width, numericality: {
only_integer: true,
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index 71d0b1db410..a4338c4e2bd 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -1,17 +1,7 @@
# frozen_string_literal: true
# Placeholder class for model that is implemented in EE
-# It reserves '+' as a reference prefix, but the table does not exist in FOSS
class Vulnerability < ApplicationRecord
- include IgnorableColumns
-
- def self.reference_prefix
- '+'
- end
-
- def self.reference_prefix_escaped
- '&plus;'
- end
end
Vulnerability.prepend_if_ee('EE::Vulnerability')
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 9462f7401c4..e329a094319 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -4,6 +4,7 @@ class Wiki
extend ::Gitlab::Utils::Override
include HasRepository
include Gitlab::Utils::StrongMemoize
+ include GlobalID::Identification
MARKUPS = { # rubocop:disable Style/MultilineIfModifier
'Markdown' => :markdown,
@@ -28,14 +29,46 @@ class Wiki
# an operation fails.
attr_reader :error_message
- def self.for_container(container, user = nil)
- "#{container.class.name}Wiki".constantize.new(container, user)
+ # Support run_after_commit callbacks, since we don't have a DB record
+ # we delegate to the container.
+ delegate :run_after_commit, to: :container
+
+ class << self
+ attr_accessor :container_class
+
+ def for_container(container, user = nil)
+ "#{container.class.name}Wiki".constantize.new(container, user)
+ end
+
+ # This is needed to support repository lookup through Gitlab::GlRepository::Identifier
+ def find_by_id(container_id)
+ container_class.find_by_id(container_id)&.wiki
+ end
end
def initialize(container, user = nil)
+ raise ArgumentError, "user must be a User, got #{user.class}" if user && !user.is_a?(User)
+
@container = container
@user = user
- raise ArgumentError, "user must be a User, got #{user.class}" if user && !user.is_a?(User)
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && container == other.container
+ end
+
+ # This is needed in:
+ # - Storage::Hashed
+ # - Gitlab::GlRepository::RepoType#identifier_for_container
+ #
+ # We also need an `#id` to support `build_stubbed` in tests, where the
+ # value doesn't matter.
+ #
+ # NOTE: Wikis don't have a DB record, so this ID can be the same
+ # for two wikis in different containers and should not be expected to
+ # be unique. Use `to_global_id` instead if you need a unique ID.
+ def id
+ container.id
end
def path
@@ -103,10 +136,10 @@ class Wiki
limited = pages.size > limit
pages = pages.first(limit) if limited
- [WikiPage.group_by_directory(pages), limited]
+ [WikiDirectory.group_pages(pages), limited]
end
- # Finds a page within the repository based on a tile
+ # Finds a page within the repository based on a title
# or slug.
#
# title - The human readable or parameterized title of
@@ -183,7 +216,7 @@ class Wiki
override :repository
def repository
- @repository ||= Gitlab::GlRepository::WIKI.repository_for(container)
+ @repository ||= Gitlab::GlRepository::WIKI.repository_for(self)
end
def repository_storage
@@ -198,7 +231,6 @@ class Wiki
def full_path
container.full_path + '.wiki'
end
- alias_method :id, :full_path
# @deprecated use full_path when you need it for an URL route or disk_path when you want to point to the filesystem
alias_method :path_with_namespace, :full_path
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
index df2fe25b08b..3a2613e15d9 100644
--- a/app/models/wiki_directory.rb
+++ b/app/models/wiki_directory.rb
@@ -3,13 +3,46 @@
class WikiDirectory
include ActiveModel::Validations
- attr_accessor :slug, :pages
+ attr_accessor :slug, :entries
validates :slug, presence: true
- def initialize(slug, pages = [])
+ # Groups a list of wiki pages into a nested collection of WikiPage and WikiDirectory objects,
+ # preserving the order of the passed pages.
+ #
+ # Returns an array with all entries for the toplevel directory.
+ #
+ # @param [Array<WikiPage>] pages
+ # @return [Array<WikiPage, WikiDirectory>]
+ #
+ def self.group_pages(pages)
+ # Build a hash to map paths to created WikiDirectory objects,
+ # and recursively create them for each level of the path.
+ # For the toplevel directory we use '' as path, as that's what WikiPage#directory returns.
+ directories = Hash.new do |_, path|
+ directories[path] = new(path).tap do |directory|
+ if path.present?
+ parent = File.dirname(path)
+ parent = '' if parent == '.'
+ directories[parent].entries << directory
+ end
+ end
+ end
+
+ pages.each do |page|
+ directories[page.directory].entries << page
+ end
+
+ directories[''].entries
+ end
+
+ def initialize(slug, entries = [])
@slug = slug
- @pages = pages
+ @entries = entries
+ end
+
+ def title
+ WikiPage.unhyphenize(File.basename(slug))
end
# Relative path to the partial to be used when rendering collections
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index faf3d19d936..989128987d5 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -31,29 +31,6 @@ class WikiPage
alias_method :==, :eql?
- # Sorts and groups pages by directory.
- #
- # pages - an array of WikiPage objects.
- #
- # Returns an array of WikiPage and WikiDirectory objects. The entries are
- # sorted by alphabetical order (directories and pages inside each directory).
- # Pages at the root level come before everything.
- def self.group_by_directory(pages)
- return [] if pages.blank?
-
- pages.each_with_object([]) do |page, grouped_pages|
- next grouped_pages << page unless page.directory.present?
-
- directory = grouped_pages.find do |obj|
- obj.is_a?(WikiDirectory) && obj.slug == page.directory
- end
-
- next directory.pages << page if directory
-
- grouped_pages << WikiDirectory.new(page.directory, [page])
- end
- end
-
def self.unhyphenize(name)
name.gsub(/-+/, ' ')
end