summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /app/models
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
downloadgitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'app/models')
-rw-r--r--app/models/active_session.rb2
-rw-r--r--app/models/alert_management.rb7
-rw-r--r--app/models/alert_management/alert.rb48
-rw-r--r--app/models/alert_management/alert_assignee.rb11
-rw-r--r--app/models/alert_management/alert_user_mention.rb8
-rw-r--r--app/models/application_record.rb4
-rw-r--r--app/models/application_setting.rb29
-rw-r--r--app/models/application_setting_implementation.rb33
-rw-r--r--app/models/audit_event.rb3
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/blob.rb2
-rw-r--r--app/models/blob_viewer/go_mod.rb43
-rw-r--r--app/models/blob_viewer/metrics_dashboard_yml.rb44
-rw-r--r--app/models/board_group_recent_visit.rb2
-rw-r--r--app/models/board_project_recent_visit.rb2
-rw-r--r--app/models/chat_team.rb2
-rw-r--r--app/models/ci/bridge.rb3
-rw-r--r--app/models/ci/build.rb49
-rw-r--r--app/models/ci/build_dependencies.rb2
-rw-r--r--app/models/ci/build_report_result.rb45
-rw-r--r--app/models/ci/build_runner_session.rb15
-rw-r--r--app/models/ci/daily_build_group_report_result.rb2
-rw-r--r--app/models/ci/freeze_period.rb2
-rw-r--r--app/models/ci/group.rb2
-rw-r--r--app/models/ci/instance_variable.rb53
-rw-r--r--app/models/ci/job_artifact.rb29
-rw-r--r--app/models/ci/pipeline.rb50
-rw-r--r--app/models/ci/pipeline_enums.rb5
-rw-r--r--app/models/ci/processable.rb4
-rw-r--r--app/models/ci/ref.rb71
-rw-r--r--app/models/ci/runner.rb32
-rw-r--r--app/models/clusters/applications/cert_manager.rb6
-rw-r--r--app/models/clusters/applications/crossplane.rb3
-rw-r--r--app/models/clusters/applications/elastic_stack.rb12
-rw-r--r--app/models/clusters/applications/fluentd.rb3
-rw-r--r--app/models/clusters/applications/helm.rb6
-rw-r--r--app/models/clusters/applications/ingress.rb3
-rw-r--r--app/models/clusters/applications/jupyter.rb3
-rw-r--r--app/models/clusters/applications/knative.rb6
-rw-r--r--app/models/clusters/applications/prometheus.rb11
-rw-r--r--app/models/clusters/applications/runner.rb5
-rw-r--r--app/models/clusters/cluster.rb22
-rw-r--r--app/models/clusters/concerns/application_core.rb2
-rw-r--r--app/models/clusters/concerns/application_data.rb5
-rw-r--r--app/models/clusters/concerns/application_status.rb22
-rw-r--r--app/models/commit_status.rb25
-rw-r--r--app/models/concerns/cacheable_attributes.rb2
-rw-r--r--app/models/concerns/ci/contextable.rb2
-rw-r--r--app/models/concerns/each_batch.rb2
-rw-r--r--app/models/concerns/featurable.rb99
-rw-r--r--app/models/concerns/has_status.rb4
-rw-r--r--app/models/concerns/import_state/sidekiq_job_tracker.rb7
-rw-r--r--app/models/concerns/integration.rb19
-rw-r--r--app/models/concerns/issuable.rb30
-rw-r--r--app/models/concerns/limitable.rb29
-rw-r--r--app/models/concerns/mentionable.rb4
-rw-r--r--app/models/concerns/milestoneish.rb20
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/prometheus_adapter.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb2
-rw-r--r--app/models/concerns/resolvable_discussion.rb7
-rw-r--r--app/models/concerns/resolvable_note.rb4
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/timebox.rb51
-rw-r--r--app/models/concerns/token_authenticatable.rb5
-rw-r--r--app/models/concerns/update_highest_role.rb4
-rw-r--r--app/models/container_expiration_policy.rb6
-rw-r--r--app/models/container_repository.rb14
-rw-r--r--app/models/dashboard_group_milestone.rb29
-rw-r--r--app/models/dashboard_milestone.rb19
-rw-r--r--app/models/data_list.rb25
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/design_management/design.rb68
-rw-r--r--app/models/design_management/version.rb2
-rw-r--r--app/models/diff_note.rb2
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/draft_note.rb122
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/event.rb194
-rw-r--r--app/models/global_milestone.rb108
-rw-r--r--app/models/group.rb59
-rw-r--r--app/models/group_deploy_key.rb11
-rw-r--r--app/models/group_group_link.rb1
-rw-r--r--app/models/group_import_state.rb7
-rw-r--r--app/models/group_milestone.rb49
-rw-r--r--app/models/internal_id.rb24
-rw-r--r--app/models/issue.rb27
-rw-r--r--app/models/issue/metrics.rb4
-rw-r--r--app/models/iteration.rb25
-rw-r--r--app/models/jira_import_state.rb27
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/lfs_object.rb4
-rw-r--r--app/models/license_template.rb2
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/members/group_member.rb11
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb32
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/metrics/dashboard/annotation.rb13
-rw-r--r--app/models/milestone.rb49
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/note.rb25
-rw-r--r--app/models/pages_domain.rb8
-rw-r--r--app/models/pages_domain_acme_order.rb2
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb43
-rw-r--r--app/models/performance_monitoring/prometheus_metric.rb26
-rw-r--r--app/models/performance_monitoring/prometheus_panel.rb25
-rw-r--r--app/models/performance_monitoring/prometheus_panel_group.rb21
-rw-r--r--app/models/project.rb132
-rw-r--r--app/models/project_ci_cd_setting.rb13
-rw-r--r--app/models/project_feature.rb106
-rw-r--r--app/models/project_group_link.rb6
-rw-r--r--app/models/project_import_state.rb6
-rw-r--r--app/models/project_metrics_setting.rb7
-rw-r--r--app/models/project_repository_storage_move.rb22
-rw-r--r--app/models/project_services/alerts_service.rb2
-rw-r--r--app/models/project_services/chat_message/alert_message.rb74
-rw-r--r--app/models/project_services/chat_message/merge_message.rb2
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb6
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb23
-rw-r--r--app/models/project_services/pipelines_email_service.rb6
-rw-r--r--app/models/project_services/prometheus_service.rb43
-rw-r--r--app/models/project_services/slack_service.rb16
-rw-r--r--app/models/project_setting.rb4
-rw-r--r--app/models/prometheus_alert_event.rb6
-rw-r--r--app/models/push_event.rb4
-rw-r--r--app/models/releases/evidence.rb55
-rw-r--r--app/models/releases/link.rb7
-rw-r--r--app/models/remote_mirror.rb12
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/repository_language.rb2
-rw-r--r--app/models/resource_label_event.rb8
-rw-r--r--app/models/resource_milestone_event.rb10
-rw-r--r--app/models/review.rb30
-rw-r--r--app/models/route.rb2
-rw-r--r--app/models/self_managed_prometheus_alert_event.rb6
-rw-r--r--app/models/service.rb26
-rw-r--r--app/models/service_list.rb27
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/snippet_input_action.rb62
-rw-r--r--app/models/snippet_input_action_collection.rb25
-rw-r--r--app/models/ssh_host_key.rb2
-rw-r--r--app/models/storage/legacy_project.rb2
-rw-r--r--app/models/terraform/state.rb2
-rw-r--r--app/models/todo.rb18
-rw-r--r--app/models/uploads/base.rb2
-rw-r--r--app/models/user.rb61
-rw-r--r--app/models/user_interacted_project.rb14
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/web_ide_terminal.rb51
-rw-r--r--app/models/wiki.rb2
-rw-r--r--app/models/wiki_directory.rb2
-rw-r--r--app/models/wiki_page.rb8
-rw-r--r--app/models/wiki_page/meta.rb2
-rw-r--r--app/models/wiki_page/slug.rb4
159 files changed, 1958 insertions, 1130 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 065bd5507be..a23190cc8b3 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -36,7 +36,7 @@ class ActiveSession
timestamp = Time.current
active_user_session = new(
- ip_address: request.ip,
+ ip_address: request.remote_ip,
browser: client.name,
os: client.os_name,
device_name: client.device_name,
diff --git a/app/models/alert_management.rb b/app/models/alert_management.rb
new file mode 100644
index 00000000000..0346b1f155f
--- /dev/null
+++ b/app/models/alert_management.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ def self.table_name_prefix
+ 'alert_management_'
+ end
+end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index acaf474ecc2..af60ddd6f9a 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -1,10 +1,14 @@
# frozen_string_literal: true
+require_dependency 'alert_management'
+
module AlertManagement
class Alert < ApplicationRecord
+ include IidRoutes
include AtomicInternalId
include ShaAttribute
include Sortable
+ include Noteable
include Gitlab::SQL::Pattern
STATUSES = {
@@ -23,9 +27,15 @@ module AlertManagement
belongs_to :project
belongs_to :issue, optional: true
- has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
- self.table_name = 'alert_management_alerts'
+ has_many :alert_assignees, inverse_of: :alert
+ has_many :assignees, through: :alert_assignees
+
+ has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :ordered_notes, -> { fresh }, as: :noteable, class_name: 'Note'
+ has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id
+
+ has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
sha_attribute :fingerprint
@@ -102,7 +112,7 @@ module AlertManagement
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
- scope :order_events_count, -> (sort_order) { order(events: sort_order) }
+ scope :order_event_count, -> (sort_order) { order(events: sort_order) }
scope :order_severity, -> (sort_order) { order(severity: sort_order) }
scope :order_status, -> (sort_order) { order(status: sort_order) }
@@ -110,12 +120,12 @@ module AlertManagement
def self.sort_by_attribute(method)
case method.to_s
- when 'start_time_asc' then order_start_time(:asc)
- when 'start_time_desc' then order_start_time(:desc)
- when 'end_time_asc' then order_end_time(:asc)
- when 'end_time_desc' then order_end_time(:desc)
- when 'events_count_asc' then order_events_count(:asc)
- when 'events_count_desc' then order_events_count(:desc)
+ when 'started_at_asc' then order_start_time(:asc)
+ when 'started_at_desc' then order_start_time(:desc)
+ when 'ended_at_asc' then order_end_time(:asc)
+ when 'ended_at_desc' then order_end_time(:desc)
+ when 'event_count_asc' then order_event_count(:asc)
+ when 'event_count_desc' then order_event_count(:desc)
when 'severity_asc' then order_severity(:asc)
when 'severity_desc' then order_severity(:desc)
when 'status_asc' then order_status(:asc)
@@ -135,8 +145,28 @@ module AlertManagement
monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]
end
+ def register_new_event!
+ increment!(:events)
+ end
+
+ # required for todos (typically contains an identifier like issue iid)
+ # no-op; we could use iid, but we don't have a reference prefix
+ def to_reference(_from = nil, full: false)
+ ''
+ end
+
+ def execute_services
+ return unless project.has_active_services?(:alert_hooks)
+
+ project.execute_services(hook_data, :alert_hooks)
+ end
+
private
+ def hook_data
+ Gitlab::DataBuilder::Alert.build(self)
+ end
+
def hosts_length
return unless hosts
diff --git a/app/models/alert_management/alert_assignee.rb b/app/models/alert_management/alert_assignee.rb
new file mode 100644
index 00000000000..c74b2699182
--- /dev/null
+++ b/app/models/alert_management/alert_assignee.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class AlertAssignee < ApplicationRecord
+ belongs_to :alert, inverse_of: :alert_assignees
+ belongs_to :assignee, class_name: 'User', foreign_key: :user_id
+
+ validates :alert, presence: true
+ validates :assignee, presence: true, uniqueness: { scope: :alert_id }
+ end
+end
diff --git a/app/models/alert_management/alert_user_mention.rb b/app/models/alert_management/alert_user_mention.rb
new file mode 100644
index 00000000000..d36aa80ee05
--- /dev/null
+++ b/app/models/alert_management/alert_user_mention.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class AlertUserMention < UserMention
+ belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert'
+ belongs_to :note
+ end
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 0979d03f6e6..c7e4d64d3d5 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -5,6 +5,10 @@ class ApplicationRecord < ActiveRecord::Base
alias_method :reset, :reload
+ def self.without_order
+ reorder(nil)
+ end
+
def self.id_in(ids)
where(id: ids)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index b29d6731b08..425a0e05c7d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -19,6 +19,12 @@ class ApplicationSetting < ApplicationRecord
belongs_to :instance_administrators_group, class_name: "Group"
+ def self.repository_storages_weighted_attributes
+ @repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze
+ end
+
+ store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true
+
# Include here so it can override methods from
# `add_authentication_token_field`
# We don't prepend for now because otherwise we'll need to
@@ -39,6 +45,7 @@ class ApplicationSetting < ApplicationRecord
cache_markdown_field :after_sign_up_text
default_value_for :id, 1
+ default_value_for :repository_storages_weighted, {}
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
@@ -136,6 +143,10 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :max_import_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :max_pages_size,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0,
@@ -152,6 +163,7 @@ class ApplicationSetting < ApplicationRecord
validates :repository_storages, presence: true
validate :check_repository_storages
+ validate :check_repository_storages_weighted
validates :auto_devops_domain,
allow_blank: true,
@@ -271,6 +283,10 @@ class ApplicationSetting < ApplicationRecord
validates :allowed_key_types, presence: true
+ repository_storages_weighted_attributes.each do |attribute|
+ validates attribute, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
+ end
+
validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.value?(level)
@@ -301,6 +317,13 @@ class ApplicationSetting < ApplicationRecord
numericality: { greater_than: 0, less_than_or_equal_to: 10 },
if: :external_authorization_service_enabled
+ validates :spam_check_endpoint_url,
+ addressable_url: true, allow_blank: true
+
+ validates :spam_check_endpoint_url,
+ presence: true,
+ if: :spam_check_endpoint_enabled
+
validates :external_auth_client_key,
presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? }
@@ -427,6 +450,12 @@ class ApplicationSetting < ApplicationRecord
recaptcha_enabled || login_recaptcha_protection_enabled
end
+ repository_storages_weighted_attributes.each do |attribute|
+ define_method :"#{attribute}=" do |value|
+ super(value.to_i)
+ end
+ end
+
private
def parsed_grafana_url
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 221e4d5e0c6..d24136cc04a 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -86,6 +86,7 @@ module ApplicationSettingImplementation
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
+ max_import_size: 50,
mirror_available: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,
@@ -104,6 +105,7 @@ module ApplicationSettingImplementation
login_recaptcha_protection_enabled: false,
repository_checks_enabled: true,
repository_storages: ['default'],
+ repository_storages_weighted: { default: 100 },
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
@@ -115,6 +117,8 @@ module ApplicationSettingImplementation
sourcegraph_enabled: false,
sourcegraph_url: nil,
sourcegraph_public_only: true,
+ spam_check_endpoint_enabled: false,
+ spam_check_endpoint_url: nil,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
namespace_storage_size_limit: 0,
terminal_max_session_time: 0,
@@ -151,7 +155,7 @@ module ApplicationSettingImplementation
snowplow_app_id: nil,
snowplow_iglu_registry_url: nil,
custom_http_clone_url_root: nil,
- productivity_analytics_start_date: Time.now,
+ productivity_analytics_start_date: Time.current,
snippet_size_limit: 50.megabytes
}
end
@@ -260,6 +264,10 @@ module ApplicationSettingImplementation
Array(read_attribute(:repository_storages))
end
+ def repository_storages_weighted
+ read_attribute(:repository_storages_weighted)
+ end
+
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
@@ -289,10 +297,21 @@ module ApplicationSettingImplementation
performance_bar_allowed_group_id.present?
end
- # Choose one of the available repository storage options. Currently all have
- # equal weighting.
+ def normalized_repository_storage_weights
+ strong_memoize(:normalized_repository_storage_weights) do
+ weights_total = repository_storages_weighted.values.reduce(:+)
+
+ repository_storages_weighted.transform_values do |w|
+ next w if weights_total == 0
+
+ w.to_f / weights_total
+ end
+ end
+ end
+
+ # Choose one of the available repository storage options based on a normalized weighted probability.
def pick_repository_storage
- repository_storages.sample
+ normalized_repository_storage_weights.max_by { |_, weight| rand**(1.0 / weight) }.first
end
def runners_registration_token
@@ -420,6 +439,12 @@ module ApplicationSettingImplementation
invalid.empty?
end
+ def check_repository_storages_weighted
+ invalid = repository_storages_weighted.keys - Gitlab.config.repositories.storages.keys
+ errors.add(:repository_storages_weighted, "can't include: %{invalid_storages}" % { invalid_storages: invalid.join(", ") }) unless
+ invalid.empty?
+ end
+
def terms_exist
return unless enforce_terms?
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 7ff0076c3e3..3bbd2e43a51 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -2,6 +2,9 @@
class AuditEvent < ApplicationRecord
include CreatedAtFilterable
+ include IgnorableColumns
+
+ ignore_column :updated_at, remove_with: '13.3', remove_after: '2020-08-22'
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 3400d6d407d..4339d419b48 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -18,7 +18,7 @@ class Badge < ApplicationRecord
# This regex will build the new PLACEHOLDER_REGEX with the new information
PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze
- default_scope { order_created_at_asc }
+ default_scope { order_created_at_asc } # rubocop:disable Cop/DefaultScope
scope :order_created_at_asc, -> { reorder(created_at: :asc) }
diff --git a/app/models/blob.rb b/app/models/blob.rb
index c8df6c7732a..874bf58530e 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -50,6 +50,7 @@ class Blob < SimpleDelegator
BlobViewer::License,
BlobViewer::Contributing,
BlobViewer::Changelog,
+ BlobViewer::MetricsDashboardYml,
BlobViewer::CargoToml,
BlobViewer::Cartfile,
@@ -57,6 +58,7 @@ class Blob < SimpleDelegator
BlobViewer::Gemfile,
BlobViewer::Gemspec,
BlobViewer::GodepsJson,
+ BlobViewer::GoMod,
BlobViewer::PackageJson,
BlobViewer::Podfile,
BlobViewer::Podspec,
diff --git a/app/models/blob_viewer/go_mod.rb b/app/models/blob_viewer/go_mod.rb
new file mode 100644
index 00000000000..ae57e2c0526
--- /dev/null
+++ b/app/models/blob_viewer/go_mod.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module BlobViewer
+ class GoMod < DependencyManager
+ include ServerSide
+ include Gitlab::Utils::StrongMemoize
+
+ MODULE_REGEX = /
+ \A (?# beginning of file)
+ module\s+ (?# module directive)
+ (?<name>.*?) (?# module name)
+ \s*(?:\/\/.*)? (?# comment)
+ (?:\n|\z) (?# newline or end of file)
+ /x.freeze
+
+ self.file_types = %i(go_mod go_sum)
+
+ def manager_name
+ 'Go Modules'
+ end
+
+ def manager_url
+ 'https://golang.org/ref/mod'
+ end
+
+ def package_type
+ 'go'
+ end
+
+ def package_name
+ strong_memoize(:package_name) do
+ next if blob.name != 'go.mod'
+ next unless match = MODULE_REGEX.match(blob.data)
+
+ match[:name]
+ end
+ end
+
+ def package_url
+ Gitlab::Golang.package_url(package_name)
+ end
+ end
+end
diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb
new file mode 100644
index 00000000000..c05fb5d88d6
--- /dev/null
+++ b/app/models/blob_viewer/metrics_dashboard_yml.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module BlobViewer
+ class MetricsDashboardYml < Base
+ include ServerSide
+ include Gitlab::Utils::StrongMemoize
+ include Auxiliary
+
+ self.partial_name = 'metrics_dashboard_yml'
+ self.loading_partial_name = 'metrics_dashboard_yml_loading'
+ self.file_types = %i(metrics_dashboard)
+ self.binary = false
+
+ def valid?
+ errors.blank?
+ end
+
+ def errors
+ strong_memoize(:errors) do
+ prepare!
+ parse_blob_data
+ end
+ end
+
+ private
+
+ def parse_blob_data
+ yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
+
+ ::PerformanceMonitoring::PrometheusDashboard.from_json(yaml)
+ nil
+ rescue Gitlab::Config::Loader::FormatError => error
+ wrap_yml_syntax_error(error)
+ rescue ActiveModel::ValidationError => invalid
+ invalid.model.errors
+ end
+
+ def wrap_yml_syntax_error(error)
+ ::PerformanceMonitoring::PrometheusDashboard.new.errors.tap do |errors|
+ errors.add(:'YAML syntax', error.message)
+ end
+ end
+ end
+end
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index 2f1cd830791..979f0e1ab92 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -14,7 +14,7 @@ class BoardGroupRecentVisit < ApplicationRecord
def self.visited!(user, board)
visit = find_or_create_by(user: user, group: board.group, board: board)
- visit.touch if visit.updated_at < Time.now
+ visit.touch if visit.updated_at < Time.current
rescue ActiveRecord::RecordNotUnique
retry
end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 236d88e909c..509c8f97b83 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -14,7 +14,7 @@ class BoardProjectRecentVisit < ApplicationRecord
def self.visited!(user, board)
visit = find_or_create_by(user: user, project: board.project, board: board)
- visit.touch if visit.updated_at < Time.now
+ visit.touch if visit.updated_at < Time.current
rescue ActiveRecord::RecordNotUnique
retry
end
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
index 28aab279545..6e39d7e2204 100644
--- a/app/models/chat_team.rb
+++ b/app/models/chat_team.rb
@@ -12,6 +12,6 @@ class ChatTeam < ApplicationRecord
# Either the group is not found, or the user doesn't have the proper
# access on the mattermost instance. In the first case, we're done either way
# in the latter case, we can't recover by retrying, so we just log what happened
- Rails.logger.error("Mattermost team deletion failed: #{e}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("Mattermost team deletion failed: #{e}")
end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 1e92a47ab49..58c26e8c806 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -16,6 +16,9 @@ module Ci
has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
foreign_key: :source_job_id
+ has_one :sourced_pipeline, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_job_id
+ has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
+
validates :ref, presence: true
# rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7f64ea7dd97..b5e68b55f72 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -26,7 +26,8 @@ module Ci
RUNNER_FEATURES = {
upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? },
refspecs: -> (build) { build.merge_request_ref? },
- artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }
+ artifacts_exclude: -> (build) { build.supports_artifacts_exclude? },
+ release_steps: -> (build) { build.release_steps? }
}.freeze
DEFAULT_RETRIES = {
@@ -39,6 +40,7 @@ module Ci
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
+ has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
@@ -55,6 +57,7 @@ module Ci
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
+ delegate :service_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
@@ -137,8 +140,8 @@ module Ci
.includes(:metadata, :job_artifacts_metadata)
end
- scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
- scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.now) }
+ scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
+ scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
@@ -259,7 +262,7 @@ module Ci
end
before_transition any => :waiting_for_resource do |build|
- build.waiting_for_resource_at = Time.now
+ build.waiting_for_resource_at = Time.current
end
before_transition on: :enqueue_waiting_for_resource do |build|
@@ -352,7 +355,7 @@ module Ci
begin
Ci::Build.retry(build, build.user)
rescue Gitlab::Access::AccessDeniedError => ex
- Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error "Unable to auto-retry job #{build.id}: #{ex}"
end
end
end
@@ -576,7 +579,7 @@ module Ci
def environment_changed_page_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless environment_status
+ 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(','))
@@ -686,6 +689,10 @@ module Ci
job_artifacts.any?
end
+ def has_test_reports?
+ job_artifacts.test_reports.exists?
+ end
+
def has_old_trace?
old_trace.present?
end
@@ -713,7 +720,7 @@ module Ci
end
def needs_touch?
- Time.now - updated_at > 15.minutes.to_i
+ Time.current - updated_at > 15.minutes.to_i
end
def valid_token?(token)
@@ -756,13 +763,13 @@ module Ci
# and use that for `ExpireBuildInstanceArtifactsWorker`?
def erase_erasable_artifacts!
- job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll
+ job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll
end
def erase(opts = {})
return false unless erasable?
- job_artifacts.destroy_all # rubocop: disable DestroyAll
+ job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll
erase_trace!
update_erased!(opts[:erased_by])
end
@@ -776,11 +783,11 @@ module Ci
end
def artifacts_expired?
- artifacts_expire_at && artifacts_expire_at < Time.now
+ artifacts_expire_at && artifacts_expire_at < Time.current
end
def artifacts_expire_in
- artifacts_expire_at - Time.now if artifacts_expire_at
+ artifacts_expire_at - Time.current if artifacts_expire_at
end
def artifacts_expire_in=(value)
@@ -809,6 +816,7 @@ module Ci
def steps
[Gitlab::Ci::Build::Step.from_commands(self),
+ Gitlab::Ci::Build::Step.from_release(self),
Gitlab::Ci::Build::Step.from_after_script(self)].compact
end
@@ -872,6 +880,16 @@ module Ci
options&.dig(:artifacts, :reports)&.any?
end
+ def supports_artifacts_exclude?
+ options&.dig(:artifacts, :exclude)&.any? &&
+ Gitlab::Ci::Features.artifacts_exclude_enabled?
+ end
+
+ def release_steps?
+ options.dig(:release)&.any? &&
+ Gitlab::Ci::Features.release_generation_enabled?
+ end
+
def hide_secrets(trace)
return unless trace
@@ -945,11 +963,6 @@ module Ci
failure_reason: :data_integrity_failure)
end
- def supports_artifacts_exclude?
- options&.dig(:artifacts, :exclude)&.any? &&
- Gitlab::Ci::Features.artifacts_exclude_enabled?
- end
-
def degradation_threshold
var = yaml_variables.find { |v| v[:key] == DEGRADATION_THRESHOLD_VARIABLE_NAME }
var[:value]&.to_i if var
@@ -993,7 +1006,7 @@ module Ci
end
def update_erased!(user = nil)
- self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
+ self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil)
end
def unscoped_project
@@ -1026,7 +1039,7 @@ module Ci
end
def has_expiring_artifacts?
- artifacts_expire_at.present? && artifacts_expire_at > Time.now
+ artifacts_expire_at.present? && artifacts_expire_at > Time.current
end
def job_jwt_variables
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index d3ff870e36a..2fcd1708cf4 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -45,7 +45,7 @@ module Ci
end
def valid_local?
- return true if Feature.enabled?('ci_disable_validates_dependencies')
+ return true if Feature.enabled?(:ci_disable_validates_dependencies)
local.all?(&:valid_dependency?)
end
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
new file mode 100644
index 00000000000..530233ad5c0
--- /dev/null
+++ b/app/models/ci/build_report_result.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildReportResult < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ self.primary_key = :build_id
+
+ belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results
+ belongs_to :project, class_name: "Project", inverse_of: :build_report_results
+
+ validates :build, :project, presence: true
+ validates :data, json_schema: { filename: "build_report_result_data" }
+
+ store_accessor :data, :tests
+
+ def tests_name
+ tests.dig("name")
+ end
+
+ def tests_duration
+ tests.dig("duration")
+ end
+
+ def tests_success
+ tests.dig("success").to_i
+ end
+
+ def tests_failed
+ tests.dig("failed").to_i
+ end
+
+ def tests_errored
+ tests.dig("errored").to_i
+ end
+
+ def tests_skipped
+ tests.dig("skipped").to_i
+ end
+
+ def tests_total
+ [tests_success, tests_failed, tests_errored, tests_skipped].sum
+ end
+ end
+end
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index b46bbe69c7c..bc7f17f046c 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -7,6 +7,8 @@ module Ci
extend Gitlab::Ci::Model
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
+ DEFAULT_SERVICE_NAME = 'build'.freeze
+ DEFAULT_PORT_NAME = 'default_port'.freeze
self.table_name = 'ci_builds_runner_session'
@@ -23,6 +25,17 @@ module Ci
channel_specification(wss_url, TERMINAL_SUBPROTOCOL)
end
+ def service_specification(service: nil, path: nil, port: nil, subprotocols: nil)
+ return {} unless url.present?
+
+ port = port.presence || DEFAULT_PORT_NAME
+ service = service.presence || DEFAULT_SERVICE_NAME
+ url = "#{self.url}/proxy/#{service}/#{port}/#{path}"
+ subprotocols = subprotocols.presence || ::Ci::BuildRunnerSession::TERMINAL_SUBPROTOCOL
+
+ channel_specification(url, subprotocols)
+ end
+
private
def channel_specification(url, subprotocol)
@@ -37,5 +50,3 @@ module Ci
end
end
end
-
-Ci::BuildRunnerSession.prepend_if_ee('EE::Ci::BuildRunnerSession')
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index 3506b27e974..d6617b8c2eb 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -9,6 +9,8 @@ module Ci
belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
belongs_to :project
+ validates :data, json_schema: { filename: "daily_build_group_report_result_data" }
+
def self.upsert_reports(data)
upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
end
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index bf03b92259a..d215372bb45 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -5,7 +5,7 @@ module Ci
include StripAttribute
self.table_name = 'ci_freeze_periods'
- default_scope { order(created_at: :asc) }
+ default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
belongs_to :project, inverse_of: :freeze_periods
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 4b2081f2977..779c6c0396f 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -24,7 +24,7 @@ module Ci
def status
strong_memoize(:status) do
- if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
+ if ::Gitlab::Ci::Features.composite_status?(project)
Gitlab::Ci::Status::Composite
.new(@jobs)
.status
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index c674f76d229..8245729a884 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -3,8 +3,13 @@
module Ci
class InstanceVariable < ApplicationRecord
extend Gitlab::Ci::Model
+ extend Gitlab::ProcessMemoryCache::Helper
include Ci::NewHasVariable
include Ci::Maskable
+ include Limitable
+
+ self.limit_name = 'ci_instance_level_variables'
+ self.limit_scope = Limitable::GLOBAL_SCOPE
alias_attribute :secret_value, :value
@@ -12,8 +17,14 @@ module Ci
message: "(%{value}) has already been taken"
}
+ validates :encrypted_value, length: {
+ maximum: 1024,
+ too_long: 'The encrypted value of the provided variable exceeds %{count} bytes. Variables over 700 characters risk exceeding the limit.'
+ }
+
scope :unprotected, -> { where(protected: false) }
- after_commit { self.class.touch_redis_cache_timestamp }
+
+ after_commit { self.class.invalidate_memory_cache(:ci_instance_variable_data) }
class << self
def all_cached
@@ -24,10 +35,6 @@ module Ci
cached_data[:unprotected]
end
- def touch_redis_cache_timestamp(time = Time.current.to_f)
- shared_backend.write(:ci_instance_variable_changed_at, time)
- end
-
private
def cached_data
@@ -37,39 +44,13 @@ module Ci
{ all: all_records, unprotected: all_records.reject(&:protected?) }
end
end
+ end
- def fetch_memory_cache(key, &payload)
- cache = process_backend.read(key)
-
- if cache && !stale_cache?(cache)
- cache[:data]
- else
- store_cache(key, &payload)
- end
- end
-
- def stale_cache?(cache_info)
- shared_timestamp = shared_backend.read(:ci_instance_variable_changed_at)
- return true unless shared_timestamp
-
- shared_timestamp.to_f > cache_info[:cached_at].to_f
- end
-
- def store_cache(key)
- data = yield
- time = Time.current.to_f
-
- process_backend.write(key, data: data, cached_at: time)
- touch_redis_cache_timestamp(time)
- data
- end
-
- def shared_backend
- Rails.cache
- end
+ private
- def process_backend
- Gitlab::ProcessMemoryCache.cache_backend
+ def validate_plan_limit_not_exceeded
+ if Gitlab::Ci::Features.instance_level_variables_limit_enabled?
+ super
end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index d931428dccd..8aba9356949 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -5,6 +5,7 @@ module Ci
include AfterCommitQueue
include ObjectStorage::BackgroundMove
include UpdateProjectStatistics
+ include UsageStatistics
include Sortable
extend Gitlab::Ci::Model
@@ -26,6 +27,7 @@ module Ci
accessibility: 'gl-accessibility.json',
codequality: 'gl-code-quality-report.json',
sast: 'gl-sast-report.json',
+ secret_detection: 'gl-secret-detection-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json',
@@ -37,7 +39,8 @@ module Ci
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
- cluster_applications: 'gl-cluster-applications.json'
+ cluster_applications: 'gl-cluster-applications.json',
+ requirements: 'requirements.json'
}.freeze
INTERNAL_TYPES = {
@@ -62,13 +65,15 @@ module Ci
accessibility: :raw,
codequality: :raw,
sast: :raw,
+ secret_detection: :raw,
dependency_scanning: :raw,
container_scanning: :raw,
dast: :raw,
license_management: :raw,
license_scanning: :raw,
performance: :raw,
- terraform: :raw
+ terraform: :raw,
+ requirements: :raw
}.freeze
DOWNLOADABLE_TYPES = %w[
@@ -87,6 +92,8 @@ module Ci
metrics
performance
sast
+ secret_detection
+ requirements
].freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
@@ -109,6 +116,7 @@ module Ci
after_save :update_file_store, if: :saved_change_to_file?
+ scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
@@ -147,7 +155,8 @@ module Ci
where(file_type: types)
end
- scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) }
+ scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) }
+ scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
scope :locked, -> { where(locked: true) }
scope :unlocked, -> { where(locked: [false, nil]) }
@@ -176,7 +185,9 @@ module Ci
cobertura: 17,
terraform: 18, # Transformed json
accessibility: 19,
- cluster_applications: 20
+ cluster_applications: 20,
+ secret_detection: 21, ## EE-specific
+ requirements: 22 ## EE-specific
}
enum file_format: {
@@ -242,8 +253,16 @@ module Ci
super || self.file_location.nil?
end
+ def expired?
+ expire_at.present? && expire_at < Time.current
+ end
+
+ def expiring?
+ expire_at.present? && expire_at > Time.current
+ end
+
def expire_in
- expire_at - Time.now if expire_at
+ expire_at - Time.current if expire_at
end
def expire_in=(value)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5db1635f64d..497e1a4d74a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -31,6 +31,7 @@ module Ci
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
belongs_to :external_pull_request
+ belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines
has_internal_id :iid, scope: :project, presence: false, track_if: -> { !importing? }, ensure_if: -> { !importing? }, init: ->(s) do
s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
@@ -40,11 +41,15 @@ module Ci
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 :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
+ has_many :job_artifacts, through: :builds
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
has_many :deployments, through: :builds
has_many :environments, -> { distinct }, through: :deployments
+ has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
+ has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
@@ -56,7 +61,6 @@ module Ci
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
- has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
@@ -64,13 +68,6 @@ module Ci
has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline
- has_one :ref_status, ->(pipeline) {
- # We use .read_attribute to save 1 extra unneeded query to load the :project.
- unscope(:where)
- .where(project_id: pipeline.read_attribute(:project_id), ref: pipeline.ref, tag: pipeline.tag)
- # Sadly :inverse_of is not supported (yet) by Rails for composite PKs.
- }, class_name: 'Ci::Ref', inverse_of: :pipelines
-
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
@@ -163,11 +160,11 @@ module Ci
# Create a separate worker for each new operation
before_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline|
- pipeline.started_at = Time.now
+ pipeline.started_at = Time.current
end
before_transition any => [:success, :failed, :canceled] do |pipeline|
- pipeline.finished_at = Time.now
+ pipeline.finished_at = Time.current
pipeline.update_duration
end
@@ -235,12 +232,10 @@ module Ci
end
after_transition any => [:success, :failed] do |pipeline|
+ ref_status = pipeline.ci_ref&.update_status_by!(pipeline)
+
pipeline.run_after_commit do
- if Feature.enabled?(:ci_pipeline_fixed_notifications)
- PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id)
- else
- PipelineNotificationWorker.perform_async(pipeline.id)
- end
+ PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref_status)
end
end
@@ -260,6 +255,7 @@ module Ci
scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :for_ref, -> (ref) { where(ref: ref) }
scope :for_id, -> (id) { where(id: id) }
+ scope :for_iid, -> (iid) { where(iid: iid) }
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
scope :with_reports, -> (reports_scope) do
@@ -397,11 +393,11 @@ module Ci
end
def ordered_stages
- if Feature.enabled?(:ci_atomic_processing, project, default_enabled: false)
+ if ::Gitlab::Ci::Features.atomic_processing?(project)
# The `Ci::Stage` contains all up-to date data
# as atomic processing updates all data in-bulk
stages
- elsif Feature.enabled?(:ci_pipeline_persisted_stages, default_enabled: true) && complete?
+ elsif complete?
# The `Ci::Stage` contains up-to date data only for `completed` pipelines
# this is due to asynchronous processing of pipeline, and stages possibly
# not updated inline with processing of pipeline
@@ -445,7 +441,7 @@ module Ci
end
def legacy_stages
- if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
+ if ::Gitlab::Ci::Features.composite_status?(project)
legacy_stages_using_composite_status
else
legacy_stages_using_sql
@@ -798,13 +794,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a
end
+ def latest_report_builds(reports_scope = ::Ci::JobArtifact.with_reports)
+ builds.latest.with_reports(reports_scope)
+ end
+
def has_reports?(reports_scope)
- complete? && builds.latest.with_reports(reports_scope).exists?
+ complete? && latest_report_builds(reports_scope).exists?
end
def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
- builds.latest.with_reports(Ci::JobArtifact.test_reports).preload(:project).find_each do |build|
+ latest_report_builds(Ci::JobArtifact.test_reports).preload(:project).find_each do |build|
build.collect_test_reports!(test_reports)
end
end
@@ -826,7 +826,7 @@ module Ci
def coverage_reports
Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports|
- builds.latest.with_reports(Ci::JobArtifact.coverage_reports).each do |build|
+ latest_report_builds(Ci::JobArtifact.coverage_reports).each do |build|
build.collect_coverage_reports!(coverage_reports)
end
end
@@ -834,7 +834,7 @@ module Ci
def terraform_reports
::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports|
- builds.latest.with_reports(::Ci::JobArtifact.terraform_reports).each do |build|
+ latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build|
build.collect_terraform_reports!(terraform_reports)
end
end
@@ -969,6 +969,12 @@ module Ci
processables.populate_scheduling_type!
end
+ def ensure_ci_ref!
+ return unless Gitlab::Ci::Features.pipeline_fixed_notifications?
+
+ self.ci_ref = Ci::Ref.ensure_for(self)
+ end
+
private
def pipeline_data
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 7e203cb67c4..2ccd8445aa8 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -27,9 +27,11 @@ module Ci
# https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7,
chat: 8,
+ webide: 9,
merge_request_event: 10,
external_pull_request_event: 11,
- parent_pipeline: 12
+ parent_pipeline: 12,
+ ondemand_scan: 13
}
end
@@ -40,6 +42,7 @@ module Ci
unknown_source: nil,
repository_source: 1,
auto_devops_source: 2,
+ webide_source: 3,
remote_source: 4,
external_project_source: 5,
bridge_source: 6
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index cc00500662d..ac5785d9c91 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -4,12 +4,8 @@ module Ci
class Processable < ::CommitStatus
include Gitlab::Utils::StrongMemoize
- has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
-
accepts_nested_attributes_for :needs
- enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true
-
scope :preload_needs, -> { preload(:needs) }
scope :with_needs, -> (names = nil) do
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index a0782bc0444..be6062b6e6e 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -3,21 +3,62 @@
module Ci
class Ref < ApplicationRecord
extend Gitlab::Ci::Model
+ include Gitlab::OptimisticLocking
- STATUSES = %w[success failed fixed].freeze
-
- belongs_to :project
- belongs_to :last_updated_by_pipeline, foreign_key: :last_updated_by_pipeline_id, class_name: 'Ci::Pipeline'
- # ActiveRecord doesn't support composite FKs for this reason we have to do the 'unscope(:where)'
- # hack.
- has_many :pipelines, ->(ref) {
- # We use .read_attribute to save 1 extra unneeded query to load the :project.
- unscope(:where)
- .where(ref: ref.ref, project_id: ref.read_attribute(:project_id), tag: ref.tag)
- # Sadly :inverse_of is not supported (yet) by Rails for composite PKs.
- }, inverse_of: :ref_status
-
- validates :status, inclusion: { in: STATUSES }
- validates :last_updated_by_pipeline, presence: true
+ FAILING_STATUSES = %w[failed broken still_failing].freeze
+
+ belongs_to :project, inverse_of: :ci_refs
+ has_many :pipelines, class_name: 'Ci::Pipeline', foreign_key: :ci_ref_id, inverse_of: :ci_ref
+
+ state_machine :status, initial: :unknown do
+ event :succeed do
+ transition unknown: :success
+ transition fixed: :success
+ transition %i[failed broken still_failing] => :fixed
+ end
+
+ event :do_fail do
+ transition unknown: :failed
+ transition %i[failed broken] => :still_failing
+ transition %i[success fixed] => :broken
+ end
+
+ state :unknown, value: 0
+ state :success, value: 1
+ state :failed, value: 2
+ state :fixed, value: 3
+ state :broken, value: 4
+ state :still_failing, value: 5
+ end
+
+ class << self
+ def ensure_for(pipeline)
+ safe_find_or_create_by(project_id: pipeline.project_id,
+ ref_path: pipeline.source_ref_path)
+ end
+
+ def failing_state?(status_name)
+ FAILING_STATUSES.include?(status_name)
+ end
+ end
+
+ def last_finished_pipeline_id
+ Ci::Pipeline.where(ci_ref_id: self.id).finished.order(id: :desc).select(:id).take&.id
+ end
+
+ def update_status_by!(pipeline)
+ return unless Gitlab::Ci::Features.pipeline_fixed_notifications?
+
+ retry_lock(self) do
+ next unless last_finished_pipeline_id == pipeline.id
+
+ case pipeline.status
+ when 'success' then self.succeed
+ when 'failed' then self.do_fail
+ end
+
+ self.status_name
+ end
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index d4e9217ff9f..8fc273556f0 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -23,10 +23,17 @@ module Ci
project_type: 3
}
- ONLINE_CONTACT_TIMEOUT = 1.hour
+ # This `ONLINE_CONTACT_TIMEOUT` needs to be larger than
+ # `RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY`
+ #
+ ONLINE_CONTACT_TIMEOUT = 2.hours
+
+ # The `RUNNER_QUEUE_EXPIRY_TIME` indicates the longest interval that
+ # Runner request needs to be refreshed by Rails instead of being handled
+ # by Workhorse
RUNNER_QUEUE_EXPIRY_TIME = 1.hour
- # This needs to be less than `ONLINE_CONTACT_TIMEOUT`
+ # The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated
UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
@@ -81,6 +88,17 @@ module Ci
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
}
+ scope :belonging_to_group_or_project, -> (group_id, project_id) {
+ groups = ::Group.where(id: group_id)
+
+ group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
+ project_runners = joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
+
+ union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql
+
+ from("(#{union_sql}) #{table_name}")
+ }
+
scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors
@@ -145,14 +163,14 @@ module Ci
# Searches for runners matching the given query.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
#
- # query - The search query as a String
+ # query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def self.search(query)
@@ -271,9 +289,9 @@ module Ci
ensure_runner_queue_value == value if value.present?
end
- def update_cached_info(values)
+ def heartbeat(values)
values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {}
- values[:contacted_at] = Time.now
+ values[:contacted_at] = Time.current
cache_attributes(values)
@@ -309,7 +327,7 @@ module Ci
real_contacted_at = read_attribute(:contacted_at)
real_contacted_at.nil? ||
- (Time.now - real_contacted_at) >= contacted_at_max_age
+ (Time.current - real_contacted_at) >= contacted_at_max_age
end
def tag_constraints
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 1efa44c39c5..53c90fa56d5 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -38,7 +38,8 @@ module Clusters
chart: chart,
files: files.merge(cluster_issuer_file),
preinstall: pre_install_script,
- postinstall: post_install_script
+ postinstall: post_install_script,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -47,7 +48,8 @@ module Clusters
name: 'certmanager',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- postdelete: post_delete_script
+ postdelete: post_delete_script,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb
index 420e56c1742..2e5a8210b3c 100644
--- a/app/models/clusters/applications/crossplane.rb
+++ b/app/models/clusters/applications/crossplane.rb
@@ -35,7 +35,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files
+ files: files,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
index 0d029aabc3b..58ac0c1f188 100644
--- a/app/models/clusters/applications/elastic_stack.rb
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -34,7 +34,8 @@ module Clusters
repository: repository,
files: files,
preinstall: migrate_to_3_script,
- postinstall: post_install_script
+ postinstall: post_install_script,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -43,7 +44,8 @@ module Clusters
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- postdelete: post_delete_script
+ postdelete: post_delete_script,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -51,7 +53,7 @@ module Clusters
super.merge('wait-for-elasticsearch.sh': File.read("#{Rails.root}/vendor/elastic_stack/wait-for-elasticsearch.sh"))
end
- def elasticsearch_client
+ def elasticsearch_client(timeout: nil)
strong_memoize(:elasticsearch_client) do
next unless kube_client
@@ -63,6 +65,7 @@ module Clusters
# ensure TLS certs are properly verified
faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
+ faraday.options.timeout = timeout unless timeout.nil?
end
rescue Kubeclient::HttpError => error
@@ -118,7 +121,8 @@ module Clusters
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
- files: files
+ files: files,
+ local_tiller_enabled: cluster.local_tiller_enabled?
).delete_command,
Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
]
diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb
index 3fd6e870edc..1bcd39618f6 100644
--- a/app/models/clusters/applications/fluentd.rb
+++ b/app/models/clusters/applications/fluentd.rb
@@ -32,7 +32,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files
+ files: files,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 4a1bcac4bb7..226a9c26db0 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -52,7 +52,8 @@ module Clusters
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
files: files,
- rbac: cluster.platform_kubernetes_rbac?
+ rbac: cluster.platform_kubernetes_rbac?,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -60,7 +61,8 @@ module Clusters
Gitlab::Kubernetes::Helm::ResetCommand.new(
name: name,
files: files,
- rbac: cluster.platform_kubernetes_rbac?
+ rbac: cluster.platform_kubernetes_rbac?,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index dd354198910..a44450ec7a9 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -63,7 +63,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files
+ files: files,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index 056ea355de6..b737f0f962f 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -45,7 +45,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- repository: repository
+ repository: repository,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 3047da12dd9..b55fc3c45fc 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -77,7 +77,8 @@ module Clusters
chart: chart,
files: files,
repository: REPOSITORY,
- postinstall: install_knative_metrics
+ postinstall: install_knative_metrics,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -99,7 +100,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
files: files,
predelete: delete_knative_services_and_metrics,
- postdelete: delete_knative_istio_leftovers
+ postdelete: delete_knative_istio_leftovers,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 3183318690c..24bb1df6d22 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -37,7 +37,7 @@ module Clusters
end
after_transition any => :updating do |application|
- application.update(last_update_started_at: Time.now)
+ application.update(last_update_started_at: Time.current)
end
end
@@ -66,7 +66,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- postinstall: install_knative_metrics
+ postinstall: install_knative_metrics,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -76,7 +77,8 @@ module Clusters
version: version,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files_with_replaced_values(values)
+ files: files_with_replaced_values(values),
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -85,7 +87,8 @@ module Clusters
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- predelete: delete_knative_istio_metrics
+ predelete: delete_knative_istio_metrics,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index a861126908f..6d3b6c4ed8f 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.16.1'
+ VERSION = '0.17.1'
self.table_name = 'clusters_applications_runners'
@@ -36,7 +36,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- repository: repository
+ repository: repository,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 83f558af1a1..bde7a2104ba 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -36,6 +36,8 @@ module Clusters
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :deployment_clusters
has_many :deployments, inverse_of: :cluster
+ has_many :successful_deployments, -> { success }, class_name: 'Deployment'
+ has_many :environments, -> { distinct }, through: :deployments
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
@@ -125,12 +127,23 @@ module Clusters
scope :gcp_installed, -> { gcp_provided.joins(:provider_gcp).merge(Clusters::Providers::Gcp.with_status(:created)) }
scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) }
+ scope :with_enabled_modsecurity, -> { joins(:application_ingress).merge(::Clusters::Applications::Ingress.modsecurity_enabled) }
+ scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
+ scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
+ scope :preload_elasticstack, -> { preload(:application_elastic_stack) }
+ scope :preload_environments, -> { preload(:environments) }
+
scope :managed, -> { where(managed: true) }
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
scope :with_management_project, -> { where.not(management_project: nil) }
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
+ scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) }
+ scope :with_project_alert_service_data, -> (project_ids) do
+ conditions = { projects: { alerts_service: [:data] } }
+ includes(conditions).joins(conditions).where(projects: { id: project_ids })
+ end
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
return [] if clusterable.is_a?(Instance)
@@ -321,6 +334,10 @@ module Clusters
end
end
+ def local_tiller_enabled?
+ Feature.enabled?(:managed_apps_local_tiller, clusterable, default_enabled: false)
+ end
+
private
def unique_management_project_environment_scope
@@ -368,7 +385,10 @@ module Clusters
def retrieve_nodes
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes }
- cluster_nodes = result[:response].to_a
+
+ return unless result[:response]
+
+ cluster_nodes = result[:response]
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes }
nodes_metrics = result[:response].to_a
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index 297d00aa281..c1f63758906 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -15,7 +15,7 @@ module Clusters
def set_initial_status
return unless not_installable?
- self.status = status_states[:installable] if cluster&.application_helm_available? || ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
+ self.status = status_states[:installable] if cluster&.application_helm_available? || cluster&.local_tiller_enabled?
end
def can_uninstall?
diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb
index 77c606553d2..ade27e69642 100644
--- a/app/models/clusters/concerns/application_data.rb
+++ b/app/models/clusters/concerns/application_data.rb
@@ -7,7 +7,8 @@ module Clusters
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
- files: files
+ files: files,
+ local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@@ -32,7 +33,7 @@ module Clusters
private
def use_tiller_ssl?
- return false if ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
+ return false if cluster.local_tiller_enabled?
cluster.application_helm.has_ssl?
end
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 0b915126f8a..86d74ed7b1c 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -97,13 +97,21 @@ module Clusters
application.status_reason = status_reason if status_reason
end
- before_transition any => [:installed, :updated] do |application, _|
- # When installing any application we are also performing an update
- # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so
- # therefore we need to reflect that in the database.
-
- unless ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
- application.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION)
+ before_transition any => [:installed, :updated] do |application, transition|
+ unless application.cluster.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm)
+ if transition.event == :make_externally_installed
+ # If an application is externally installed
+ # We assume the helm application is externally installed too
+ helm = application.cluster.application_helm || application.cluster.build_application_helm
+
+ helm.make_externally_installed!
+ else
+ # When installing any application we are also performing an update
+ # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so
+ # therefore we need to reflect that in the database.
+
+ application.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION)
+ end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7e99f128dad..475f82f23ca 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -14,6 +14,10 @@ class CommitStatus < ApplicationRecord
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
+ has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
+
+ enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true
+
delegate :commit, to: :pipeline
delegate :sha, :short_sha, :before_sha, to: :pipeline
@@ -90,7 +94,12 @@ class CommitStatus < ApplicationRecord
end
before_save if: :status_changed?, unless: :importing? do
- if Feature.disabled?(:ci_atomic_processing, project)
+ # we mark `processed` as always changed:
+ # another process might change its value and our object
+ # will not be refreshed to pick the change
+ self.processed_will_change!
+
+ if !::Gitlab::Ci::Features.atomic_processing?(project)
self.processed = nil
elsif latest?
self.processed = false # force refresh of all dependent ones
@@ -132,15 +141,15 @@ class CommitStatus < ApplicationRecord
end
before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
- commit_status.queued_at = Time.now
+ commit_status.queued_at = Time.current
end
before_transition [:created, :preparing, :pending] => :running do |commit_status|
- commit_status.started_at = Time.now
+ commit_status.started_at = Time.current
end
before_transition any => [:success, :failed, :canceled] do |commit_status|
- commit_status.finished_at = Time.now
+ commit_status.finished_at = Time.current
end
before_transition any => :failed do |commit_status, transition|
@@ -185,8 +194,10 @@ class CommitStatus < ApplicationRecord
end
def self.update_as_processed!
- # Marks items as processed, and increases `lock_version` (Optimisitc Locking)
- update_all('processed=TRUE, lock_version=COALESCE(lock_version,0)+1')
+ # Marks items as processed
+ # we do not increase `lock_version`, as we are the one
+ # holding given lock_version (Optimisitc Locking)
+ update_all(processed: true)
end
def self.locking_enabled?
@@ -276,7 +287,7 @@ class CommitStatus < ApplicationRecord
end
def schedule_stage_and_pipeline_update
- if Feature.enabled?(:ci_atomic_processing, project)
+ if ::Gitlab::Ci::Features.atomic_processing?(project)
# Atomic Processing requires only single Worker
PipelineProcessWorker.perform_async(pipeline_id, [id])
else
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index d459af23a2f..de176ffde5c 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -55,7 +55,7 @@ module CacheableAttributes
current_without_cache.tap { |current_record| current_record&.cache! }
rescue => e
if Rails.env.production?
- Rails.logger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}")
else
raise e
end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index ccd90ea5900..7ea5382a4fa 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -18,7 +18,7 @@ module Ci
variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
- variables.concat(dependency_variables) if Feature.enabled?(:ci_dependency_variables, project)
+ variables.concat(dependency_variables)
variables.concat(secret_instance_variables)
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index 6314b46a7e3..af5f4e30d06 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -17,7 +17,7 @@ module EachBatch
# Example:
#
# User.each_batch do |relation|
- # relation.update_all(updated_at: Time.now)
+ # relation.update_all(updated_at: Time.current)
# end
#
# The supplied block is also passed an optional batch index:
diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb
new file mode 100644
index 00000000000..60aa46ce04c
--- /dev/null
+++ b/app/models/concerns/featurable.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+# == Featurable concern
+#
+# This concern adds features (tools) functionality to Project and Group
+# To enable features you need to call `set_available_features`
+#
+# Example:
+#
+# class ProjectFeature
+# include Featurable
+# set_available_features %i(wiki merge_request)
+
+module Featurable
+ extend ActiveSupport::Concern
+
+ # Can be enabled only for members, everyone or disabled
+ # Access control is made only for non private containers.
+ #
+ # Permission levels:
+ #
+ # Disabled: not enabled for anyone
+ # Private: enabled only for team members
+ # Enabled: enabled for everyone able to access the project
+ # Public: enabled for everyone (only allowed for pages)
+ DISABLED = 0
+ PRIVATE = 10
+ ENABLED = 20
+ PUBLIC = 30
+
+ STRING_OPTIONS = HashWithIndifferentAccess.new({
+ 'disabled' => DISABLED,
+ 'private' => PRIVATE,
+ 'enabled' => ENABLED,
+ 'public' => PUBLIC
+ }).freeze
+
+ class_methods do
+ def set_available_features(available_features = [])
+ @available_features = available_features
+
+ class_eval do
+ available_features.each do |feature|
+ define_method("#{feature}_enabled?") do
+ public_send("#{feature}_access_level") > DISABLED # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+ end
+
+ def available_features
+ @available_features
+ end
+
+ def access_level_attribute(feature)
+ feature = ensure_feature!(feature)
+
+ "#{feature}_access_level".to_sym
+ end
+
+ def quoted_access_level_column(feature)
+ attribute = connection.quote_column_name(access_level_attribute(feature))
+ table = connection.quote_table_name(table_name)
+
+ "#{table}.#{attribute}"
+ end
+
+ def access_level_from_str(level)
+ STRING_OPTIONS.fetch(level)
+ end
+
+ def str_from_access_level(level)
+ STRING_OPTIONS.key(level)
+ end
+
+ def ensure_feature!(feature)
+ feature = feature.model_name.plural if feature.respond_to?(:model_name)
+ feature = feature.to_sym
+ raise ArgumentError, "invalid feature: #{feature}" unless available_features.include?(feature)
+
+ feature
+ end
+ end
+
+ def access_level(feature)
+ public_send(self.class.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def feature_available?(feature, user)
+ # This feature might not be behind a feature flag at all, so default to true
+ return false unless ::Feature.enabled?(feature, user, default_enabled: true)
+
+ get_permission(user, feature)
+ end
+
+ def string_access_level(feature)
+ self.class.str_from_access_level(access_level(feature))
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index b80f8c2bbb2..c885dea862f 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -66,7 +66,7 @@ module HasStatus
# 1. By plucking all related objects,
# 2. Or executes expensive SQL query
def slow_composite_status(project:)
- if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
+ if ::Gitlab::Ci::Features.composite_status?(project)
Gitlab::Ci::Status::Composite
.new(all, with_allow_failure: columns_hash.key?('allow_failure'))
.status
@@ -160,7 +160,7 @@ module HasStatus
if started_at && finished_at
finished_at - started_at
elsif started_at
- Time.now - started_at
+ Time.current - started_at
end
end
end
diff --git a/app/models/concerns/import_state/sidekiq_job_tracker.rb b/app/models/concerns/import_state/sidekiq_job_tracker.rb
index 55f171d158d..b7d0ed0f51b 100644
--- a/app/models/concerns/import_state/sidekiq_job_tracker.rb
+++ b/app/models/concerns/import_state/sidekiq_job_tracker.rb
@@ -5,14 +5,17 @@ module ImportState
extend ActiveSupport::Concern
included do
+ scope :with_jid, -> { where.not(jid: nil) }
+ scope :without_jid, -> { where(jid: nil) }
+
# Refreshes the expiration time of the associated import job ID.
#
# This method can be used by asynchronous importers to refresh the status,
- # preventing the StuckImportJobsWorker from marking the import as failed.
+ # preventing the Gitlab::Import::StuckProjectImportJobsWorker from marking the import as failed.
def refresh_jid_expiration
return unless jid
- Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
end
def self.jid_by(project_id:, status:)
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb
new file mode 100644
index 00000000000..644a0ba1b5e
--- /dev/null
+++ b/app/models/concerns/integration.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Integration
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def with_custom_integration_for(integration, page = nil, per = nil)
+ custom_integration_project_ids = Service
+ .where(type: integration.type)
+ .where(inherit_from_id: nil)
+ .distinct # Required until https://gitlab.com/gitlab-org/gitlab/-/issues/207385
+ .page(page)
+ .per(per)
+ .pluck(:project_id)
+
+ Project.where(id: custom_integration_project_ids)
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index a1b14dca4ac..220af8ab7c7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -39,15 +39,6 @@ module Issuable
locked: 4
}.with_indifferent_access.freeze
- # This object is used to gather issuable meta data for displaying
- # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
- # lists avoiding n+1 queries and improving performance.
- IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do
- def merge_requests_count(user = nil)
- mrs_count
- end
- end
-
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true
@@ -139,7 +130,6 @@ module Issuable
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) }
- scope :any_label, -> { joins(:label_links).distinct }
scope :join_project, -> { joins(:project) }
scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) }
scope :references_project, -> { references(:project) }
@@ -185,6 +175,10 @@ module Issuable
assignees.count > 1
end
+ def supports_weight?
+ false
+ end
+
private
def description_max_length_for_new_records_is_valid
@@ -201,7 +195,7 @@ module Issuable
class_methods do
# Searches for records with a matching title.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
# query - The search query as a String
#
@@ -225,7 +219,7 @@ module Issuable
# Searches for records with a matching title or description.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
# query - The search query as a String
# matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma.
@@ -316,6 +310,14 @@ module Issuable
end
end
+ def any_label(sort = nil)
+ if sort
+ joins(:label_links).group(*grouping_columns(sort))
+ else
+ joins(:label_links).distinct
+ end
+ end
+
# Includes table keys in group by clause when sorting
# preventing errors in postgres
#
@@ -401,6 +403,10 @@ module Issuable
participants(user).include?(user)
end
+ def can_assign_epic?(user)
+ false
+ end
+
def to_hook_data(user, old_associations: {})
changes = previous_changes
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index f320f54bb82..3cb0bd85936 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -2,6 +2,7 @@
module Limitable
extend ActiveSupport::Concern
+ GLOBAL_SCOPE = :limitable_global_scope
included do
class_attribute :limit_scope
@@ -14,14 +15,34 @@ module Limitable
private
def validate_plan_limit_not_exceeded
+ if GLOBAL_SCOPE == limit_scope
+ validate_global_plan_limit_not_exceeded
+ else
+ validate_scoped_plan_limit_not_exceeded
+ end
+ end
+
+ def validate_scoped_plan_limit_not_exceeded
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
relation = self.class.where(limit_scope => scope_relation)
+ limits = scope_relation.actual_limits
- if scope_relation.actual_limits.exceeded?(limit_name, relation)
- errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
- { name: limit_name.humanize(capitalize: false), count: scope_relation.actual_limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
- end
+ check_plan_limit_not_exceeded(limits, relation)
+ end
+
+ def validate_global_plan_limit_not_exceeded
+ relation = self.class.all
+ limits = Plan.default.actual_limits
+
+ check_plan_limit_not_exceeded(limits, relation)
+ end
+
+ def check_plan_limit_not_exceeded(limits, relation)
+ return unless limits.exceeded?(limit_name, relation)
+
+ errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
+ { name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index d157404f7bc..7b4485376d4 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -259,8 +259,8 @@ module Mentionable
# 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) if self.respond_to?(:project)
- return Feature.enabled?(:store_mentioned_users_to_db, self.group) if self.respond_to?(:group)
+ 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
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index fa5a79cc12b..5f24564dc56 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -97,26 +97,6 @@ module Milestoneish
due_date && due_date.past?
end
- def group_milestone?
- false
- end
-
- def project_milestone?
- false
- end
-
- def legacy_group_milestone?
- false
- end
-
- def dashboard_milestone?
- false
- end
-
- def global_milestone?
- false
- end
-
def total_time_spent
@total_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + merge_requests.joins(:timelogs).sum(:time_spent)
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 933a0b167e2..183b902dd37 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -24,7 +24,7 @@ module Noteable
# The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
# API call)
def system_note_timestamp
- @system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @system_note_timestamp || Time.current # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
attr_writer :system_note_timestamp
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index 761a151a474..adb6a59e11c 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -44,7 +44,7 @@ module PrometheusAdapter
{
success: true,
data: data,
- last_update: Time.now.utc
+ last_update: Time.current.utc
}
rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err.message }
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 1653ecdb305..1d89a4497d9 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -50,7 +50,7 @@ module RelativePositioning
# This method takes two integer values (positions) and
# calculates the position between them. The range is huge as
# the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
- # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number
+ # when we have enough space. If distance is less than IDEAL_DISTANCE, we are calculating an average number.
def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 5d78eea7fca..5174ae05d15 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -23,7 +23,10 @@ module ResolvableDiscussion
:last_note
)
- delegate :potentially_resolvable?, to: :first_note
+ delegate :potentially_resolvable?,
+ :noteable_id,
+ :noteable_type,
+ to: :first_note
delegate :resolved_at,
:resolved_by,
@@ -79,7 +82,7 @@ module ResolvableDiscussion
return false unless current_user
return false unless resolvable?
- current_user == self.noteable.author ||
+ current_user == self.noteable.try(:author) ||
current_user.can?(:resolve_note, self.project)
end
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 2d2d5fb7168..4e8a1bb643e 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -23,7 +23,7 @@ module ResolvableNote
class_methods do
# This method must be kept in sync with `#resolve!`
def resolve!(current_user)
- unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
+ unresolved.update_all(resolved_at: Time.current, resolved_by_id: current_user.id)
end
# This method must be kept in sync with `#unresolve!`
@@ -57,7 +57,7 @@ module ResolvableNote
return false unless resolvable?
return false if resolved?
- self.resolved_at = Time.now
+ self.resolved_at = Time.current
self.resolved_by = current_user
self.resolved_by_push = resolved_by_push
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index da4f2a79895..250889fdf8b 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -67,7 +67,7 @@ module Storage
unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
- Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}")
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index d29e6a01c56..8927e42dd97 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -7,7 +7,9 @@ module Timebox
include CacheMarkdownField
include Gitlab::SQL::Pattern
include IidRoutes
+ include Referable
include StripAttribute
+ include FromUnion
TimeboxStruct = Struct.new(:title, :name, :id) do
# Ensure these models match the interface required for exporting
@@ -64,7 +66,11 @@ module Timebox
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
- where(project_id: projects).or(where(group_id: groups))
+ if Feature.enabled?(:optimized_timebox_queries, default_enabled: true)
+ from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
+ else
+ where(project_id: projects).or(where(group_id: groups))
+ end
end
scope :within_timeframe, -> (start_date, end_date) do
@@ -122,6 +128,35 @@ module Timebox
end
end
+ ##
+ # Returns the String necessary to reference a Timebox in Markdown. Group
+ # timeboxes only support name references, and do not support cross-project
+ # references.
+ #
+ # format - Symbol format to use (default: :iid, optional: :name)
+ #
+ # Examples:
+ #
+ # Milestone.first.to_reference # => "%1"
+ # Iteration.first.to_reference(format: :name) # => "*iteration:\"goal\""
+ # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1"
+ # Iteration.first.to_reference(same_namespace_project) # => "gitlab-foss*iteration:1"
+ #
+ def to_reference(from = nil, format: :name, full: false)
+ format_reference = timebox_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
+
+ if project
+ "#{project.to_reference_base(from, full: full)}#{reference}"
+ else
+ reference
+ end
+ end
+
+ def reference_link_text(from = nil)
+ self.class.reference_prefix + self.title
+ end
+
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
@@ -162,6 +197,20 @@ module Timebox
private
+ def timebox_format_reference(format = :iid)
+ raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
+
+ if group_timebox? && format == :iid
+ raise ArgumentError, _('Cannot refer to a group %{timebox_type} by an internal id!') % { timebox_type: timebox_name }
+ end
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ iid
+ end
+ end
+
# Timebox titles must be unique across project and group timeboxes
def uniqueness_of_title
if project
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 4099039dd96..a1f83884f02 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -4,6 +4,10 @@ module TokenAuthenticatable
extend ActiveSupport::Concern
class_methods do
+ def encrypted_token_authenticatable_fields
+ @encrypted_token_authenticatable_fields ||= []
+ end
+
private
def add_authentication_token_field(token_field, options = {})
@@ -12,6 +16,7 @@ module TokenAuthenticatable
end
token_authenticatable_fields.push(token_field)
+ encrypted_token_authenticatable_fields.push(token_field) if options[:encrypted]
attr_accessor :cleartext_tokens
diff --git a/app/models/concerns/update_highest_role.rb b/app/models/concerns/update_highest_role.rb
index 7efc436c6c8..6432cc794a5 100644
--- a/app/models/concerns/update_highest_role.rb
+++ b/app/models/concerns/update_highest_role.rb
@@ -29,9 +29,7 @@ module UpdateHighestRole
UpdateHighestRoleWorker.perform_in(HIGHEST_ROLE_JOB_DELAY, update_highest_role_attribute)
else
# use same logging as ExclusiveLeaseGuard
- # rubocop:disable Gitlab/RailsLogger
- Rails.logger.error('Cannot obtain an exclusive lease. There must be another instance already in execution.')
- # rubocop:enable Gitlab/RailsLogger
+ Gitlab::AppLogger.error('Cannot obtain an exclusive lease. There must be another instance already in execution.')
end
end
end
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index 76bfbabf3b3..b1dd720d908 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -13,6 +13,8 @@ class ContainerExpirationPolicy < ApplicationRecord
validates :cadence, presence: true, inclusion: { in: ->(_) { self.cadence_options.stringify_keys } }
validates :older_than, inclusion: { in: ->(_) { self.older_than_options.stringify_keys } }, allow_nil: true
validates :keep_n, inclusion: { in: ->(_) { self.keep_n_options.keys } }, allow_nil: true
+ validates :name_regex, untrusted_regexp: true, if: :enabled?
+ validates :name_regex_keep, untrusted_regexp: true, if: :enabled?
scope :active, -> { where(enabled: true) }
scope :preloaded, -> { preload(project: [:route]) }
@@ -50,4 +52,8 @@ class ContainerExpirationPolicy < ApplicationRecord
def set_next_run_at
self.next_run_at = Time.zone.now + ChronicDuration.parse(cadence).seconds
end
+
+ def disable!
+ update_attribute(:enabled, false)
+ end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 455c672cea3..b0f7edac2f3 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -16,7 +16,13 @@ class ContainerRepository < ApplicationRecord
scope :ordered, -> { order(:name) }
scope :with_api_entity_associations, -> { preload(project: [:route, { namespace: :route }]) }
scope :for_group_and_its_subgroups, ->(group) do
- where(project_id: Project.for_group_and_its_subgroups(group).with_container_registry.select(:id))
+ project_scope = Project
+ .for_group_and_its_subgroups(group)
+ .with_container_registry
+ .select(:id)
+
+ ContainerRepository
+ .joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id")
end
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
@@ -67,6 +73,12 @@ class ContainerRepository < ApplicationRecord
end
end
+ def tags_count
+ return 0 unless manifest && manifest['tags']
+
+ manifest['tags'].size
+ end
+
def blob(config)
ContainerRegistry::Blob.new(self, config)
end
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
deleted file mode 100644
index 48c09f4cd6b..00000000000
--- a/app/models/dashboard_group_milestone.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-# Dashboard Group Milestones are milestones that allow us to pull more info out for the UI that the Milestone object doesn't allow for
-class DashboardGroupMilestone < GlobalMilestone
- extend ::Gitlab::Utils::Override
-
- attr_reader :group_name
-
- def initialize(milestone)
- super
-
- @group_name = milestone.group.full_name
- end
-
- def self.build_collection(groups, params)
- milestones = Milestone.of_groups(groups.select(:id))
- .reorder_by_due_date_asc
- .order_by_name_asc
- milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
- Milestone.filter_by_state(milestones, params[:state]).map { |m| new(m) }
- end
-
- def dashboard_milestone?
- true
- end
-
- def merge_requests_enabled?
- true
- end
-end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
deleted file mode 100644
index fd59b94b737..00000000000
--- a/app/models/dashboard_milestone.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class DashboardMilestone < GlobalMilestone
- attr_reader :project_name
-
- def initialize(milestone)
- super
-
- @project_name = milestone.project.full_name
- end
-
- def project_milestone?
- true
- end
-
- def merge_requests_enabled?
- project.merge_requests_enabled?
- end
-end
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
new file mode 100644
index 00000000000..12011cb17f7
--- /dev/null
+++ b/app/models/data_list.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class DataList
+ def initialize(batch, data_fields_hash, klass)
+ @batch = batch
+ @data_fields_hash = data_fields_hash
+ @klass = klass
+ end
+
+ def to_array
+ [klass, columns, values]
+ end
+
+ private
+
+ attr_reader :batch, :data_fields_hash, :klass
+
+ def columns
+ data_fields_hash.keys << 'service_id'
+ end
+
+ def values
+ batch.map { |row| data_fields_hash.values << row['id'] }
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index ba65acff7f3..aa3e3a8f66d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -64,7 +64,7 @@ class Deployment < ApplicationRecord
end
before_transition any => [:success, :failed, :canceled] do |deployment|
- deployment.finished_at = Time.now
+ deployment.finished_at = Time.current
end
after_transition any => :success do |deployment|
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index e9b69eab7a7..0dca6333fa1 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -20,9 +20,11 @@ module DesignManagement
has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'DesignUserMention', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+
validates :project, :filename, presence: true
validates :issue, presence: true, unless: :importing?
- validates :filename, uniqueness: { scope: :issue_id }
+ validates :filename, uniqueness: { scope: :issue_id }, length: { maximum: 255 }
validate :validate_file_is_image
alias_attribute :title, :filename
@@ -126,68 +128,23 @@ module DesignManagement
# #12["filename with [] in it.jpg"]
def to_reference(from = nil, full: false)
infix = full ? '/designs' : ''
- totally_simple = %r{ \A #{self.class.simple_file_name} \z }x
- safe_name = if totally_simple.match?(filename)
- filename
- elsif filename =~ /[<>]/
- %Q{base64:#{Base64.strict_encode64(filename)}}
- else
- escaped = filename.gsub(%r{[\\"]}) { |x| "\\#{x}" }
- %Q{"#{escaped}"}
- end
+ safe_name = Sanitize.fragment(filename)
"#{issue.to_reference(from, full: full)}#{infix}[#{safe_name}]"
end
def self.reference_pattern
- @reference_pattern ||= begin
- # Filenames can be escaped with double quotes to name filenames
- # that include square brackets, or other special characters
- %r{
- #{Issue.reference_pattern}
- (\/designs)?
- \[
- (?<design> #{simple_file_name} | #{quoted_file_name} | #{base_64_encoded_name})
- \]
- }x
- end
- end
-
- def self.simple_file_name
- %r{
- (?<simple_file_name>
- ( \w | [_:,'-] | \. | \s )+
- \.
- \w+
- )
- }x
- end
-
- def self.base_64_encoded_name
- %r{
- base64:
- (?<base_64_encoded_name>
- [A-Za-z0-9+\n]+
- =?
- )
- }x
- end
-
- def self.quoted_file_name
- %r{
- "
- (?<escaped_filename>
- (\\ \\ | \\ " | [^"\\])+
- )
- "
- }x
+ # no-op: We only support link_reference_pattern parsing
end
def self.link_reference_pattern
@link_reference_pattern ||= begin
- exts = SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT
path_segment = %r{issues/#{Gitlab::Regex.issue}/designs}
- filename_pattern = %r{(?<simple_file_name>[a-z0-9_=-]+\.(#{exts.join('|')}))}i
+ ext = Regexp.new(Regexp.union(SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT).source, Regexp::IGNORECASE)
+ valid_char = %r{[^/\s]} # any char that is not a forward slash or whitespace
+ filename_pattern = %r{
+ (?<url_filename> #{valid_char}+ \. #{ext})
+ }x
super(path_segment, filename_pattern)
end
@@ -234,6 +191,11 @@ module DesignManagement
alias_method :after_note_created, :after_note_changed
alias_method :after_note_destroyed, :after_note_changed
+ # Part of the interface of objects we can create events about
+ def resource_parent
+ project
+ end
+
private
def head_version
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 6be98fe3d44..55c9084caf2 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -88,7 +88,7 @@ module DesignManagement
rows = design_actions.map { |action| action.row_attrs(version) }
- Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows)
+ Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index ff39dbb59f3..4b2e62bf761 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -136,7 +136,7 @@ class DiffNote < Note
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
- file = noteable.diffs(original_position.diff_options).diff_files.first
+ file = original_position.find_diff_file_from(noteable)
# if line is not found in persisted diffs, fallback and retrieve file from repository using gitaly
# This is required because of https://gitlab.com/gitlab-org/gitlab/issues/42676
file = nil if file&.line_for_position(original_position).nil? && importing?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index c07078c03dd..e928bb0959a 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -20,6 +20,7 @@ class Discussion
:noteable_ability_name,
:to_ability_name,
:editable?,
+ :resolved_by_id,
:system_note_with_references_visible_for?,
:resource_parent,
diff --git a/app/models/draft_note.rb b/app/models/draft_note.rb
new file mode 100644
index 00000000000..febede9beba
--- /dev/null
+++ b/app/models/draft_note.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+class DraftNote < ApplicationRecord
+ include DiffPositionableNote
+ include Gitlab::Utils::StrongMemoize
+ include Sortable
+ include ShaAttribute
+
+ PUBLISH_ATTRS = %i(noteable_id noteable_type type note).freeze
+ DIFF_ATTRS = %i(position original_position change_position commit_id).freeze
+
+ sha_attribute :commit_id
+
+ # Attribute used to store quick actions changes and users referenced.
+ attr_accessor :commands_changes
+ attr_accessor :users_referenced
+
+ # Text with quick actions filtered out
+ attr_accessor :rendered_note
+
+ attr_accessor :review
+
+ belongs_to :author, class_name: 'User'
+ belongs_to :merge_request
+
+ validates :merge_request_id, presence: true
+ validates :author_id, presence: true, uniqueness: { scope: [:merge_request_id, :discussion_id] }, if: :discussion_id?
+ validates :discussion_id, allow_nil: true, format: { with: /\A\h{40}\z/ }
+
+ scope :authored_by, ->(u) { where(author_id: u.id) }
+
+ delegate :file_path, :file_hash, :file_identifier_hash, to: :diff_file, allow_nil: true
+
+ def self.positions
+ where.not(position: nil)
+ .select(:position)
+ .map(&:position)
+ end
+
+ def project
+ merge_request.target_project
+ end
+
+ # noteable_id and noteable_type methods
+ # are used to generate discussion_id on Discussion.discussion_id
+ def noteable_id
+ merge_request_id
+ end
+
+ def noteable
+ merge_request
+ end
+
+ def noteable_type
+ "MergeRequest"
+ end
+
+ def for_commit?
+ commit_id.present?
+ end
+
+ def importing?
+ false
+ end
+
+ def resolvable?
+ false
+ end
+
+ def emoji_awardable?
+ false
+ end
+
+ def on_diff?
+ position&.complete?
+ end
+
+ def type
+ return 'DiffNote' if on_diff?
+ return 'DiscussionNote' if discussion_id.present?
+
+ 'Note'
+ end
+
+ def references
+ {
+ users: users_referenced,
+ commands: commands_changes
+ }
+ end
+
+ def line_code
+ @line_code ||= diff_file&.line_code_for_position(original_position)
+ end
+
+ def publish_params
+ attrs = PUBLISH_ATTRS.dup
+ attrs.concat(DIFF_ATTRS) if on_diff?
+ params = slice(*attrs)
+ params[:in_reply_to_discussion_id] = discussion_id if discussion_id.present?
+ params[:review_id] = review.id if review.present?
+
+ params
+ end
+
+ def self.preload_author(draft_notes)
+ ActiveRecord::Associations::Preloader.new.preload(draft_notes, { author: :status })
+ end
+
+ def diff_file
+ strong_memoize(:diff_file) do
+ file = original_position&.diff_file(project.repository)
+
+ file&.unfold_diff_lines(original_position)
+
+ file
+ end
+ end
+
+ def commit
+ @commit ||= project.commit(commit_id) if commit_id.present?
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 21044771bbb..8dae2d760f5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -339,7 +339,7 @@ class Environment < ApplicationRecord
end
def auto_stop_in
- auto_stop_at - Time.now if auto_stop_at
+ auto_stop_at - Time.current if auto_stop_at
end
def auto_stop_in=(value)
diff --git a/app/models/event.rb b/app/models/event.rb
index 12b85697690..9c0fcbb354b 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -7,36 +7,31 @@ class Event < ApplicationRecord
include DeleteWithLimit
include CreatedAtFilterable
include Gitlab::Utils::StrongMemoize
+ include UsageStatistics
- default_scope { reorder(nil) }
-
- CREATED = 1
- UPDATED = 2
- CLOSED = 3
- REOPENED = 4
- PUSHED = 5
- COMMENTED = 6
- MERGED = 7
- JOINED = 8 # User joined project
- LEFT = 9 # User left project
- DESTROYED = 10
- EXPIRED = 11 # User left project due to expiry
+ default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
ACTIONS = HashWithIndifferentAccess.new(
- created: CREATED,
- updated: UPDATED,
- closed: CLOSED,
- reopened: REOPENED,
- pushed: PUSHED,
- commented: COMMENTED,
- merged: MERGED,
- joined: JOINED,
- left: LEFT,
- destroyed: DESTROYED,
- expired: EXPIRED
+ created: 1,
+ updated: 2,
+ closed: 3,
+ reopened: 4,
+ pushed: 5,
+ commented: 6,
+ merged: 7,
+ joined: 8, # User joined project
+ left: 9, # User left project
+ destroyed: 10,
+ expired: 11, # User left project due to expiry
+ approved: 12,
+ archived: 13 # Recoverable deletion
).freeze
- WIKI_ACTIONS = [CREATED, UPDATED, DESTROYED].freeze
+ private_constant :ACTIONS
+
+ WIKI_ACTIONS = [:created, :updated, :destroyed].freeze
+
+ DESIGN_ACTIONS = [:created, :updated, :destroyed, :archived].freeze
TARGET_TYPES = HashWithIndifferentAccess.new(
issue: Issue,
@@ -46,16 +41,20 @@ class Event < ApplicationRecord
project: Project,
snippet: Snippet,
user: User,
- wiki: WikiPage::Meta
+ wiki: WikiPage::Meta,
+ design: DesignManagement::Design
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
+ enum action: ACTIONS, _suffix: true
+
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true
+ delegate :title, to: :design, prefix: true, allow_nil: true
belongs_to :author, class_name: "User"
belongs_to :project
@@ -77,16 +76,16 @@ class Event < ApplicationRecord
# Callbacks
after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push_action?
- after_create :track_user_interacted_projects
+ after_create ->(event) { UserInteractedProject.track(event) }
# Scopes
scope :recent, -> { reorder(id: :desc) }
- scope :code_push, -> { where(action: PUSHED) }
- scope :merged, -> { where(action: MERGED) }
scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') }
+ scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
# Needed to implement feature flag: can be removed when feature flag is removed
scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
+ scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
@@ -105,6 +104,13 @@ class Event < ApplicationRecord
# should ensure the ID points to a valid user.
validates :author_id, presence: true
+ validates :action_enum_value,
+ if: :design?,
+ inclusion: {
+ in: actions.values_at(*DESIGN_ACTIONS),
+ message: ->(event, _data) { "#{event.action} is not a valid design action" }
+ }
+
self.inheritance_column = 'action'
class << self
@@ -113,7 +119,7 @@ class Event < ApplicationRecord
end
def find_sti_class(action)
- if action.to_i == PUSHED
+ if actions.fetch(action, action) == actions[:pushed] # action can be integer or symbol
PushEvent
else
Event
@@ -123,19 +129,15 @@ class Event < ApplicationRecord
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
- Event::PUSHED,
- %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
- "Note", Event::COMMENTED)
+ actions[:pushed],
+ %w(MergeRequest Issue), [actions[:created], actions[:closed], actions[:merged]],
+ "Note", actions[:commented])
end
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
- def actions
- ACTIONS.keys
- end
-
def target_types
TARGET_TYPES.keys
end
@@ -148,7 +150,9 @@ class Event < ApplicationRecord
def visible_to_user?(user = nil)
return false unless capability.present?
- Ability.allowed?(user, capability, permission_object)
+ capability.all? do |rule|
+ Ability.allowed?(user, rule, permission_object)
+ end
end
def resource_parent
@@ -159,46 +163,10 @@ class Event < ApplicationRecord
target.try(:title)
end
- def created_action?
- action == CREATED
- end
-
def push_action?
false
end
- def merged_action?
- action == MERGED
- end
-
- def closed_action?
- action == CLOSED
- end
-
- def reopened_action?
- action == REOPENED
- end
-
- def joined_action?
- action == JOINED
- end
-
- def left_action?
- action == LEFT
- end
-
- def expired_action?
- action == EXPIRED
- end
-
- def destroyed_action?
- action == DESTROYED
- end
-
- def commented_action?
- action == COMMENTED
- end
-
def membership_changed?
joined_action? || left_action? || expired_action?
end
@@ -208,11 +176,11 @@ class Event < ApplicationRecord
end
def created_wiki_page?
- wiki_page? && action == CREATED
+ wiki_page? && created_action?
end
def updated_wiki_page?
- wiki_page? && action == UPDATED
+ wiki_page? && updated_action?
end
def created_target?
@@ -239,6 +207,10 @@ class Event < ApplicationRecord
target_type == 'WikiPage::Meta'
end
+ def design?
+ target_type == 'DesignManagement::Design'
+ end
+
def milestone
target if milestone?
end
@@ -247,6 +219,10 @@ class Event < ApplicationRecord
target if issue?
end
+ def design
+ target if design?
+ end
+
def merge_request
target if merge_request?
end
@@ -266,6 +242,8 @@ class Event < ApplicationRecord
def action_name
if push_action?
push_action_name
+ elsif design?
+ design_action_names[action.to_sym]
elsif closed_action?
"closed"
elsif merged_action?
@@ -386,34 +364,30 @@ class Event < ApplicationRecord
protected
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- #
- # TODO Refactor this method so we no longer need to disable the above cops
- # https://gitlab.com/gitlab-org/gitlab/-/issues/216879.
def capability
@capability ||= begin
- if push_action? || commit_note?
- :download_code
- elsif membership_changed? || created_project_action?
- :read_project
- elsif issue? || issue_note?
- :read_issue
- elsif merge_request? || merge_request_note?
- :read_merge_request
- elsif personal_snippet_note? || project_snippet_note?
- :read_snippet
- elsif milestone?
- :read_milestone
- elsif wiki_page?
- :read_wiki
- elsif design_note?
- :read_design
- end
- end
- end
- # rubocop:enable Metrics/CyclomaticComplexity
- # rubocop:enable Metrics/PerceivedComplexity
+ capabilities.flat_map do |ability, syms|
+ if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
+ [ability]
+ else
+ []
+ end
+ end
+ end
+ end
+
+ def capabilities
+ {
+ download_code: %i[push_action? commit_note?],
+ read_project: %i[membership_changed? created_project_action?],
+ read_issue: %i[issue? issue_note?],
+ read_merge_request: %i[merge_request? merge_request_note?],
+ read_snippet: %i[personal_snippet_note? project_snippet_note?],
+ read_milestone: %i[milestone?],
+ read_wiki: %i[wiki_page?],
+ read_design: %i[design_note? design?]
+ }
+ end
private
@@ -455,11 +429,17 @@ class Event < ApplicationRecord
.update_all(last_repository_updated_at: created_at)
end
- def track_user_interacted_projects
- # Note the call to .available? is due to earlier migrations
- # that would otherwise conflict with the call to .track
- # (because the table does not exist yet).
- UserInteractedProject.track(self) if UserInteractedProject.available?
+ def design_action_names
+ {
+ created: _('uploaded'),
+ updated: _('revised'),
+ destroyed: _('deleted'),
+ archived: _('archived')
+ }
+ end
+
+ def action_enum_value
+ self.class.actions[action]
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
deleted file mode 100644
index 43de7454cb7..00000000000
--- a/app/models/global_milestone.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-# frozen_string_literal: true
-# Global Milestones are milestones that can be shared across multiple projects
-class GlobalMilestone
- include Milestoneish
-
- STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
-
- attr_reader :milestone
- alias_attribute :name, :title
-
- delegate :title, :state, :due_date, :start_date, :participants, :project,
- :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
- :timebox_id, :milestoneish_id, :resource_parent, :releases, to: :milestone
-
- def to_hash
- {
- name: title,
- title: title,
- group_name: group&.full_name,
- project_name: project&.full_name
- }
- end
-
- def for_display
- @milestone
- end
-
- def self.build_collection(projects, params)
- items = Milestone.of_projects(projects)
- .reorder_by_due_date_asc
- .order_by_name_asc
- items = items.search_title(params[:search_title]) if params[:search_title].present?
-
- Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
- end
-
- # necessary for legacy milestones
- def self.build(projects, title)
- milestones = Milestone.of_projects(projects).where(title: title)
- return if milestones.blank?
-
- new(milestones.first)
- end
-
- def self.states_count(projects, group = nil)
- legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
- group_milestones_count = group_milestones_states_count(group)
-
- legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
- legacy_group_milestones_count + group_milestones_count
- end
- end
-
- def self.group_milestones_states_count(group)
- return STATE_COUNT_HASH unless group
-
- counts_by_state = Milestone.of_groups(group).count_by_state
-
- {
- opened: counts_by_state['active'] || 0,
- closed: counts_by_state['closed'] || 0,
- all: counts_by_state.values.sum
- }
- end
-
- def self.legacy_group_milestone_states_count(projects)
- return STATE_COUNT_HASH unless projects
-
- # We need to reorder(nil) on the projects, because the controller passes them in sorted.
- relation = Milestone.of_projects(projects.reorder(nil)).count_by_state
-
- {
- opened: relation['active'] || 0,
- closed: relation['closed'] || 0,
- all: relation.values.sum
- }
- end
-
- def initialize(milestone)
- @milestone = milestone
- end
-
- def active?
- state == 'active'
- end
-
- def closed?
- state == 'closed'
- end
-
- def issues
- @issues ||= Issue.of_milestones(milestone).includes(:project, :assignees, :labels)
- end
-
- def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignees, :labels)
- end
-
- def labels
- @labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
- end
-
- def global_milestone?
- true
- end
-end
-
-GlobalMilestone.include_if_ee('::EE::GlobalMilestone')
diff --git a/app/models/group.rb b/app/models/group.rb
index 04cb6b8b4da..dd7624ab420 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -325,15 +325,17 @@ class Group < Namespace
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
- if parent_id
+ if has_parent?
self_and_ancestors.reorder(nil).select(:id)
else
id
end
- GroupMember
- .active_without_invites_and_requests
- .where(source_id: source_ids)
+ group_hierarchy_members = GroupMember.active_without_invites_and_requests
+ .where(source_id: source_ids)
+
+ GroupMember.from_union([group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares])
end
def members_from_self_and_ancestors_with_effective_access_level
@@ -398,7 +400,7 @@ class Group < Namespace
.first
&.access_level
- max_member_access || max_member_access_for_user_from_shared_groups(user) || GroupMember::NO_ACCESS
+ max_member_access || GroupMember::NO_ACCESS
end
def mattermost_team_params
@@ -494,6 +496,11 @@ class Group < Namespace
# TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
end
+ def preload_shared_group_links
+ preloader = ActiveRecord::Associations::Preloader.new
+ preloader.preload(self, shared_with_group_links: [shared_with_group: :route])
+ end
+
private
def update_two_factor_requirement
@@ -524,27 +531,39 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
- def max_member_access_for_user_from_shared_groups(user)
+ def members_from_self_and_ancestor_group_shares
group_group_link_table = GroupGroupLink.arel_table
group_member_table = GroupMember.arel_table
- group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
+ source_ids =
+ if has_parent?
+ self_and_ancestors.reorder(nil).select(:id)
+ else
+ id
+ end
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
cte_alias = cte.table.alias(GroupGroupLink.table_name)
- link = GroupGroupLink
- .with(cte.to_arel)
- .select(smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
- 'group_access'))
- .from([group_member_table, cte.alias_to(group_group_link_table)])
- .where(group_member_table[:user_id].eq(user.id))
- .where(group_member_table[:requested_at].eq(nil))
- .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
- .where(group_member_table[:source_type].eq('Namespace'))
- .reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
- .first
-
- link&.group_access
+ # Instead of members.access_level, we need to maximize that access_level at
+ # the respective group_group_links.group_access.
+ member_columns = GroupMember.attribute_names.map do |column_name|
+ if column_name == 'access_level'
+ smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
+ 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ GroupMember
+ .with(cte.to_arel)
+ .select(*member_columns)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:requested_at].eq(nil))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .where(group_member_table[:source_type].eq('Namespace'))
end
def smallest_value_arel(args, column_alias)
diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb
new file mode 100644
index 00000000000..d1f1aa544cd
--- /dev/null
+++ b/app/models/group_deploy_key.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class GroupDeployKey < Key
+ self.table_name = 'group_deploy_keys'
+
+ validates :user, presence: true
+
+ def type
+ 'DeployKey'
+ end
+end
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index c233f59b1a6..fdc54ba33ab 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -14,6 +14,7 @@ class GroupGroupLink < ApplicationRecord
presence: true
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
+ scope :public_or_visible_to_user, ->(group, user) { where(shared_group: group, shared_with_group: Group.public_or_visible_to_user(user)) } # rubocop:disable Cop/GroupPublicOrVisibleToUser
def self.access_options
Gitlab::Access.options_with_owner
diff --git a/app/models/group_import_state.rb b/app/models/group_import_state.rb
index 7773b887249..d22c1ac5550 100644
--- a/app/models/group_import_state.rb
+++ b/app/models/group_import_state.rb
@@ -5,7 +5,8 @@ class GroupImportState < ApplicationRecord
belongs_to :group, inverse_of: :import_state
- validates :group, :status, :jid, presence: true
+ validates :group, :status, presence: true
+ validates :jid, presence: true, if: -> { started? || finished? }
state_machine :status, initial: :created do
state :created, value: 0
@@ -31,4 +32,8 @@ class GroupImportState < ApplicationRecord
state.update_column(:last_error, last_error) if last_error
end
end
+
+ def in_progress?
+ created? || started?
+ end
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
deleted file mode 100644
index 60e97174e50..00000000000
--- a/app/models/group_milestone.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-# Group Milestones are milestones that can be shared among many projects within the same group
-class GroupMilestone < GlobalMilestone
- attr_reader :group, :milestones
-
- def self.build_collection(group, projects, params)
- params =
- { state: params[:state], search_title: params[:search_title] }
-
- project_milestones = Milestone.of_projects(projects)
- project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present?
- child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
- grouped_milestones = child_milestones.group_by(&:title)
-
- grouped_milestones.map do |title, grouped|
- new(title, grouped, group)
- end
- end
-
- def self.build(group, projects, title)
- child_milestones = Milestone.of_projects(projects).where(title: title)
- return if child_milestones.blank?
-
- new(title, child_milestones, group)
- end
-
- def initialize(title, milestones, group)
- @milestones = milestones
- @group = group
- end
-
- def milestone
- @milestone ||= milestones.find { |m| m.description.present? } || milestones.first
- end
-
- def issues_finder_params
- { group_id: group.id }
- end
-
- def legacy_group_milestone?
- true
- end
-
- def merge_requests_enabled?
- true
- end
-end
-
-GroupMilestone.include_if_ee('::EE::GroupMilestone')
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index b6882701e23..21cf6bfa414 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -25,8 +25,6 @@ class InternalId < ApplicationRecord
validates :usage, presence: true
- REQUIRED_SCHEMA_VERSION = 20180305095250
-
# Increments #last_value and saves the record
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
@@ -63,24 +61,16 @@ class InternalId < ApplicationRecord
class << self
def track_greatest(subject, scope, usage, new_value, init)
- return new_value unless available?
-
InternalIdGenerator.new(subject, scope, usage)
.track_greatest(init, new_value)
end
def generate_next(subject, scope, usage, init)
- # Shortcut if `internal_ids` table is not available (yet)
- # This can be the case in other (unrelated) migration specs
- return (init.call(subject) || 0) + 1 unless available?
-
InternalIdGenerator.new(subject, scope, usage)
.generate(init)
end
def reset(subject, scope, usage, value)
- return false unless available?
-
InternalIdGenerator.new(subject, scope, usage)
.reset(value)
end
@@ -95,20 +85,6 @@ class InternalId < ApplicationRecord
where(filter).delete_all
end
-
- def available?
- return true unless Rails.env.test?
-
- Gitlab::SafeRequestStore.fetch(:internal_ids_available_flag) do
- ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION
- end
- end
-
- # Flushes cached information about schema
- def reset_column_information
- Gitlab::SafeRequestStore[:internal_ids_available_flag] = nil
- super
- end
end
class InternalIdGenerator
diff --git a/app/models/issue.rb b/app/models/issue.rb
index a04ac412940..5c5190f88b1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -59,6 +59,9 @@ class Issue < ApplicationRecord
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
+ has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
+ has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
+ has_many :prometheus_alerts, through: :prometheus_alert_events
accepts_nested_attributes_for :sentry_issue
@@ -86,12 +89,14 @@ class Issue < ApplicationRecord
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
+ scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
+ scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
+ scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
- scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
# 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
@@ -139,6 +144,10 @@ class Issue < ApplicationRecord
issue.closed_at = nil
issue.closed_by = nil
end
+
+ after_transition any => :closed do |issue|
+ issue.resolve_associated_alert_management_alert
+ end
end
# Alias to state machine .with_state_id method
@@ -344,10 +353,26 @@ class Issue < ApplicationRecord
previous_changes['updated_at']&.first || updated_at
end
+ def banzai_render_context(field)
+ super.merge(label_url_method: :project_issues_url)
+ end
+
def design_collection
@design_collection ||= ::DesignManagement::DesignCollection.new(self)
end
+ def resolve_associated_alert_management_alert
+ return unless alert_management_alert
+ return if alert_management_alert.resolve
+
+ Gitlab::AppLogger.warn(
+ message: 'Cannot resolve an associated Alert Management alert',
+ issue_id: id,
+ alert_id: alert_management_alert.id,
+ alert_errors: alert_management_alert.errors.messages
+ )
+ end
+
private
def ensure_metrics
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
index d4e51dcfbca..a5e1957c096 100644
--- a/app/models/issue/metrics.rb
+++ b/app/models/issue/metrics.rb
@@ -11,11 +11,11 @@ class Issue::Metrics < ApplicationRecord
def record!
if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
- self.first_associated_with_milestone_at = Time.now
+ self.first_associated_with_milestone_at = Time.current
end
if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
- self.first_added_to_board_at = Time.now
+ self.first_added_to_board_at = Time.current
end
self.save
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index 1acd08f2063..2bda0725471 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Iteration < ApplicationRecord
- include Timebox
-
self.table_name = 'sprints'
attr_accessor :skip_future_date_validation
@@ -15,9 +13,6 @@ class Iteration < ApplicationRecord
include AtomicInternalId
- has_many :issues, foreign_key: 'sprint_id'
- has_many :merge_requests, foreign_key: 'sprint_id'
-
belongs_to :project
belongs_to :group
@@ -33,6 +28,12 @@ class Iteration < ApplicationRecord
scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) }
+ 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)
+ .where('due_date is NULL or due_date >= ?', start_date)
+ end
+
state_machine :state_enum, initial: :upcoming do
event :start do
transition upcoming: :started
@@ -62,6 +63,14 @@ class Iteration < ApplicationRecord
else iterations.upcoming
end
end
+
+ def reference_prefix
+ '*iteration:'
+ end
+
+ def reference_pattern
+ nil
+ end
end
def state
@@ -72,6 +81,10 @@ class Iteration < ApplicationRecord
self.state_enum = STATE_ENUM_MAP[value]
end
+ def resource_parent
+ group || project
+ end
+
private
def start_or_due_dates_changed?
@@ -98,3 +111,5 @@ class Iteration < ApplicationRecord
end
end
end
+
+Iteration.prepend_if_ee('EE::Iteration')
diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb
index 92147794e88..2d952c552a8 100644
--- a/app/models/jira_import_state.rb
+++ b/app/models/jira_import_state.rb
@@ -7,6 +7,7 @@ class JiraImportState < ApplicationRecord
self.table_name = 'jira_imports'
+ ERROR_MESSAGE_SIZE = 1000 # 1000 characters limit
STATUSES = { initial: 0, scheduled: 1, started: 2, failed: 3, finished: 4 }.freeze
belongs_to :project
@@ -14,6 +15,7 @@ class JiraImportState < ApplicationRecord
belongs_to :label
scope :by_jira_project_key, -> (jira_project_key) { where(jira_project_key: jira_project_key) }
+ scope :with_status, ->(statuses) { where(status: statuses) }
validates :project, presence: true
validates :jira_project_key, presence: true
@@ -25,6 +27,8 @@ class JiraImportState < ApplicationRecord
message: _('Cannot have multiple Jira imports running at the same time')
}
+ before_save :ensure_error_message_size
+
alias_method :scheduled_by, :user
state_machine :status, initial: :initial do
@@ -47,7 +51,7 @@ class JiraImportState < ApplicationRecord
after_transition initial: :scheduled do |state, _|
state.run_after_commit do
job_id = Gitlab::JiraImport::Stage::StartImportWorker.perform_async(project.id)
- state.update(jid: job_id, scheduled_at: Time.now) if job_id
+ state.update(jid: job_id, scheduled_at: Time.current) if job_id
end
end
@@ -65,6 +69,13 @@ class JiraImportState < ApplicationRecord
end
end
+ after_transition any => :failed do |state, transition|
+ arguments_hash = transition.args.first
+ error_message = arguments_hash&.dig(:error_message)
+
+ state.update_column(:error_message, error_message) if error_message.present?
+ end
+
# Supress warning:
# both JiraImportState and its :status machine have defined a different default for "status".
# although both have same value but represented in 2 ways: integer(0) and symbol(:initial)
@@ -102,4 +113,18 @@ class JiraImportState < ApplicationRecord
def self.finished_imports_count
finished.sum(:imported_issues_count)
end
+
+ def mark_as_failed(error_message)
+ sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
+
+ do_fail(error_message: error_message)
+ rescue ActiveRecord::ActiveRecordError => e
+ Gitlab::AppLogger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
+ end
+
+ private
+
+ def ensure_error_message_size
+ self.error_message = error_message&.truncate(ERROR_MESSAGE_SIZE)
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 652b5e23490..910cc0d68cd 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -31,7 +31,7 @@ class Label < ApplicationRecord
validates :title, uniqueness: { scope: [:group_id, :project_id] }
validates :title, length: { maximum: 255 }
- default_scope { order(title: :asc) }
+ default_scope { order(title: :asc) } # rubocop:disable Cop/DefaultScope
scope :templates, -> { where(template: true, type: [Label.name, nil]) }
scope :with_title, ->(title) { where(title: title) }
@@ -133,7 +133,7 @@ class Label < ApplicationRecord
# Searches for labels with a matching title or description.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
# query - The search query as a String.
#
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 6a86aebae39..3761484b15d 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -44,13 +44,13 @@ class LfsObject < ApplicationRecord
file_store == LfsObjectUploader::Store::LOCAL
end
- # rubocop: disable DestroyAll
+ # rubocop: disable Cop/DestroyAll
def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil })
.destroy_all
end
- # rubocop: enable DestroyAll
+ # rubocop: enable Cop/DestroyAll
def self.calculate_oid(path)
self.hexdigest(path)
diff --git a/app/models/license_template.rb b/app/models/license_template.rb
index 73e403f98b4..bd24259984b 100644
--- a/app/models/license_template.rb
+++ b/app/models/license_template.rb
@@ -39,7 +39,7 @@ class LicenseTemplate
end
# Populate placeholders in the LicenseTemplate content
- def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s)
+ def resolve!(project_name: nil, fullname: nil, year: Time.current.year.to_s)
# Ensure the string isn't shared with any other instance of LicenseTemplate
new_content = content.dup
new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present?
diff --git a/app/models/member.rb b/app/models/member.rb
index 791073da095..f2926d32d47 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -320,7 +320,7 @@ class Member < ApplicationRecord
return false unless invite?
self.invite_token = nil
- self.invite_accepted_at = Time.now.utc
+ self.invite_accepted_at = Time.current.utc
self.user = new_user
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 431a2ccf416..9a916cd40ae 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -13,12 +13,19 @@ class GroupMember < Member
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\ANamespace\z/ }
- default_scope { where(source_type: SOURCE_TYPE) }
+ default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
- scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count }
scope :of_ldap_type, -> { where(ldap: true) }
+ scope :count_users_by_group_id, -> do
+ if Feature.enabled?(:optimized_count_users_by_group_id)
+ group(:source_id).count
+ else
+ joins(:user).group(:source_id).count
+ end
+ end
+
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index fa2e0cb8198..833b27756ab 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -9,7 +9,7 @@ class ProjectMember < Member
default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\AProject\z/ }
validates :access_level, inclusion: { in: Gitlab::Access.values }
- default_scope { where(source_type: SOURCE_TYPE) }
+ default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_namespaces, ->(groups) do
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b4d0b729454..caf7b554427 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -88,6 +88,9 @@ class MergeRequest < ApplicationRecord
has_many :deployments,
through: :deployment_merge_requests
+ has_many :draft_notes
+ has_many :reviews, inverse_of: :merge_request
+
KNOWN_MERGE_PARAMS = [
:auto_merge_strategy,
:should_remove_source_branch,
@@ -101,7 +104,7 @@ class MergeRequest < ApplicationRecord
after_create :ensure_merge_request_diff
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
- after_save :ensure_metrics, unless: :importing?
+ after_commit :ensure_metrics, on: [:create, :update], unless: :importing?
after_commit :expire_etag_cache, unless: :importing?
# When this attribute is true some MR validation is ignored
@@ -541,13 +544,21 @@ class MergeRequest < ApplicationRecord
merge_request_diffs.where.not(id: merge_request_diff.id)
end
- # Overwritten in EE
- def note_positions_for_paths(paths, _user = nil)
+ def note_positions_for_paths(paths, user = nil)
positions = notes.new_diff_notes.joins(:note_diff_file)
.where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths)
.positions
- Gitlab::Diff::PositionCollection.new(positions, diff_head_sha)
+ collection = Gitlab::Diff::PositionCollection.new(positions, diff_head_sha)
+
+ return collection unless user
+
+ positions = draft_notes
+ .authored_by(user)
+ .positions
+ .select { |pos| paths.include?(pos.file_path) }
+
+ collection.concat(positions)
end
def preloads_discussion_diff_highlighting?
@@ -866,7 +877,7 @@ class MergeRequest < ApplicationRecord
check_service = MergeRequests::MergeabilityCheckService.new(self)
- if async && Feature.enabled?(:async_merge_request_check_mergeability, project, default_enabled: true)
+ if async
check_service.async_execute
else
check_service.execute(retry_lease: false)
@@ -885,11 +896,11 @@ class MergeRequest < ApplicationRecord
end
def merge_event
- @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
+ @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :merged).last
end
def closed_event
- @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
+ @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :closed).last
end
def work_in_progress?
@@ -1158,6 +1169,7 @@ class MergeRequest < ApplicationRecord
def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds?
return false unless actual_head_pipeline
+ return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped?
actual_head_pipeline.success?
end
@@ -1302,8 +1314,6 @@ class MergeRequest < ApplicationRecord
end
def has_accessibility_reports?
- return false unless Feature.enabled?(:accessibility_report_view, project)
-
actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports)
end
@@ -1568,6 +1578,10 @@ class MergeRequest < ApplicationRecord
deployments.visible.includes(:environment).order(id: :desc).limit(10)
end
+ def banzai_render_context(field)
+ super.merge(label_url_method: :project_merge_requests_url)
+ 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 eecb10e6dbc..de97fc33f8d 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -20,7 +20,7 @@ class MergeRequestContextCommit < ApplicationRecord
# 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)
+ Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index 9dce7c53ab6..b89d1983ce3 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -12,6 +12,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args)
+ Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index f793bd3d76f..66b27aeac91 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -437,7 +437,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
update!(stored_externally: false)
end
@@ -495,7 +495,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
# Faster inserts
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
end
def build_external_diff_tempfile(rows)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 2819ea7ce1e..9f6933d0879 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -27,6 +27,6 @@ class MergeRequestDiffCommit < ApplicationRecord
)
end
- Gitlab::Database.bulk_insert(self.table_name, rows)
+ Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb
index 8166880f0c9..3383dda20c9 100644
--- a/app/models/metrics/dashboard/annotation.rb
+++ b/app/models/metrics/dashboard/annotation.rb
@@ -3,6 +3,8 @@
module Metrics
module Dashboard
class Annotation < ApplicationRecord
+ include DeleteWithLimit
+
self.table_name = 'metrics_dashboard_annotations'
belongs_to :environment, inverse_of: :metrics_dashboard_annotations
@@ -14,14 +16,25 @@ module Metrics
validates :panel_xid, length: { maximum: 255 }
validate :single_ownership
validate :orphaned_annotation
+ validate :ending_at_after_starting_at
scope :after, ->(after) { where('starting_at >= ?', after) }
scope :before, ->(before) { where('starting_at <= ?', before) }
scope :for_dashboard, ->(dashboard_path) { where(dashboard_path: dashboard_path) }
+ scope :ending_before, ->(timestamp) { where('COALESCE(ending_at, starting_at) < ?', timestamp) }
private
+ # If annotation has NULL in ending_at column that indicates, that this annotation IS TIED TO SINGLE POINT
+ # IN TIME designated by starting_at timestamp. It does NOT mean that annotation is ever going starting from
+ # stating_at timestamp
+ def ending_at_after_starting_at
+ return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at
+
+ errors.add(:ending_at, s_("Metrics::Dashboard::Annotation|can't be before starting_at time"))
+ end
+
def single_ownership
return if cluster.nil? ^ environment.nil?
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index b5e4f62792e..58adfd5f70b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -2,7 +2,6 @@
class Milestone < ApplicationRecord
include Sortable
- include Referable
include Timebox
include Milestoneish
include FromUnion
@@ -29,6 +28,7 @@ class Milestone < ApplicationRecord
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
+ scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
@@ -122,35 +122,6 @@ class Milestone < ApplicationRecord
}
end
- ##
- # Returns the String necessary to reference a Milestone in Markdown. Group
- # milestones only support name references, and do not support cross-project
- # references.
- #
- # format - Symbol format to use (default: :iid, optional: :name)
- #
- # Examples:
- #
- # Milestone.first.to_reference # => "%1"
- # Milestone.first.to_reference(format: :name) # => "%\"goal\""
- # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1"
- # Milestone.first.to_reference(same_namespace_project) # => "gitlab-foss%1"
- #
- def to_reference(from = nil, format: :name, full: false)
- format_reference = milestone_format_reference(format)
- reference = "#{self.class.reference_prefix}#{format_reference}"
-
- if project
- "#{project.to_reference_base(from, full: full)}#{reference}"
- else
- reference
- end
- end
-
- def reference_link_text(from = nil)
- self.class.reference_prefix + self.title
- end
-
def for_display
self
end
@@ -179,22 +150,12 @@ class Milestone < ApplicationRecord
end
end
- private
-
- def milestone_format_reference(format = :iid)
- raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
-
- if group_milestone? && format == :iid
- raise ArgumentError, _('Cannot refer to a group milestone by an internal id!')
- end
-
- if format == :name && !name.include?('"')
- %("#{name}")
- else
- iid
- end
+ def subgroup_milestone?
+ group_milestone? && parent.subgroup?
end
+ private
+
def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 8116f7a256f..90b4be7a674 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -100,11 +100,11 @@ class Namespace < ApplicationRecord
# Searches for namespaces matching the given query.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
- # query - The search query as a String
+ # query - The search query as a String.
#
- # Returns an ActiveRecord::Relation
+ # Returns an ActiveRecord::Relation.
def search(query)
fuzzy_search(query, [:name, :path])
end
@@ -277,7 +277,7 @@ class Namespace < ApplicationRecord
end
def has_parent?
- parent.present?
+ parent_id.present? || parent.present?
end
def root_ancestor
diff --git a/app/models/note.rb b/app/models/note.rb
index d174ba8fe83..6b6a7c50b00 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -72,6 +72,7 @@ class Note < ApplicationRecord
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
+ belongs_to :review, inverse_of: :notes
has_many :todos
@@ -273,6 +274,10 @@ class Note < ApplicationRecord
noteable_type == "Snippet"
end
+ def for_alert_mangement_alert?
+ noteable_type == 'AlertManagement::Alert'
+ end
+
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
@@ -350,8 +355,10 @@ class Note < ApplicationRecord
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
- def confidential?
- confidential || noteable.try(:confidential?)
+ def confidential?(include_noteable: false)
+ return true if confidential
+
+ include_noteable && noteable.try(:confidential?)
end
def editable?
@@ -393,7 +400,13 @@ class Note < ApplicationRecord
end
def noteable_ability_name
- for_snippet? ? 'snippet' : noteable_type.demodulize.underscore
+ if for_snippet?
+ 'snippet'
+ elsif for_alert_mangement_alert?
+ 'alert_management_alert'
+ else
+ noteable_type.demodulize.underscore
+ end
end
def can_be_discussion_note?
@@ -520,7 +533,7 @@ class Note < ApplicationRecord
end
def banzai_render_context(field)
- super.merge(noteable: noteable, system_note: system?)
+ super.merge(noteable: noteable, system_note: system?, label_url_method: noteable_label_url_method)
end
def retrieve_upload(_identifier, paths)
@@ -603,6 +616,10 @@ class Note < ApplicationRecord
errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT
end
+
+ def noteable_label_url_method
+ for_merge_request? ? :project_merge_requests_url : :project_issues_url
+ end
end
Note.prepend_if_ee('EE::Note')
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index da5e4012f05..856496f0941 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -49,11 +49,11 @@ class PagesDomain < ApplicationRecord
after_update :update_daemon, if: :saved_change_to_pages_config?
after_destroy :update_daemon
- scope :enabled, -> { where('enabled_until >= ?', Time.now ) }
+ scope :enabled, -> { where('enabled_until >= ?', Time.current ) }
scope :needs_verification, -> do
verified_at = arel_table[:verified_at]
enabled_until = arel_table[:enabled_until]
- threshold = Time.now + VERIFICATION_THRESHOLD
+ threshold = Time.current + VERIFICATION_THRESHOLD
where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
end
@@ -69,7 +69,7 @@ class PagesDomain < ApplicationRecord
from_union([user_provided, certificate_not_valid, certificate_expiring])
end
- scope :for_removal, -> { where("remove_at < ?", Time.now) }
+ scope :for_removal, -> { where("remove_at < ?", Time.current) }
scope :with_logging_info, -> { includes(project: [:namespace, :route]) }
@@ -141,7 +141,7 @@ class PagesDomain < ApplicationRecord
def expired?
return false unless x509
- current = Time.new
+ current = Time.current
current < x509.not_before || x509.not_after < current
end
diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb
index 63d7fbc8206..411456cc237 100644
--- a/app/models/pages_domain_acme_order.rb
+++ b/app/models/pages_domain_acme_order.rb
@@ -3,7 +3,7 @@
class PagesDomainAcmeOrder < ApplicationRecord
belongs_to :pages_domain
- scope :expired, -> { where("expires_at < ?", Time.now) }
+ scope :expired, -> { where("expires_at < ?", Time.current) }
validates :pages_domain, presence: true
validates :expires_at, presence: true
diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb
index 57222c61b36..b04e7e689cd 100644
--- a/app/models/performance_monitoring/prometheus_dashboard.rb
+++ b/app/models/performance_monitoring/prometheus_dashboard.rb
@@ -4,30 +4,38 @@ module PerformanceMonitoring
class PrometheusDashboard
include ActiveModel::Model
- attr_accessor :dashboard, :panel_groups, :path, :environment, :priority, :templating
+ attr_accessor :dashboard, :panel_groups, :path, :environment, :priority, :templating, :links
validates :dashboard, presence: true
validates :panel_groups, presence: true
class << self
def from_json(json_content)
- dashboard = new(
- dashboard: json_content['dashboard'],
- panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) }
- )
-
- dashboard.tap(&:validate!)
+ build_from_hash(json_content).tap(&:validate!)
end
def find_for(project:, user:, path:, options: {})
- dashboard_response = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
- return unless dashboard_response[:status] == :success
+ template = { path: path, environment: options[:environment] }
+ rsp = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
+
+ case rsp[:http_status] || rsp[:status]
+ when :success
+ new(template.merge(rsp[:dashboard] || {})) # when there is empty dashboard file returned rsp is still a success
+ when :unprocessable_entity
+ new(template) # validation error
+ else
+ nil # any other error
+ end
+ end
+
+ private
+
+ def build_from_hash(attributes)
+ return new unless attributes.is_a?(Hash)
new(
- {
- path: path,
- environment: options[:environment]
- }.merge(dashboard_response[:dashboard])
+ dashboard: attributes['dashboard'],
+ panel_groups: attributes['panel_groups']&.map { |group| PrometheusPanelGroup.from_json(group) }
)
end
end
@@ -36,6 +44,15 @@ module PerformanceMonitoring
self.as_json(only: yaml_valid_attributes).to_yaml
end
+ # This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
+ # implementation. For new existing logic was reused to faster deliver MVC
+ def schema_validation_warnings
+ self.class.from_json(self.as_json)
+ nil
+ rescue ActiveModel::ValidationError => exception
+ exception.model.errors.map { |attr, error| "#{attr}: #{error}" }
+ end
+
private
def yaml_valid_attributes
diff --git a/app/models/performance_monitoring/prometheus_metric.rb b/app/models/performance_monitoring/prometheus_metric.rb
index 7b8bef906fa..d67b1809d93 100644
--- a/app/models/performance_monitoring/prometheus_metric.rb
+++ b/app/models/performance_monitoring/prometheus_metric.rb
@@ -10,16 +10,24 @@ module PerformanceMonitoring
validates :query, presence: true, unless: :query_range
validates :query_range, presence: true, unless: :query
- def self.from_json(json_content)
- metric = PrometheusMetric.new(
- id: json_content['id'],
- unit: json_content['unit'],
- label: json_content['label'],
- query: json_content['query'],
- query_range: json_content['query_range']
- )
+ class << self
+ def from_json(json_content)
+ build_from_hash(json_content).tap(&:validate!)
+ end
- metric.tap(&:validate!)
+ private
+
+ def build_from_hash(attributes)
+ return new unless attributes.is_a?(Hash)
+
+ new(
+ id: attributes['id'],
+ unit: attributes['unit'],
+ label: attributes['label'],
+ query: attributes['query'],
+ query_range: attributes['query_range']
+ )
+ end
end
end
end
diff --git a/app/models/performance_monitoring/prometheus_panel.rb b/app/models/performance_monitoring/prometheus_panel.rb
index 3fe029abda0..a16a68ba832 100644
--- a/app/models/performance_monitoring/prometheus_panel.rb
+++ b/app/models/performance_monitoring/prometheus_panel.rb
@@ -8,17 +8,24 @@ module PerformanceMonitoring
validates :title, presence: true
validates :metrics, presence: true
+ class << self
+ def from_json(json_content)
+ build_from_hash(json_content).tap(&:validate!)
+ end
- def self.from_json(json_content)
- panel = new(
- type: json_content['type'],
- title: json_content['title'],
- y_label: json_content['y_label'],
- weight: json_content['weight'],
- metrics: json_content['metrics'].map { |metric| PrometheusMetric.from_json(metric) }
- )
+ private
- panel.tap(&:validate!)
+ def build_from_hash(attributes)
+ return new unless attributes.is_a?(Hash)
+
+ new(
+ type: attributes['type'],
+ title: attributes['title'],
+ y_label: attributes['y_label'],
+ weight: attributes['weight'],
+ metrics: attributes['metrics']&.map { |metric| PrometheusMetric.from_json(metric) }
+ )
+ end
end
def id(group_title)
diff --git a/app/models/performance_monitoring/prometheus_panel_group.rb b/app/models/performance_monitoring/prometheus_panel_group.rb
index e672545fce3..f88106f259b 100644
--- a/app/models/performance_monitoring/prometheus_panel_group.rb
+++ b/app/models/performance_monitoring/prometheus_panel_group.rb
@@ -8,15 +8,22 @@ module PerformanceMonitoring
validates :group, presence: true
validates :panels, presence: true
+ class << self
+ def from_json(json_content)
+ build_from_hash(json_content).tap(&:validate!)
+ end
- def self.from_json(json_content)
- panel_group = new(
- group: json_content['group'],
- priority: json_content['priority'],
- panels: json_content['panels'].map { |panel| PrometheusPanel.from_json(panel) }
- )
+ private
- panel_group.tap(&:validate!)
+ def build_from_hash(attributes)
+ return new unless attributes.is_a?(Hash)
+
+ new(
+ group: attributes['group'],
+ priority: attributes['priority'],
+ panels: attributes['panels']&.map { |panel| PrometheusPanel.from_json(panel) }
+ )
+ end
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c0dd2eb8584..845e9e83e78 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -33,6 +33,7 @@ class Project < ApplicationRecord
include OptionallySearch
include FromUnion
include IgnorableColumns
+ include Integration
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -95,8 +96,7 @@ class Project < ApplicationRecord
after_create :create_project_feature, unless: :project_feature
after_create :create_ci_cd_settings,
- unless: :ci_cd_settings,
- if: proc { ProjectCiCdSetting.available? }
+ unless: :ci_cd_settings
after_create :create_container_expiration_policy,
unless: :container_expiration_policy
@@ -198,7 +198,7 @@ class Project < ApplicationRecord
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
has_one :grafana_integration, inverse_of: :project
- has_one :project_setting, ->(project) { where_or_create_by(project: project) }, inverse_of: :project
+ has_one :project_setting, inverse_of: :project, autosave: true
has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
# Merge Requests for target project should be removed with it
@@ -282,7 +282,7 @@ class Project < ApplicationRecord
class_name: 'Ci::Pipeline',
inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
- has_many :ci_refs, class_name: 'Ci::Ref'
+ has_many :ci_refs, class_name: 'Ci::Ref', inverse_of: :project
# Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
@@ -291,6 +291,7 @@ class Project < ApplicationRecord
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
+ has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
has_many :job_artifacts, class_name: 'Ci::JobArtifact'
has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
@@ -328,6 +329,9 @@ class Project < ApplicationRecord
has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove'
+ has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
+ has_many :reviews, inverse_of: :project
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -368,9 +372,11 @@ class Project < ApplicationRecord
delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
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 :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=, to: :project_setting
# Validations
validates :creator, presence: true, on: :create
@@ -442,7 +448,7 @@ class Project < ApplicationRecord
scope :archived, -> { where(archived: true) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
- scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
+ scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :inc_routes, -> { includes(:route, namespace: :route) }
scope :with_statistics, -> { includes(:statistics) }
@@ -507,6 +513,11 @@ class Project < ApplicationRecord
.where(project_pages_metadata: { project_id: nil })
end
+ scope :with_api_entity_associations, -> {
+ preload(:project_feature, :route, :tags,
+ group: :ip_restrictions, namespace: [:route, :owner])
+ }
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@@ -521,6 +532,10 @@ class Project < ApplicationRecord
# Used by Projects::CleanupService to hold a map of rewritten object IDs
mount_uploader :bfg_object_map, AttachmentUploader
+ def self.with_web_entity_associations
+ preload(:project_feature, :route, :creator, :group, namespace: [:route, :owner])
+ end
+
def self.eager_load_namespace_and_owner
includes(namespace: :owner)
end
@@ -602,8 +617,7 @@ class Project < ApplicationRecord
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
- # search. On MySQL a regular "LIKE" is used as it's already
- # case-insensitive.
+ # search.
#
# query - The search query as a String.
def search(query, include_namespace: false)
@@ -713,6 +727,10 @@ class Project < ApplicationRecord
super
end
+ def project_setting
+ super.presence || build_project_setting
+ end
+
def all_pipelines
if builds_enabled?
super
@@ -729,6 +747,10 @@ class Project < ApplicationRecord
end
end
+ def active_webide_pipelines(user:)
+ webide_pipelines.running_or_pending.for_user(user)
+ end
+
def autoclose_referenced_issues
return true if super.nil?
@@ -798,10 +820,6 @@ class Project < ApplicationRecord
Feature.enabled?(:context_commits, default_enabled: true)
end
- def jira_issues_import_feature_flag_enabled?
- Feature.enabled?(:jira_issue_import, self, default_enabled: true)
- end
-
# LFS and hashed repository storage are required for using Design Management.
def design_management_enabled?
lfs_enabled? && hashed_storage?(:repository)
@@ -889,18 +907,6 @@ class Project < ApplicationRecord
latest_jira_import&.status || 'initial'
end
- def validate_jira_import_settings!(user: nil)
- raise Projects::ImportService::Error, _('Jira import feature is disabled.') unless jira_issues_import_feature_flag_enabled?
- raise Projects::ImportService::Error, _('Jira integration not configured.') unless jira_service&.active?
-
- if user
- raise Projects::ImportService::Error, _('Cannot import because issues are not available in this project.') unless feature_available?(:issues, user)
- raise Projects::ImportService::Error, _('You do not have permissions to run the import.') unless user.can?(:admin_project, self)
- end
-
- raise Projects::ImportService::Error, _('Unable to connect to the Jira instance. Please check your Jira integration configuration.') unless jira_service.test(nil)[:success]
- end
-
def human_import_status_name
import_state&.human_status_name || 'none'
end
@@ -921,17 +927,15 @@ class Project < ApplicationRecord
job_id
end
- # rubocop:disable Gitlab/RailsLogger
def log_import_activity(job_id, type: :import)
job_type = type.to_s.capitalize
if job_id
- Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
+ Gitlab::AppLogger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
else
- Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
+ Gitlab::AppLogger.error("#{job_type} job failed to create for #{full_path}.")
end
end
- # rubocop:enable Gitlab/RailsLogger
def reset_cache_and_import_attrs
run_after_commit do
@@ -1007,7 +1011,7 @@ class Project < ApplicationRecord
end
def jira_import?
- import_type == 'jira' && latest_jira_import.present? && jira_issues_import_feature_flag_enabled?
+ import_type == 'jira' && latest_jira_import.present?
end
def gitlab_project_import?
@@ -1036,7 +1040,7 @@ class Project < ApplicationRecord
remote_mirrors.stuck.update_all(
update_status: :failed,
last_error: _('The remote mirror took to long to complete.'),
- last_update_at: Time.now
+ last_update_at: Time.current
)
end
@@ -1194,14 +1198,6 @@ class Project < ApplicationRecord
get_issue(issue_id)
end
- def default_issue_tracker
- gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
- end
-
- def issues_tracker
- external_issue_tracker || default_issue_tracker
- end
-
def external_issue_reference_pattern
external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
end
@@ -1257,7 +1253,7 @@ class Project < ApplicationRecord
available_services_names.map do |service_name|
find_or_initialize_service(service_name)
- end
+ end.sort_by(&:title)
end
def disabled_services
@@ -1267,16 +1263,7 @@ class Project < ApplicationRecord
def find_or_initialize_service(name)
return if disabled_services.include?(name)
- service = find_service(services, name)
- return service if service
-
- template = find_service(services_templates, name)
-
- if template
- Service.build_from_template(id, template)
- else
- public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
- end
+ find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
end
# rubocop: disable CodeReuse/ServiceClass
@@ -1781,17 +1768,15 @@ class Project < ApplicationRecord
ensure_pages_metadatum.update!(deployed: false)
end
- # rubocop:disable Gitlab/RailsLogger
def write_repository_config(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
repository.raw_repository.write_config(full_path: gl_full_path)
rescue Gitlab::Git::Repository::NoRepository => e
- Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
+ Gitlab::AppLogger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
end
- # rubocop:enable Gitlab/RailsLogger
def after_import
repository.expire_content_cache
@@ -1834,17 +1819,15 @@ class Project < ApplicationRecord
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
- # rubocop:disable Gitlab/RailsLogger
def add_export_job(current_user:, after_export_strategy: nil, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id
- Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
+ Gitlab::AppLogger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
else
- Rails.logger.error "Export job failed to start for project ID #{self.id}"
+ Gitlab::AppLogger.error "Export job failed to start for project ID #{self.id}"
end
end
- # rubocop:enable Gitlab/RailsLogger
def import_export_shared
@import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
@@ -2082,21 +2065,6 @@ class Project < ApplicationRecord
end
end
- def change_repository_storage(new_repository_storage_key)
- return if repository_read_only?
- return if repository_storage == new_repository_storage_key
-
- raise ArgumentError unless ::Gitlab.config.repositories.storages.key?(new_repository_storage_key)
-
- storage_move = repository_storage_moves.create!(
- source_storage_name: repository_storage,
- destination_storage_name: new_repository_storage_key
- )
- storage_move.schedule!
-
- self.repository_read_only = true
- end
-
def pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
end
@@ -2438,12 +2406,32 @@ class Project < ApplicationRecord
touch(:last_activity_at, :last_repository_updated_at)
end
+ def metrics_setting
+ super || build_metrics_setting
+ end
+
private
def find_service(services, name)
services.find { |service| service.to_param == name }
end
+ def build_from_instance_or_template(name)
+ instance = find_service(services_instances, name)
+ return Service.build_from_integration(id, instance) if instance
+
+ template = find_service(services_templates, name)
+ return Service.build_from_integration(id, template) if template
+ end
+
+ def services_templates
+ @services_templates ||= Service.templates
+ end
+
+ def services_instances
+ @services_instances ||= Service.instances
+ end
+
def closest_namespace_setting(name)
namespace.closest_setting(name)
end
@@ -2572,10 +2560,6 @@ class Project < ApplicationRecord
end
end
- def services_templates
- @services_templates ||= Service.where(template: true)
- end
-
def ensure_pages_metadatum
pages_metadatum || create_pages_metadatum!
rescue ActiveRecord::RecordNotUnique
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index c295837002a..e5fc481b035 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -3,9 +3,6 @@
class ProjectCiCdSetting < ApplicationRecord
belongs_to :project, inverse_of: :ci_cd_settings
- # The version of the schema that first introduced this model/table.
- MINIMUM_SCHEMA_VERSION = 20180403035759
-
DEFAULT_GIT_DEPTH = 50
before_create :set_default_git_depth
@@ -20,16 +17,6 @@ class ProjectCiCdSetting < ApplicationRecord
default_value_for :forward_deployment_enabled, true
- def self.available?
- @available ||=
- ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
- end
-
- def self.reset_column_information
- @available = nil
- super
- end
-
def forward_deployment_enabled?
super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true)
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 9201cd24d66..b3ebcbd4b17 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -1,51 +1,16 @@
# frozen_string_literal: true
class ProjectFeature < ApplicationRecord
- # == Project features permissions
- #
- # Grants access level to project tools
- #
- # Tools can be enabled only for users, everyone or disabled
- # Access control is made only for non private projects
- #
- # levels:
- #
- # Disabled: not enabled for anyone
- # Private: enabled only for team members
- # Enabled: enabled for everyone able to access the project
- # Public: enabled for everyone (only allowed for pages)
- #
-
- # Permission levels
- DISABLED = 0
- PRIVATE = 10
- ENABLED = 20
- PUBLIC = 30
+ include Featurable
FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard).freeze
+
+ set_available_features(FEATURES)
+
PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
- STRING_OPTIONS = HashWithIndifferentAccess.new({
- 'disabled' => DISABLED,
- 'private' => PRIVATE,
- 'enabled' => ENABLED,
- 'public' => PUBLIC
- }).freeze
class << self
- def access_level_attribute(feature)
- feature = ensure_feature!(feature)
-
- "#{feature}_access_level".to_sym
- end
-
- def quoted_access_level_column(feature)
- attribute = connection.quote_column_name(access_level_attribute(feature))
- table = connection.quote_table_name(table_name)
-
- "#{table}.#{attribute}"
- end
-
def required_minimum_access_level(feature)
feature = ensure_feature!(feature)
@@ -60,24 +25,6 @@ class ProjectFeature < ApplicationRecord
required_minimum_access_level(feature)
end
end
-
- def access_level_from_str(level)
- STRING_OPTIONS.fetch(level)
- end
-
- def str_from_access_level(level)
- STRING_OPTIONS.key(level)
- end
-
- private
-
- def ensure_feature!(feature)
- feature = feature.model_name.plural if feature.respond_to?(:model_name)
- feature = feature.to_sym
- raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
-
- feature
- end
end
# Default scopes force us to unscope here since a service may need to check
@@ -107,45 +54,6 @@ class ProjectFeature < ApplicationRecord
end
end
- def feature_available?(feature, user)
- # This feature might not be behind a feature flag at all, so default to true
- return false unless ::Feature.enabled?(feature, user, default_enabled: true)
-
- get_permission(user, feature)
- end
-
- def access_level(feature)
- public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def string_access_level(feature)
- ProjectFeature.str_from_access_level(access_level(feature))
- end
-
- def builds_enabled?
- builds_access_level > DISABLED
- end
-
- def wiki_enabled?
- wiki_access_level > DISABLED
- end
-
- def merge_requests_enabled?
- merge_requests_access_level > DISABLED
- end
-
- def forking_enabled?
- forking_access_level > DISABLED
- end
-
- def issues_enabled?
- issues_access_level > DISABLED
- end
-
- def pages_enabled?
- pages_access_level > DISABLED
- end
-
def public_pages?
return true unless Gitlab.config.pages.access_control
@@ -164,7 +72,7 @@ class ProjectFeature < ApplicationRecord
# which cannot be higher than repository access level
def repository_children_level
validator = lambda do |field|
- level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
+ level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
not_allowed = level > repository_access_level
self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
end
@@ -175,8 +83,8 @@ class ProjectFeature < ApplicationRecord
# Validates access level for other than pages cannot be PUBLIC
def allowed_access_levels
validator = lambda do |field|
- level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
- not_allowed = level > ProjectFeature::ENABLED
+ level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
+ not_allowed = level > ENABLED
self.errors.add(field, "cannot have public visibility level") if not_allowed
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index f1c491d1a05..f065246e8af 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -15,8 +15,6 @@ class ProjectGroupLink < ApplicationRecord
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
- after_commit :refresh_group_members_authorized_projects
-
alias_method :shared_with_group, :group
def self.access_options
@@ -49,10 +47,6 @@ class ProjectGroupLink < ApplicationRecord
errors.add(:base, _("Project cannot be shared with the group it is in or one of its ancestors."))
end
end
-
- def refresh_group_members_authorized_projects
- group.refresh_members_authorized_projects
- end
end
ProjectGroupLink.prepend_if_ee('EE::ProjectGroupLink')
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index e434ea58729..4bd3ffbea2f 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -84,7 +84,11 @@ class ProjectImportState < ApplicationRecord
update_column(:last_error, sanitized_message)
rescue ActiveRecord::ActiveRecordError => e
- Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::Import::Logger.error(
+ message: 'Error setting import status to failed',
+ error: e.message,
+ original_error: sanitized_message
+ )
ensure
@errors = original_errors
end
diff --git a/app/models/project_metrics_setting.rb b/app/models/project_metrics_setting.rb
index a2a7dc571a4..c66d0f52f4c 100644
--- a/app/models/project_metrics_setting.rb
+++ b/app/models/project_metrics_setting.rb
@@ -4,6 +4,13 @@ class ProjectMetricsSetting < ApplicationRecord
belongs_to :project
validates :external_dashboard_url,
+ allow_nil: true,
length: { maximum: 255 },
addressable_url: { enforce_sanitization: true, ascii_only: true }
+
+ enum dashboard_timezone: { local: 0, utc: 1 }
+
+ def dashboard_timezone=(val)
+ super(val&.downcase)
+ end
end
diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb
index e88cc5cfca6..b18d9765a57 100644
--- a/app/models/project_repository_storage_move.rb
+++ b/app/models/project_repository_storage_move.rb
@@ -18,6 +18,7 @@ class ProjectRepositoryStorageMove < ApplicationRecord
on: :create,
presence: true,
inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
+ validate :project_repository_writable, on: :create
state_machine initial: :initial do
event :schedule do
@@ -36,7 +37,9 @@ class ProjectRepositoryStorageMove < ApplicationRecord
transition [:initial, :scheduled, :started] => :failed
end
- after_transition initial: :scheduled do |storage_move, _|
+ after_transition initial: :scheduled do |storage_move|
+ storage_move.project.update_column(:repository_read_only, true)
+
storage_move.run_after_commit do
ProjectUpdateRepositoryStorageWorker.perform_async(
storage_move.project_id,
@@ -46,6 +49,17 @@ class ProjectRepositoryStorageMove < ApplicationRecord
end
end
+ after_transition started: :finished do |storage_move|
+ storage_move.project.update_columns(
+ repository_read_only: false,
+ repository_storage: storage_move.destination_storage_name
+ )
+ end
+
+ after_transition started: :failed do |storage_move|
+ storage_move.project.update_column(:repository_read_only, false)
+ end
+
state :initial, value: 1
state :scheduled, value: 2
state :started, value: 3
@@ -55,4 +69,10 @@ class ProjectRepositoryStorageMove < ApplicationRecord
scope :order_created_at_desc, -> { order(created_at: :desc) }
scope :with_projects, -> { includes(project: :route) }
+
+ private
+
+ def project_repository_writable
+ errors.add(:project, _('is read only')) if project&.repository_read_only?
+ end
end
diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb
index 16bf37fd189..58c47accfd1 100644
--- a/app/models/project_services/alerts_service.rb
+++ b/app/models/project_services/alerts_service.rb
@@ -41,7 +41,7 @@ class AlertsService < Service
end
def description
- _('Receive alerts on GitLab from any source')
+ _('Authorize external services to send alerts to GitLab')
end
def detailed_description
diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb
new file mode 100644
index 00000000000..c8913775843
--- /dev/null
+++ b/app/models/project_services/chat_message/alert_message.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module ChatMessage
+ class AlertMessage < BaseMessage
+ attr_reader :title
+ attr_reader :alert_url
+ attr_reader :severity
+ attr_reader :events
+ attr_reader :status
+ attr_reader :started_at
+
+ def initialize(params)
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @title = params.dig(:object_attributes, :title)
+ @alert_url = params.dig(:object_attributes, :url)
+ @severity = params.dig(:object_attributes, :severity)
+ @events = params.dig(:object_attributes, :events)
+ @status = params.dig(:object_attributes, :status)
+ @started_at = params.dig(:object_attributes, :started_at)
+ end
+
+ def attachments
+ [{
+ title: title,
+ title_link: alert_url,
+ color: attachment_color,
+ fields: attachment_fields
+ }]
+ end
+
+ def message
+ "Alert firing in #{project_name}"
+ end
+
+ private
+
+ def attachment_color
+ "#C95823"
+ end
+
+ def attachment_fields
+ [
+ {
+ title: "Severity",
+ value: severity.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Events",
+ value: events,
+ short: true
+ },
+ {
+ title: "Status",
+ value: status.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Start time",
+ value: format_time(started_at),
+ short: true
+ }
+ ]
+ end
+
+ # This formats time into the following format
+ # April 23rd, 2020 1:06AM UTC
+ def format_time(time)
+ time = Time.zone.parse(time.to_s)
+ time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index 0a2d9120adc..c4fcdff8386 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -48,7 +48,7 @@ module ChatMessage
end
def merge_request_message
- "#{user_combined_name} #{state_or_action_text} #{merge_request_link} in #{project_link}"
+ "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
end
def merge_request_link
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 1cd3837433f..f4c6938fa78 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -183,7 +183,7 @@ module ChatMessage
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
- "#{project_url}/commits/#{ref}"
+ "#{project_url}/-/commits/#{ref}"
end
end
@@ -200,14 +200,14 @@ module ChatMessage
end
def pipeline_failed_jobs_url
- "#{project_url}/pipelines/#{pipeline_id}/failures"
+ "#{project_url}/-/pipelines/#{pipeline_id}/failures"
end
def pipeline_url
if failed_jobs.any?
pipeline_failed_jobs_url
else
- "#{project_url}/pipelines/#{pipeline_id}"
+ "#{project_url}/-/pipelines/#{pipeline_id}"
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c92e8ecb31c..ad531412fb7 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -252,8 +252,8 @@ class HipchatService < Service
status = pipeline_attributes[:status]
duration = pipeline_attributes[:duration]
- branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
- pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
+ branch_link = "<a href=\"#{project_url}/-/commits/#{CGI.escape(ref)}\">#{ref}</a>"
+ pipeline_url = "<a href=\"#{project_url}/-/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
"#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 53da874ede8..bb4d35cad22 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -6,6 +6,8 @@ class JiraService < IssueTrackerService
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
+ PROJECTS_PER_PAGE = 50
+
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
@@ -201,6 +203,10 @@ class JiraService < IssueTrackerService
add_comment(data, jira_issue)
end
+ def valid_connection?
+ test(nil)[:success]
+ end
+
def test(_)
result = test_settings
success = result.present?
@@ -209,11 +215,6 @@ class JiraService < IssueTrackerService
{ success: success, result: result }
end
- # Jira does not need test data.
- def test_data(_, _)
- nil
- end
-
override :support_close_issue?
def support_close_issue?
true
@@ -413,17 +414,9 @@ class JiraService < IssueTrackerService
# Handle errors when doing Jira API calls
def jira_request
yield
- rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
+ rescue => error
@error = error
- log_error(
- "Error sending message",
- client_url: client_url,
- error: {
- exception_class: error.class.name,
- exception_message: error.message,
- exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
- }
- )
+ log_error("Error sending message", client_url: client_url, error: @error.message)
nil
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index a58a264de5e..c11b2f7cc65 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -56,12 +56,6 @@ class PipelinesEmailService < Service
project&.ci_pipelines&.any?
end
- def test_data(project, user)
- data = Gitlab::DataBuilder::Pipeline.build(project.ci_pipelines.last)
- data[:user] = user.hook_attrs
- data
- end
-
def fields
[
{ type: 'textarea',
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 4a28d1ff2b0..44a41969b1c 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -5,6 +5,8 @@ class PrometheusService < MonitoringService
# Access to prometheus is directly through the API
prop_accessor :api_url
+ prop_accessor :google_iap_service_account_json
+ prop_accessor :google_iap_audience_client_id
boolean_accessor :manual_configuration
# We need to allow the self-monitoring project to connect to the internal
@@ -49,7 +51,7 @@ class PrometheusService < MonitoringService
end
def fields
- [
+ result = [
{
type: 'checkbox',
name: 'manual_configuration',
@@ -64,6 +66,28 @@ class PrometheusService < MonitoringService
required: true
}
]
+
+ if Feature.enabled?(:prometheus_service_iap_auth)
+ result += [
+ {
+ type: 'text',
+ name: 'google_iap_audience_client_id',
+ title: 'Google IAP Audience Client ID',
+ placeholder: s_('PrometheusService|Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'),
+ autocomplete: 'off',
+ required: false
+ },
+ {
+ type: 'textarea',
+ name: 'google_iap_service_account_json',
+ title: 'Google IAP Service Account JSON',
+ placeholder: s_('PrometheusService|Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'),
+ required: false
+ }
+ ]
+ end
+
+ result
end
# Check we can connect to the Prometheus API
@@ -77,7 +101,14 @@ class PrometheusService < MonitoringService
def prometheus_client
return unless should_return_client?
- Gitlab::PrometheusClient.new(api_url, allow_local_requests: allow_local_api_url?)
+ options = { allow_local_requests: allow_local_api_url? }
+
+ if Feature.enabled?(:prometheus_service_iap_auth) && behind_iap?
+ # Adds the Authorization header
+ options[:headers] = iap_client.apply({})
+ end
+
+ Gitlab::PrometheusClient.new(api_url, options)
end
def prometheus_available?
@@ -149,4 +180,12 @@ class PrometheusService < MonitoringService
Prometheus::CreateDefaultAlertsWorker.perform_async(project_id)
end
+
+ def behind_iap?
+ manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present?
+ end
+
+ def iap_client
+ @iap_client ||= Google::Auth::Credentials.new(Gitlab::Json.parse(google_iap_service_account_json), target_audience: google_iap_audience_client_id).client
+ end
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 6d567bb1383..79245e84238 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class SlackService < ChatNotificationService
+ prop_accessor EVENT_CHANNEL['alert']
+
def title
'Slack notifications'
end
@@ -21,13 +23,25 @@ class SlackService < ChatNotificationService
'https://hooks.slack.com/services/…'
end
+ def supported_events
+ additional = []
+ additional << 'alert'
+
+ super + additional
+ end
+
+ def get_message(object_kind, data)
+ return ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+
+ super
+ end
+
module Notifier
private
def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
-
notifier.ping(
message.pretext,
attachments: message.attachments,
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 7c93faf3928..9022d3e879d 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -4,10 +4,6 @@ class ProjectSetting < ApplicationRecord
belongs_to :project, inverse_of: :project_setting
self.primary_key = :project_id
-
- def self.where_or_create_by(attrs)
- where(primary_key => safe_find_or_create_by(attrs))
- end
end
ProjectSetting.prepend_if_ee('EE::ProjectSetting')
diff --git a/app/models/prometheus_alert_event.rb b/app/models/prometheus_alert_event.rb
index 7e61f6d5e3c..25f58a0b9d5 100644
--- a/app/models/prometheus_alert_event.rb
+++ b/app/models/prometheus_alert_event.rb
@@ -34,10 +34,4 @@ class PrometheusAlertEvent < ApplicationRecord
def self.status_value_for(name)
state_machines[:status].states[name].value
end
-
- def self.payload_key_for(gitlab_alert_id, started_at)
- plain = [gitlab_alert_id, started_at].join('/')
-
- Digest::SHA1.hexdigest(plain)
- end
end
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
index 5cab686f20b..0f626cb618e 100644
--- a/app/models/push_event.rb
+++ b/app/models/push_event.rb
@@ -68,7 +68,7 @@ class PushEvent < Event
end
def self.sti_name
- PUSHED
+ actions[:pushed]
end
def push_action?
@@ -111,7 +111,7 @@ class PushEvent < Event
end
def validate_push_action
- return if action == PUSHED
+ return if pushed_action?
errors.add(:action, "the action #{action.inspect} is not valid")
end
diff --git a/app/models/releases/evidence.rb b/app/models/releases/evidence.rb
index 1aac7e33e41..7c428f5ad03 100644
--- a/app/models/releases/evidence.rb
+++ b/app/models/releases/evidence.rb
@@ -1,44 +1,35 @@
# frozen_string_literal: true
-class Releases::Evidence < ApplicationRecord
- include ShaAttribute
- include Presentable
+module Releases
+ class Evidence < ApplicationRecord
+ include ShaAttribute
+ include Presentable
- belongs_to :release, inverse_of: :evidences
+ belongs_to :release, inverse_of: :evidences
- before_validation :generate_summary_and_sha
+ default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
- default_scope { order(created_at: :asc) }
+ sha_attribute :summary_sha
+ alias_attribute :collected_at, :created_at
+ alias_attribute :sha, :summary_sha
- sha_attribute :summary_sha
- alias_attribute :collected_at, :created_at
-
- def milestones
- @milestones ||= release.milestones.includes(:issues)
- end
-
- ##
- # Return `summary` without sensitive information.
- #
- # Removing issues from summary in order to prevent leaking confidential ones.
- # See more https://gitlab.com/gitlab-org/gitlab/issues/121930
- def summary
- safe_summary = read_attribute(:summary)
-
- safe_summary.dig('release', 'milestones')&.each do |milestone|
- milestone.delete('issues')
+ def milestones
+ @milestones ||= release.milestones.includes(:issues)
end
- safe_summary
- end
-
- private
+ ##
+ # Return `summary` without sensitive information.
+ #
+ # Removing issues from summary in order to prevent leaking confidential ones.
+ # See more https://gitlab.com/gitlab-org/gitlab/issues/121930
+ def summary
+ safe_summary = read_attribute(:summary)
- def generate_summary_and_sha
- summary = Evidences::EvidenceSerializer.new.represent(self) # rubocop: disable CodeReuse/Serializer
- return unless summary
+ safe_summary.dig('release', 'milestones')&.each do |milestone|
+ milestone.delete('issues')
+ end
- self.summary = summary
- self.summary_sha = Gitlab::CryptoHelper.sha256(summary)
+ safe_summary
+ end
end
end
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
index 65be2a22958..dc7e78a85a9 100644
--- a/app/models/releases/link.rb
+++ b/app/models/releases/link.rb
@@ -14,6 +14,13 @@ module Releases
scope :sorted, -> { order(created_at: :desc) }
+ enum link_type: {
+ other: 0,
+ runbook: 1,
+ package: 2,
+ image: 3
+ }
+
def internal?
url.start_with?(release.project.web_url)
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 8e7612e63c8..8b15d481c1b 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -68,13 +68,13 @@ class RemoteMirror < ApplicationRecord
after_transition any => :started do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_running)
- remote_mirror.update(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.current)
end
after_transition started: :finished do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_finished)
- timestamp = Time.now
+ timestamp = Time.current
remote_mirror.update!(
last_update_at: timestamp,
last_successful_update_at: timestamp,
@@ -86,7 +86,7 @@ class RemoteMirror < ApplicationRecord
after_transition started: :failed do |remote_mirror|
Gitlab::Metrics.add_event(:remote_mirrors_failed)
- remote_mirror.update(last_update_at: Time.now)
+ remote_mirror.update(last_update_at: Time.current)
remote_mirror.run_after_commit do
RemoteMirrorNotificationWorker.perform_async(remote_mirror.id)
@@ -144,9 +144,9 @@ class RemoteMirror < ApplicationRecord
return unless sync?
if recently_scheduled?
- RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now)
+ RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.current)
else
- RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now)
+ RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.current)
end
end
@@ -261,7 +261,7 @@ class RemoteMirror < ApplicationRecord
def recently_scheduled?
return false unless self.last_update_started_at
- self.last_update_started_at >= Time.now - backoff_delay
+ self.last_update_started_at >= Time.current - backoff_delay
end
def reset_fields
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2673033ff1f..911cfc7db38 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -950,7 +950,6 @@ class Repository
async_remove_remote(remote_name) if tmp_remote_name
end
- # rubocop:disable Gitlab/RailsLogger
def async_remove_remote(remote_name)
return unless remote_name
return unless project
@@ -958,14 +957,13 @@ class Repository
job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
if job_id
- Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
+ Gitlab::AppLogger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
else
- Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
+ Gitlab::AppLogger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
end
job_id
end
- # rubocop:enable Gitlab/RailsLogger
def fetch_source_branch!(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
@@ -1171,7 +1169,7 @@ class Repository
if target
target.committed_date
else
- Time.now
+ Time.current
end
end
end
diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb
index e468d716239..6b1793a551f 100644
--- a/app/models/repository_language.rb
+++ b/app/models/repository_language.rb
@@ -4,7 +4,7 @@ class RepositoryLanguage < ApplicationRecord
belongs_to :project
belongs_to :programming_language
- default_scope { includes(:programming_language) }
+ default_scope { includes(:programming_language) } # rubocop:disable Cop/DefaultScope
validates :project, presence: true
validates :share, inclusion: { in: 0..100, message: "The share of a language is between 0 and 100" }
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 845be408d5e..cc96698be09 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -71,6 +71,14 @@ class ResourceLabelEvent < ResourceEvent
end
end
+ def self.visible_to_user?(user, events)
+ ResourceLabelEvent.preload_label_subjects(events)
+
+ events.select do |event|
+ Ability.allowed?(user, :read_label, event)
+ end
+ end
+
private
def label_reference
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index 039f26d8e3f..36068cf508b 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -9,6 +9,8 @@ class ResourceMilestoneEvent < ResourceEvent
validate :exactly_one_issuable
+ scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
+
enum action: {
add: 1,
remove: 2
@@ -26,4 +28,12 @@ class ResourceMilestoneEvent < ResourceEvent
def milestone_title
milestone&.title
end
+
+ def milestone_parent
+ milestone&.parent
+ end
+
+ def issuable
+ issue || merge_request
+ end
end
diff --git a/app/models/review.rb b/app/models/review.rb
new file mode 100644
index 00000000000..5a30e2963c8
--- /dev/null
+++ b/app/models/review.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class Review < ApplicationRecord
+ include Participable
+ include Mentionable
+
+ belongs_to :author, class_name: 'User', foreign_key: :author_id, inverse_of: :reviews
+ belongs_to :merge_request, inverse_of: :reviews
+ belongs_to :project, inverse_of: :reviews
+
+ has_many :notes, -> { order(:id) }, inverse_of: :review
+
+ delegate :name, to: :author, prefix: true
+
+ participant :author
+
+ def all_references(current_user = nil, extractor: nil)
+ ext = super
+
+ notes.each do |note|
+ note.all_references(current_user, extractor: ext)
+ end
+
+ ext
+ end
+
+ def user_mentions
+ merge_request.user_mentions.where.not(note_id: nil)
+ end
+end
diff --git a/app/models/route.rb b/app/models/route.rb
index 63a0461807b..706589e79b8 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -42,7 +42,7 @@ class Route < ApplicationRecord
old_path = route.path
# Callbacks must be run manually
- route.update_columns(attributes.merge(updated_at: Time.now))
+ route.update_columns(attributes.merge(updated_at: Time.current))
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
diff --git a/app/models/self_managed_prometheus_alert_event.rb b/app/models/self_managed_prometheus_alert_event.rb
index d2d4a5c37d4..cf26563e92d 100644
--- a/app/models/self_managed_prometheus_alert_event.rb
+++ b/app/models/self_managed_prometheus_alert_event.rb
@@ -15,10 +15,4 @@ class SelfManagedPrometheusAlertEvent < ApplicationRecord
yield event if block_given?
end
end
-
- def self.payload_key_for(started_at, alert_name, query_expression)
- plain = [started_at, alert_name, query_expression].join('/')
-
- Digest::SHA1.hexdigest(plain)
- end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index fb4d9a77077..2880526c9de 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -22,6 +22,7 @@ class Service < ApplicationRecord
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, false
+ default_value_for :alert_events, true
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :confidential_issues_events, true
@@ -72,6 +73,7 @@ class Service < ApplicationRecord
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
+ scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
@@ -134,8 +136,12 @@ class Service < ApplicationRecord
%w(active)
end
- def test_data(project, user)
- Gitlab::DataBuilder::Push.build_sample(project, user)
+ def to_service_hash
+ as_json(methods: :type, except: %w[id template instance project_id])
+ end
+
+ def to_data_fields_hash
+ data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id')
end
def event_channel_names
@@ -164,7 +170,7 @@ class Service < ApplicationRecord
end
def configurable_events
- events = self.class.supported_events
+ events = supported_events
# No need to disable individual triggers when there is only one
if events.count == 1
@@ -335,17 +341,19 @@ class Service < ApplicationRecord
services_names.map { |service_name| "#{service_name}_service".camelize }
end
- def self.build_from_template(project_id, template)
- service = template.dup
+ def self.build_from_integration(project_id, integration)
+ service = integration.dup
- if template.supports_data_fields?
- data_fields = template.data_fields.dup
+ if integration.supports_data_fields?
+ data_fields = integration.data_fields.dup
data_fields.service = service
end
service.template = false
+ service.instance = false
+ service.inherit_from_id = integration.id if integration.instance?
service.project_id = project_id
- service.active = false if service.active? && service.invalid?
+ service.active = false if service.invalid?
service
end
@@ -394,6 +402,8 @@ class Service < ApplicationRecord
"Event will be triggered when a commit is created/updated"
when "deployment"
"Event will be triggered when a deployment finishes"
+ when "alert"
+ "Event will be triggered when a new, unique alert is recorded"
end
end
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
new file mode 100644
index 00000000000..fa3760f0c56
--- /dev/null
+++ b/app/models/service_list.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class ServiceList
+ def initialize(batch, service_hash, extra_hash = {})
+ @batch = batch
+ @service_hash = service_hash
+ @extra_hash = extra_hash
+ end
+
+ def to_array
+ [Service, columns, values]
+ end
+
+ private
+
+ attr_reader :batch, :service_hash, :extra_hash
+
+ def columns
+ (service_hash.keys << 'project_id') + extra_hash.keys
+ end
+
+ def values
+ batch.map do |project_id|
+ (service_hash.values << project_id) + extra_hash.values
+ end
+ end
+end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 72ebdf61787..b63ab003711 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -337,7 +337,7 @@ class Snippet < ApplicationRecord
class << self
# Searches for snippets with a matching title, description or file name.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
# query - The search query as a String.
#
diff --git a/app/models/snippet_input_action.rb b/app/models/snippet_input_action.rb
new file mode 100644
index 00000000000..7f4ab775ab0
--- /dev/null
+++ b/app/models/snippet_input_action.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class SnippetInputAction
+ include ActiveModel::Validations
+
+ ACTIONS = %i[create update delete move].freeze
+
+ ACTIONS.each do |action_const|
+ define_method "#{action_const}_action?" do
+ action == action_const
+ end
+ end
+
+ attr_reader :action, :previous_path, :file_path, :content
+
+ validates :action, inclusion: { in: ACTIONS, message: "%{value} is not a valid action" }
+ validates :previous_path, presence: true, if: :move_action?
+ validates :file_path, presence: true
+ validates :content, presence: true, if: -> (action) { action.create_action? || action.update_action? }
+ validate :ensure_same_file_path_and_previous_path, if: :update_action?
+ validate :ensure_allowed_action
+
+ def initialize(action: nil, previous_path: nil, file_path: nil, content: nil, allowed_actions: nil)
+ @action = action&.to_sym
+ @previous_path = previous_path
+ @file_path = file_path
+ @content = content
+ @allowed_actions = Array(allowed_actions).map(&:to_sym)
+ end
+
+ def to_commit_action
+ {
+ action: action,
+ previous_path: build_previous_path,
+ file_path: file_path,
+ content: content
+ }
+ end
+
+ private
+
+ def build_previous_path
+ return previous_path unless update_action?
+
+ previous_path.presence || file_path
+ end
+
+ def ensure_same_file_path_and_previous_path
+ return if previous_path.blank? || file_path.blank?
+ return if previous_path == file_path
+
+ errors.add(:file_path, "can't be different from the previous_path attribute")
+ end
+
+ def ensure_allowed_action
+ return if @allowed_actions.empty?
+
+ unless @allowed_actions.include?(action)
+ errors.add(:action, 'is not allowed')
+ end
+ end
+end
diff --git a/app/models/snippet_input_action_collection.rb b/app/models/snippet_input_action_collection.rb
new file mode 100644
index 00000000000..38313e3a980
--- /dev/null
+++ b/app/models/snippet_input_action_collection.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class SnippetInputActionCollection
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :actions
+
+ delegate :empty?, :any?, :[], to: :actions
+
+ def initialize(actions = [], allowed_actions: nil)
+ @actions = actions.map { |action| SnippetInputAction.new(action.merge(allowed_actions: allowed_actions)) }
+ end
+
+ def to_commit_actions
+ strong_memoize(:commit_actions) do
+ actions.map { |action| action.to_commit_action }
+ end
+ end
+
+ def valid?
+ strong_memoize(:valid) do
+ actions.all?(&:valid?)
+ end
+ end
+end
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index 72690ad7d04..7e34988c7a0 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -107,7 +107,7 @@ class SshHostKey
if status.success? && !errors.present?
{ known_hosts: known_hosts }
else
- Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.debug("Failed to detect SSH host keys for #{id}: #{errors}")
{ error: 'Failed to detect SSH host keys' }
end
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 345172cca76..f643d52587e 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -35,7 +35,7 @@ module Storage
gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
return true
rescue => e
- Rails.logger.error "Exception renaming #{old_full_path} -> #{new_full_path}: #{e}" # rubocop:disable Gitlab/RailsLogger
+ Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}")
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
return false
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index c4e047ff9d1..6ed074b2190 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -2,6 +2,8 @@
module Terraform
class State < ApplicationRecord
+ include UsageStatistics
+
DEFAULT = '{"version":1}'.freeze
HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32
diff --git a/app/models/todo.rb b/app/models/todo.rb
index dc42551f0ab..102f36a991e 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -66,6 +66,8 @@ class Todo < ApplicationRecord
scope :with_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) }
scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
+ enum resolved_by_action: { system_done: 0, api_all_done: 1, api_done: 2, mark_all_done: 3, mark_done: 4 }, _prefix: :resolved_by
+
state_machine :state, initial: :pending do
event :done do
transition [:pending] => :done
@@ -100,17 +102,17 @@ class Todo < ApplicationRecord
state.nil? ? exists?(target: target) : exists?(target: target, state: state)
end
- # Updates the state of a relation of todos to the new state.
+ # Updates attributes of a relation of todos to the new state.
#
- # new_state - The new state of the todos.
+ # new_attributes - The new attributes of the todos.
#
# Returns an `Array` containing the IDs of the updated todos.
- def update_state(new_state)
- # Only update those that are not really on that state
- base = where.not(state: new_state).except(:order)
+ def batch_update(**new_attributes)
+ # Only update those that have different state
+ base = where.not(state: new_attributes[:state]).except(:order)
ids = base.pluck(:id)
- base.update_all(state: new_state, updated_at: Time.now)
+ base.update_all(new_attributes.merge(updated_at: Time.current))
ids
end
@@ -187,6 +189,10 @@ class Todo < ApplicationRecord
target_type == DesignManagement::Design.name
end
+ def for_alert?
+ target_type == AlertManagement::Alert.name
+ end
+
# override to return commits, which are not active record
def target
if for_commit?
diff --git a/app/models/uploads/base.rb b/app/models/uploads/base.rb
index 442ed733566..7555c72e101 100644
--- a/app/models/uploads/base.rb
+++ b/app/models/uploads/base.rb
@@ -7,7 +7,7 @@ module Uploads
attr_reader :logger
def initialize(logger: nil)
- @logger = Rails.logger # rubocop:disable Gitlab/RailsLogger
+ @logger = Gitlab::AppLogger
end
def delete_keys_async(keys_to_delete)
diff --git a/app/models/user.rb b/app/models/user.rb
index 927ffa4d12b..431a5b3a5b7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -69,7 +69,6 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180
- ignore_column :bot_type, remove_with: '13.1', remove_after: '2020-05-22'
ignore_column :ghost, remove_with: '13.2', remove_after: '2020-06-22'
# Override Devise::Models::Trackable#update_tracked_fields!
@@ -181,6 +180,8 @@ class User < ApplicationRecord
has_one :user_highest_role
has_one :user_canonical_email
+ has_many :reviews, foreign_key: :author_id, inverse_of: :author
+
#
# Validations
#
@@ -264,18 +265,21 @@ class User < ApplicationRecord
# User's role
enum role: { software_developer: 0, development_team_lead: 1, devops_engineer: 2, systems_administrator: 3, security_analyst: 4, data_analyst: 5, product_manager: 6, product_designer: 7, other: 8 }, _suffix: true
+ delegate :notes_filter_for,
+ :set_notes_filter,
+ :first_day_of_week, :first_day_of_week=,
+ :timezone, :timezone=,
+ :time_display_relative, :time_display_relative=,
+ :time_format_in_24h, :time_format_in_24h=,
+ :show_whitespace_in_diffs, :show_whitespace_in_diffs=,
+ :tab_width, :tab_width=,
+ :sourcegraph_enabled, :sourcegraph_enabled=,
+ :setup_for_company, :setup_for_company=,
+ :render_whitespace_in_code, :render_whitespace_in_code=,
+ :experience_level, :experience_level=,
+ to: :user_preference
+
delegate :path, to: :namespace, allow_nil: true, prefix: true
- delegate :notes_filter_for, to: :user_preference
- delegate :set_notes_filter, to: :user_preference
- delegate :first_day_of_week, :first_day_of_week=, to: :user_preference
- delegate :timezone, :timezone=, to: :user_preference
- delegate :time_display_relative, :time_display_relative=, to: :user_preference
- delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
- delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference
- delegate :tab_width, :tab_width=, to: :user_preference
- delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
- delegate :setup_for_company, :setup_for_company=, to: :user_preference
- delegate :render_whitespace_in_code, :render_whitespace_in_code=, to: :user_preference
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
@@ -342,6 +346,7 @@ class User < ApplicationRecord
where('EXISTS (?)',
::PersonalAccessToken
.where('personal_access_tokens.user_id = users.id')
+ .without_impersonation
.expiring_and_not_notified(at).select(1))
end
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
@@ -517,7 +522,7 @@ class User < ApplicationRecord
# Searches users matching the given query.
#
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
#
# query - The search query as a String
#
@@ -560,7 +565,7 @@ class User < ApplicationRecord
# searches user by given pattern
# it compares name, email, username fields and user's secondary emails with given pattern
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ # This method uses ILIKE on PostgreSQL.
def search_with_secondary_emails(query)
return none if query.blank?
@@ -689,7 +694,7 @@ class User < ApplicationRecord
@reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
- self.reset_password_sent_at = Time.now.utc
+ self.reset_password_sent_at = Time.current.utc
@reset_token
end
@@ -716,7 +721,7 @@ class User < ApplicationRecord
otp_grace_period_started_at: nil,
otp_backup_codes: nil
)
- self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
+ self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll
end
end
@@ -957,11 +962,11 @@ class User < ApplicationRecord
end
def allow_password_authentication_for_web?
- Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user? && !ultraauth_user?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
end
def allow_password_authentication_for_git?
- Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user? && !ultraauth_user?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
end
def can_change_username?
@@ -1049,14 +1054,6 @@ class User < ApplicationRecord
end
end
- def ultraauth_user?
- if identities.loaded?
- identities.find { |identity| Gitlab::Auth::OAuth::Provider.ultraauth_provider?(identity.provider) && !identity.extern_uid.nil? }
- else
- identities.exists?(["provider = ? AND extern_uid IS NOT NULL", "ultraauth"])
- end
- end
-
def ldap_identity
@ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
end
@@ -1129,7 +1126,7 @@ class User < ApplicationRecord
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current
else
false
end
@@ -1378,7 +1375,7 @@ class User < ApplicationRecord
def contributed_projects
events = Event.select(:project_id)
.contributions.where(author_id: self)
- .where("created_at > ?", Time.now - 1.year)
+ .where("created_at > ?", Time.current - 1.year)
.distinct
.reorder(nil)
@@ -1642,16 +1639,12 @@ class User < ApplicationRecord
super.presence || build_user_detail
end
- def todos_limited_to(ids)
- todos.where(id: ids)
- end
-
def pending_todo_for(target)
todos.find_by(target: target, state: :pending)
end
def password_expired?
- !!(password_expires_at && password_expires_at < Time.now)
+ !!(password_expires_at && password_expires_at < Time.current)
end
def can_be_deactivated?
@@ -1832,7 +1825,7 @@ class User < ApplicationRecord
def update_highest_role?
return false unless persisted?
- (previous_changes.keys & %w(state user_type ghost)).any?
+ (previous_changes.keys & %w(state user_type)).any?
end
def update_highest_role_attribute
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index f6f72f4b77a..1c615777018 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -9,9 +9,6 @@ class UserInteractedProject < ApplicationRecord
CACHE_EXPIRY_TIME = 1.day
- # Schema version required for this model
- REQUIRED_SCHEMA_VERSION = 20180223120443
-
class << self
def track(event)
# For events without a project, we simply don't care.
@@ -38,17 +35,6 @@ class UserInteractedProject < ApplicationRecord
end
end
- # Check if we can safely call .track (table exists)
- def available?
- @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
- end
-
- # Flushes cached information about schema
- def reset_column_information
- @available_flag = nil
- super
- end
-
private
def cached_exists?(project_id:, user_id:, &block)
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 48a56cded0e..d3b3a46bf74 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -15,6 +15,8 @@ class UserPreference < ApplicationRecord
less_than_or_equal_to: Gitlab::TabWidth::MAX
}
+ enum experience_level: { novice: 0, experienced: 1 }
+
default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
default_value_for :time_display_relative, value: true, allows_nil: false
diff --git a/app/models/web_ide_terminal.rb b/app/models/web_ide_terminal.rb
new file mode 100644
index 00000000000..ef70df2405f
--- /dev/null
+++ b/app/models/web_ide_terminal.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class WebIdeTerminal
+ include ::Gitlab::Routing
+
+ attr_reader :build, :project
+
+ delegate :id, :status, to: :build
+
+ def initialize(build)
+ @build = build
+ @project = build.project
+ end
+
+ def show_path
+ web_ide_terminal_route_generator(:show)
+ end
+
+ def retry_path
+ web_ide_terminal_route_generator(:retry)
+ end
+
+ def cancel_path
+ web_ide_terminal_route_generator(:cancel)
+ end
+
+ def terminal_path
+ terminal_project_job_path(project, build, format: :ws)
+ end
+
+ def proxy_websocket_path
+ proxy_project_job_path(project, build, format: :ws)
+ end
+
+ def services
+ build.services.map(&:alias).compact + Array(build.image&.alias)
+ end
+
+ private
+
+ def web_ide_terminal_route_generator(action, options = {})
+ options.reverse_merge!(action: action,
+ controller: 'projects/web_ide_terminals',
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: build.id,
+ only_path: true)
+
+ url_for(options)
+ end
+end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 54bcec32095..4c497cc304c 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -205,7 +205,7 @@ class Wiki
end
def wiki_base_path
- Gitlab.config.gitlab.relative_url_root + web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
+ web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
end
private
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
index 712ba79bbd2..df2fe25b08b 100644
--- a/app/models/wiki_directory.rb
+++ b/app/models/wiki_directory.rb
@@ -15,6 +15,6 @@ class WikiDirectory
# Relative path to the partial to be used when rendering collections
# of this object.
def to_partial_path
- 'projects/wikis/wiki_directory'
+ '../shared/wikis/wiki_directory'
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 319cdd38d93..9e4e2f68d38 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -261,8 +261,7 @@ class WikiPage
# Relative path to the partial to be used when rendering collections
# of this object.
def to_partial_path
- # TODO: Move into shared/ with https://gitlab.com/gitlab-org/gitlab/-/issues/196054
- 'projects/wikis/wiki_page'
+ '../shared/wikis/wiki_page'
end
def id
@@ -271,7 +270,10 @@ class WikiPage
def title_changed?
if persisted?
- old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(page.url_path))
+ # A page's `title` will be returned from Gollum/Gitaly with any +<>
+ # characters changed to -, whereas the `path` preserves these characters.
+ path_without_extension = Pathname(page.path).sub_ext('').to_s
+ old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(path_without_extension))
new_title, new_dir = wiki.page_title_and_dir(self.class.unhyphenize(title))
new_title != old_title || (title.include?('/') && new_dir != old_dir)
diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb
index 474968122b1..215d84dc463 100644
--- a/app/models/wiki_page/meta.rb
+++ b/app/models/wiki_page/meta.rb
@@ -120,7 +120,7 @@ class WikiPage
end
def insert_slugs(strings, is_new, canonical_slug)
- creation = Time.now.utc
+ creation = Time.current.utc
slug_attrs = strings.map do |slug|
{
diff --git a/app/models/wiki_page/slug.rb b/app/models/wiki_page/slug.rb
index 246fa8d6442..c1725d34921 100644
--- a/app/models/wiki_page/slug.rb
+++ b/app/models/wiki_page/slug.rb
@@ -16,11 +16,11 @@ class WikiPage
scope :canonical, -> { where(canonical: true) }
def update_columns(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
+ super(attrs.reverse_merge(updated_at: Time.current.utc))
end
def self.update_all(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
+ super(attrs.reverse_merge(updated_at: Time.current.utc))
end
end
end