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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/alert_management/alert.rb2
-rw-r--r--app/models/alert_management/http_integration.rb41
-rw-r--r--app/models/analytics/instance_statistics/measurement.rb22
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/application_setting/term.rb2
-rw-r--r--app/models/audit_event.rb13
-rw-r--r--app/models/authentication_event.rb10
-rw-r--r--app/models/ci/build.rb18
-rw-r--r--app/models/ci/build_pending_state.rb6
-rw-r--r--app/models/ci/build_trace_chunk.rb38
-rw-r--r--app/models/ci/build_trace_chunks/database.rb2
-rw-r--r--app/models/ci/pipeline.rb7
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb3
-rw-r--r--app/models/commit.rb32
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/concerns/approvable_base.rb24
-rw-r--r--app/models/concerns/avatarable.rb15
-rw-r--r--app/models/concerns/checksummable.rb8
-rw-r--r--app/models/concerns/counter_attribute.rb29
-rw-r--r--app/models/concerns/integration.rb4
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/issue_available_features.rb23
-rw-r--r--app/models/concerns/mentionable.rb16
-rw-r--r--app/models/concerns/reactive_caching.rb3
-rw-r--r--app/models/concerns/reactive_service.rb1
-rw-r--r--app/models/concerns/referable.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb6
-rw-r--r--app/models/concerns/update_project_statistics.rb5
-rw-r--r--app/models/container_repository.rb8
-rw-r--r--app/models/data_list.rb10
-rw-r--r--app/models/design_management/design.rb10
-rw-r--r--app/models/design_management/design_collection.rb1
-rw-r--r--app/models/environment.rb1
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/group.rb109
-rw-r--r--app/models/group_import_state.rb3
-rw-r--r--app/models/issuable_severity.rb7
-rw-r--r--app/models/issue.rb17
-rw-r--r--app/models/issue_email_participant.rb13
-rw-r--r--app/models/iteration.rb16
-rw-r--r--app/models/member.rb13
-rw-r--r--app/models/merge_request.rb28
-rw-r--r--app/models/merge_request_context_commit.rb4
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/namespace.rb50
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_reason.rb2
-rw-r--r--app/models/notification_recipient.rb2
-rw-r--r--app/models/notification_setting.rb1
-rw-r--r--app/models/operations/feature_flags/strategy.rb33
-rw-r--r--app/models/packages/event.rb26
-rw-r--r--app/models/packages/package.rb12
-rw-r--r--app/models/pages_deployment.rb4
-rw-r--r--app/models/postgresql/replication_slot.rb4
-rw-r--r--app/models/project.rb33
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_statistics.rb31
-rw-r--r--app/models/project_tracing_setting.rb15
-rw-r--r--app/models/prometheus_alert.rb2
-rw-r--r--app/models/prometheus_metric.rb2
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/resource_label_event.rb11
-rw-r--r--app/models/resource_state_event.rb25
-rw-r--r--app/models/resource_timebox_event.rb15
-rw-r--r--app/models/resource_weight_event.rb10
-rw-r--r--app/models/service.rb23
-rw-r--r--app/models/service_list.rb12
-rw-r--r--app/models/snippet_input_action_collection.rb6
-rw-r--r--app/models/snippet_repository.rb2
-rw-r--r--app/models/snippet_statistics.rb2
-rw-r--r--app/models/system_note_metadata.rb4
-rw-r--r--app/models/terraform/state.rb13
-rw-r--r--app/models/terraform/state_version.rb6
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb18
-rw-r--r--app/models/user_callout.rb2
-rw-r--r--app/models/user_interacted_project.rb2
-rw-r--r--app/models/user_preference.rb3
-rw-r--r--app/models/wiki.rb4
-rw-r--r--app/models/wiki_directory.rb39
-rw-r--r--app/models/wiki_page.rb23
82 files changed, 829 insertions, 236 deletions
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index e9b89af45c6..f5e56cb50c9 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -191,7 +191,7 @@ module AlertManagement
end
def prometheus?
- monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]
+ monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
def register_new_event!
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
new file mode 100644
index 00000000000..7f954e1d384
--- /dev/null
+++ b/app/models/alert_management/http_integration.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class HttpIntegration < ApplicationRecord
+ belongs_to :project, inverse_of: :alert_management_http_integrations
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm'
+
+ validates :project, presence: true
+ validates :active, inclusion: { in: [true, false] }
+
+ validates :token, presence: true
+ validates :name, presence: true, length: { maximum: 255 }
+ validates :endpoint_identifier, presence: true, length: { maximum: 255 }
+ validates :endpoint_identifier, uniqueness: { scope: [:project_id, :active] }, if: :active?
+
+ before_validation :prevent_token_assignment
+ before_validation :ensure_token
+
+ private
+
+ def prevent_token_assignment
+ if token.present? && token_changed?
+ self.token = nil
+ self.encrypted_token = encrypted_token_was
+ self.encrypted_token_iv = encrypted_token_iv_was
+ end
+ end
+
+ def ensure_token
+ self.token = generate_token if token.blank?
+ end
+
+ def generate_token
+ SecureRandom.hex
+ end
+ end
+end
diff --git a/app/models/analytics/instance_statistics/measurement.rb b/app/models/analytics/instance_statistics/measurement.rb
index eaaf9e999b3..76cc1111e90 100644
--- a/app/models/analytics/instance_statistics/measurement.rb
+++ b/app/models/analytics/instance_statistics/measurement.rb
@@ -3,13 +3,19 @@
module Analytics
module InstanceStatistics
class Measurement < ApplicationRecord
+ EXPERIMENTAL_IDENTIFIERS = %i[pipelines_succeeded pipelines_failed pipelines_canceled pipelines_skipped].freeze
+
enum identifier: {
projects: 1,
users: 2,
issues: 3,
merge_requests: 4,
groups: 5,
- pipelines: 6
+ pipelines: 6,
+ pipelines_succeeded: 7,
+ pipelines_failed: 8,
+ pipelines_canceled: 9,
+ pipelines_skipped: 10
}
IDENTIFIER_QUERY_MAPPING = {
@@ -18,7 +24,11 @@ module Analytics
identifiers[:issues] => -> { Issue },
identifiers[:merge_requests] => -> { MergeRequest },
identifiers[:groups] => -> { Group },
- identifiers[:pipelines] => -> { Ci::Pipeline }
+ identifiers[:pipelines] => -> { Ci::Pipeline },
+ identifiers[:pipelines_succeeded] => -> { Ci::Pipeline.success },
+ identifiers[:pipelines_failed] => -> { Ci::Pipeline.failed },
+ identifiers[:pipelines_canceled] => -> { Ci::Pipeline.canceled },
+ identifiers[:pipelines_skipped] => -> { Ci::Pipeline.skipped }
}.freeze
validates :recorded_at, :identifier, :count, presence: true
@@ -26,6 +36,14 @@ module Analytics
scope :order_by_latest, -> { order(recorded_at: :desc) }
scope :with_identifier, -> (identifier) { where(identifier: identifier) }
+
+ def self.measurement_identifier_values
+ if Feature.enabled?(:store_ci_pipeline_counts_by_status, default_enabled: true)
+ identifiers.values
+ else
+ identifiers.values - EXPERIMENTAL_IDENTIFIERS.map { |identifier| identifiers[identifier] }
+ end
+ end
end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e9a3dcf39df..2d26d5655ca 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -95,7 +95,7 @@ class ApplicationSetting < ApplicationRecord
allow_blank: true,
addressable_url: true
- validates :admin_notification_email,
+ validates :abuse_notification_email,
devise_email: true,
allow_blank: true
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
index 723540c9b91..bab036f5697 100644
--- a/app/models/application_setting/term.rb
+++ b/app/models/application_setting/term.rb
@@ -14,6 +14,8 @@ class ApplicationSetting
end
def accepted_by_user?(user)
+ return true if user.project_bot?
+
user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists?
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index f46803be057..929e7ecbf48 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -5,7 +5,13 @@ class AuditEvent < ApplicationRecord
include IgnorableColumns
include BulkInsertSafe
- PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details, :target_type].freeze
+ PARALLEL_PERSISTENCE_COLUMNS = [
+ :author_name,
+ :entity_path,
+ :target_details,
+ :target_type,
+ :target_id
+ ].freeze
ignore_column :type, remove_with: '13.6', remove_after: '2020-11-22'
@@ -16,6 +22,7 @@ class AuditEvent < ApplicationRecord
validates :author_id, presence: true
validates :entity_id, presence: true
validates :entity_type, presence: true
+ validates :ip_address, ip_address: true
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
@@ -59,8 +66,8 @@ class AuditEvent < ApplicationRecord
end
def lazy_author
- BatchLoader.for(author_id).batch(default_value: default_author_value) do |author_ids, loader|
- User.where(id: author_ids).find_each do |user|
+ BatchLoader.for(author_id).batch(default_value: default_author_value, replace_methods: false) do |author_ids, loader|
+ User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
loader.call(user.id, user)
end
end
diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb
index 1ac3c5fbd9c..ac6e08caf50 100644
--- a/app/models/authentication_event.rb
+++ b/app/models/authentication_event.rb
@@ -1,12 +1,22 @@
# frozen_string_literal: true
class AuthenticationEvent < ApplicationRecord
+ include UsageStatistics
+
belongs_to :user, optional: true
validates :provider, :user_name, :result, presence: true
+ validates :ip_address, ip_address: true
enum result: {
failed: 0,
success: 1
}
+
+ scope :for_provider, ->(provider) { where(provider: provider) }
+ scope :ldap, -> { where('provider LIKE ?', 'ldap%')}
+
+ def self.providers
+ distinct.pluck(:provider)
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 99580a52e96..6b4a71d4e28 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -327,6 +327,8 @@ module Ci
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
+ build.run_status_commit_hooks!
+
BuildFinishedWorker.perform_async(id)
end
end
@@ -963,8 +965,24 @@ module Ci
pending_state.try(:delete)
end
+ def run_on_status_commit(&block)
+ status_commit_hooks.push(block)
+ end
+
+ protected
+
+ def run_status_commit_hooks!
+ status_commit_hooks.reverse_each do |hook|
+ instance_eval(&hook)
+ end
+ end
+
private
+ def status_commit_hooks
+ @status_commit_hooks ||= []
+ end
+
def auto_retry
strong_memoize(:auto_retry) do
Gitlab::Ci::Build::AutoRetry.new(self)
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 45f323adec2..299c67f441d 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -9,4 +9,10 @@ class Ci::BuildPendingState < ApplicationRecord
enum failure_reason: CommitStatus.failure_reasons
validates :build, presence: true
+
+ def crc32
+ trace_checksum.try do |checksum|
+ checksum.to_s.split('crc32:').last.to_i(16)
+ end
+ end
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 444742062d9..55bcb8adaca 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -3,6 +3,7 @@
module Ci
class BuildTraceChunk < ApplicationRecord
extend ::Gitlab::Ci::Model
+ include ::Comparable
include ::FastDestroyAll
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
@@ -29,6 +30,7 @@ module Ci
}
scope :live, -> { redis }
+ scope :persisted, -> { not_redis.order(:chunk_index) }
class << self
def all_stores
@@ -63,12 +65,24 @@ module Ci
get_store_class(store).delete_keys(value)
end
end
+
+ ##
+ # Sometimes we do not want to read raw data. This method makes it easier
+ # to find attributes that are just metadata excluding raw data.
+ #
+ def metadata_attributes
+ attribute_names - %w[raw_data]
+ end
end
def data
@data ||= get_data.to_s
end
+ def crc32
+ checksum.to_i
+ end
+
def truncate(offset = 0)
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
return if offset == size # Skip the following process as it doesn't affect anything
@@ -126,8 +140,13 @@ module Ci
# no chunk with higher index in the database.
#
def final?
- build.pending_state.present? &&
- build.trace_chunks.maximum(:chunk_index).to_i == chunk_index
+ build.pending_state.present? && chunks_max_index == chunk_index
+ end
+
+ def <=>(other)
+ return unless self.build_id == other.build_id
+
+ self.chunk_index <=> other.chunk_index
end
private
@@ -145,12 +164,19 @@ module Ci
current_size = current_data&.bytesize.to_i
unless current_size == CHUNK_SIZE || final?
- raise FailedToPersistDataError, 'Data is not fulfilled in a bucket'
+ raise FailedToPersistDataError, <<~MSG
+ data is not fulfilled in a bucket
+
+ size: #{current_size}
+ state: #{build.pending_state.present?}
+ max: #{chunks_max_index}
+ index: #{chunk_index}
+ MSG
end
self.raw_data = nil
self.data_store = new_store
- self.checksum = crc32(current_data)
+ self.checksum = self.class.crc32(current_data)
##
# We need to so persist data then save a new store identifier before we
@@ -203,6 +229,10 @@ module Ci
self.class.get_store_class(data_store)
end
+ def chunks_max_index
+ build.trace_chunks.maximum(:chunk_index).to_i
+ end
+
def lock_params
["trace_write:#{build_id}:chunks:#{chunk_index}",
{ ttl: WRITE_LOCK_TTL,
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
index ea8072099c6..7448afba4c2 100644
--- a/app/models/ci/build_trace_chunks/database.rb
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -17,6 +17,8 @@ module Ci
def data(model)
model.raw_data
+ rescue ActiveModel::MissingAttributeError
+ model.reset.raw_data
end
def set_data(model, new_data)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 47eba685afe..f9a55fa9157 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -42,6 +42,7 @@ module Ci
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :latest_statuses, -> { latest }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
@@ -577,11 +578,11 @@ module Ci
end
def retried
- @retried ||= (statuses.order(id: :desc) - statuses.latest)
+ @retried ||= (statuses.order(id: :desc) - latest_statuses)
end
def coverage
- coverage_array = statuses.latest.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
@@ -875,7 +876,7 @@ module Ci
end
def builds_with_coverage
- builds.with_coverage
+ builds.latest.with_coverage
end
def has_reports?(reports_scope)
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index f0b3c11ba1d..d07ea7b71dc 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.20.2'
+ VERSION = '0.21.1'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 7af78960e35..b85a902d58b 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -11,6 +11,7 @@ module Clusters
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
self.table_name = 'cluster_platforms_kubernetes'
+ self.reactive_cache_work_type = :external_dependency
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
@@ -101,7 +102,7 @@ module Clusters
def terminals(environment, data)
pods = filter_by_project_environment(data[:pods], environment.project.full_path_slug, environment.slug)
terminals = pods.flat_map { |pod| terminals_for_pod(api_url, environment.deployment_namespace, pod) }.compact
- terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
+ terminals.each { |terminal| add_terminal_auth(terminal, **terminal_auth) }
end
def kubeclient
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5e0fceb23a4..5fe1b451ccd 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -29,12 +29,6 @@ class Commit
delegate :repository, to: :container
delegate :project, to: :repository, allow_nil: true
- DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
-
- # Commits above this size will not be rendered in HTML
- DIFF_HARD_LIMIT_FILES = 1000
- DIFF_HARD_LIMIT_LINES = 50000
-
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze
@@ -80,10 +74,30 @@ class Commit
sha[0..MIN_SHA_LENGTH]
end
- def max_diff_options
+ def diff_safe_lines
+ Gitlab::Git::DiffCollection.default_limits[:max_lines]
+ end
+
+ def diff_hard_limit_files(project: nil)
+ if Feature.enabled?(:increased_diff_limits, project)
+ 2000
+ else
+ 1000
+ end
+ end
+
+ def diff_hard_limit_lines(project: nil)
+ if Feature.enabled?(:increased_diff_limits, project)
+ 75000
+ else
+ 50000
+ end
+ end
+
+ def max_diff_options(project: nil)
{
- max_files: DIFF_HARD_LIMIT_FILES,
- max_lines: DIFF_HARD_LIMIT_LINES
+ max_files: diff_hard_limit_files(project: project),
+ max_lines: diff_hard_limit_lines(project: project)
}
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2f0596c93cc..b0169d6290a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -204,8 +204,13 @@ class CommitStatus < ApplicationRecord
# 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
- # 'rspec:linux: [aws, max memory]' => 'rspec:linux'
- common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
+ if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled?
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
+ common_name.gsub!(%r{: \[.*\]\s*\z}, '')
+ else
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]'
+ common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
+ end
common_name.strip!
common_name
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index d07c4ec43ac..c2d94b50f8d 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -2,10 +2,34 @@
module ApprovableBase
extend ActiveSupport::Concern
+ include FromUnion
included do
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approved_by_users, through: :approvals, source: :user
+
+ scope :without_approvals, -> { left_outer_joins(:approvals).where(approvals: { id: nil }) }
+ scope :with_approvals, -> { joins(:approvals) }
+ scope :approved_by_users_with_ids, -> (*user_ids) do
+ with_approvals
+ .merge(Approval.with_user)
+ .where(users: { id: user_ids })
+ .group(:id)
+ .having("COUNT(users.id) = ?", user_ids.size)
+ end
+ scope :approved_by_users_with_usernames, -> (*usernames) do
+ with_approvals
+ .merge(Approval.with_user)
+ .where(users: { username: usernames })
+ .group(:id)
+ .having("COUNT(users.id) = ?", usernames.size)
+ end
+ end
+
+ class_methods do
+ def select_from_union(relations)
+ where(id: from_union(relations))
+ end
end
def approved_by?(user)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 0dd55ab67b5..92926620f8c 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -3,16 +3,11 @@
module Avatarable
extend ActiveSupport::Concern
- ALLOWED_IMAGE_SCALER_WIDTHS = [
- 400,
- 200,
- 64,
- 48,
- 40,
- 26,
- 20,
- 16
- ].freeze
+ USER_AVATAR_SIZES = [16, 20, 23, 24, 26, 32, 36, 38, 40, 48, 60, 64, 96, 120, 160].freeze
+ PROJECT_AVATAR_SIZES = [15, 40, 48, 64, 88].freeze
+ GROUP_AVATAR_SIZES = [15, 37, 38, 39, 40, 64, 96].freeze
+
+ ALLOWED_IMAGE_SCALER_WIDTHS = (USER_AVATAR_SIZES | PROJECT_AVATAR_SIZES | GROUP_AVATAR_SIZES).freeze
included do
prepend ShadowMethods
diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb
index d6d17bfc604..056abafd0ce 100644
--- a/app/models/concerns/checksummable.rb
+++ b/app/models/concerns/checksummable.rb
@@ -3,11 +3,11 @@
module Checksummable
extend ActiveSupport::Concern
- def crc32(data)
- Zlib.crc32(data)
- end
-
class_methods do
+ def crc32(data)
+ Zlib.crc32(data)
+ end
+
def hexdigest(path)
::Digest::SHA256.file(path).hexdigest
end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index a5c7393e8f7..b468415c4c7 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -20,6 +20,14 @@
# To increment the counter we can use the method:
# delayed_increment_counter(:commit_count, 3)
#
+# It is possible to register callbacks to be executed after increments have
+# been flushed to the database. Callbacks are not executed if there are no increments
+# to flush.
+#
+# counter_attribute_after_flush do |statistic|
+# Namespaces::ScheduleAggregationWorker.perform_async(statistic.namespace_id)
+# end
+#
module CounterAttribute
extend ActiveSupport::Concern
extend AfterCommitQueue
@@ -48,6 +56,15 @@ module CounterAttribute
def counter_attributes
@counter_attributes ||= Set.new
end
+
+ def after_flush_callbacks
+ @after_flush_callbacks ||= []
+ end
+
+ # perform registered callbacks after increments have been flushed to the database
+ def counter_attribute_after_flush(&callback)
+ after_flush_callbacks << callback
+ end
end
# This method must only be called by FlushCounterIncrementsWorker
@@ -75,6 +92,8 @@ module CounterAttribute
unsafe_update_counters(id, attribute => increment_value)
redis_state { |redis| redis.del(flushed_key) }
end
+
+ execute_after_flush_callbacks
end
end
@@ -108,13 +127,13 @@ module CounterAttribute
counter_key(attribute) + ':lock'
end
- private
-
def counter_attribute_enabled?(attribute)
Feature.enabled?(:efficient_counter_attribute, project) &&
self.class.counter_attributes.include?(attribute)
end
+ private
+
def steal_increments(increment_key, flushed_key)
redis_state do |redis|
redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
@@ -129,6 +148,12 @@ module CounterAttribute
self.class.update_counters(id, increments)
end
+ def execute_after_flush_callbacks
+ self.class.after_flush_callbacks.each do |callback|
+ callback.call(self)
+ end
+ end
+
def redis_state(&block)
Gitlab::Redis::SharedState.with(&block)
end
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb
index 34ff5bb1195..9d446841a9f 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/integration.rb
@@ -16,7 +16,7 @@ module Integration
Project.where(id: custom_integration_project_ids)
end
- def ids_without_integration(integration, limit)
+ def without_integration(integration)
services = Service
.select('1')
.where('services.project_id = projects.id')
@@ -26,8 +26,6 @@ module Integration
.where('NOT EXISTS (?)', services)
.where(pending_delete: false)
.where(archived: false)
- .limit(limit)
- .pluck(:id)
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 888e1b384a2..7624a1a4e80 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -182,7 +182,7 @@ module Issuable
end
def supports_time_tracking?
- is_a?(TimeTrackable) && !incident?
+ is_a?(TimeTrackable)
end
def supports_severity?
@@ -203,15 +203,6 @@ module Issuable
issuable_severity&.severity || IssuableSeverity::DEFAULT
end
- def update_severity(severity)
- return unless incident?
-
- severity = severity.to_s.downcase
- severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity)
-
- (issuable_severity || build_issuable_severity(issue_id: id)).update(severity: severity)
- end
-
private
def description_max_length_for_new_records_is_valid
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
new file mode 100644
index 00000000000..6efb8103b7b
--- /dev/null
+++ b/app/models/concerns/issue_available_features.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Verifies features availability based on issue type.
+# This can be used, for example, for hiding UI elements or blocking specific
+# quick actions for particular issue types;
+module IssueAvailableFeatures
+ extend ActiveSupport::Concern
+
+ # EE only features are listed on EE::IssueAvailableFeatures
+ def available_features_for_issue_types
+ {}.with_indifferent_access
+ end
+
+ def issue_type_supports?(feature)
+ unless available_features_for_issue_types.has_key?(feature)
+ raise ArgumentError, 'invalid feature'
+ end
+
+ available_features_for_issue_types[feature].include?(issue_type)
+ end
+end
+
+IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 7b4485376d4..b10e8547e86 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -81,13 +81,6 @@ module Mentionable
end
def store_mentions!
- # if store_mentioned_users_to_db feature flag is not enabled then consider storing operation as succeeded
- # because we wrap this method in transaction with with_transaction_returning_status, and we need the status to be
- # successful if mentionable.save is successful.
- #
- # This line will get removed when we remove the feature flag.
- return true unless store_mentioned_users_to_db_enabled?
-
refs = all_references(self.author)
references = {}
@@ -253,15 +246,6 @@ module Mentionable
def model_user_mention
user_mentions.where(note_id: nil).first_or_initialize
end
-
- # We need this method to be checking that store_mentioned_users_to_db feature flag is enabled at the group level
- # and not the project level as epics are defined at group level and we want to have epics store user mentions as well
- # for the test period.
- # During the test period the flag should be enabled at the group level.
- def store_mentioned_users_to_db_enabled?
- return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group, default_enabled: true) if self.respond_to?(:project)
- return Feature.enabled?(:store_mentioned_users_to_db, self.group, default_enabled: true) if self.respond_to?(:group)
- end
end
Mentionable.prepend_if_ee('EE::Mentionable')
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 5f30fc0c36c..26f1544103c 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -9,7 +9,7 @@ module ReactiveCaching
ExceededReactiveCacheLimit = Class.new(StandardError)
WORK_TYPE = {
- default: ReactiveCachingWorker,
+ no_dependency: ReactiveCachingWorker,
external_dependency: ExternalServiceReactiveCachingWorker
}.freeze
@@ -30,7 +30,6 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_hard_limit = nil # this value should be set in megabytes. E.g: 1.megabyte
- self.reactive_cache_work_type = :default
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
index af69da24994..c444f238944 100644
--- a/app/models/concerns/reactive_service.rb
+++ b/app/models/concerns/reactive_service.rb
@@ -8,5 +8,6 @@ module ReactiveService
# Default cache key: class name + project_id
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
+ self.reactive_cache_work_type = :external_dependency
end
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 40edd3b3ead..9a17131c91c 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -85,7 +85,7 @@ module Referable
\/#{route.is_a?(Regexp) ? route : Regexp.escape(route)}
\/#{pattern}
(?<path>
- (\/[a-z0-9_=-]+)*
+ (\/[a-z0-9_=-]+)*\/*
)?
(?<query>
\?[a-z0-9_=-]+
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 3cbc174536c..b62eb50840d 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -200,4 +200,10 @@ module RelativePositioning
# Override if you want to be notified of failures to move
def could_not_move(exception)
end
+
+ # Override if the implementing class is not a simple application record, for
+ # example if the record is loaded from a union.
+ def reset_relative_position
+ reset.relative_position
+ end
end
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index a7028e18451..586f1dbb65c 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -80,10 +80,7 @@ module UpdateProjectStatistics
run_after_commit do
ProjectStatistics.increment_statistic(
- project_id, self.class.project_statistics_name, delta)
-
- Namespaces::ScheduleAggregationWorker.perform_async(
- project.namespace_id)
+ project, self.class.project_statistics_name, delta)
end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index b0f7edac2f3..d97b8776085 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -107,6 +107,14 @@ class ContainerRepository < ApplicationRecord
client.delete_repository_tag_by_name(self.path, name)
end
+ def reset_expiration_policy_started_at!
+ update!(expiration_policy_started_at: nil)
+ end
+
+ def start_expiration_policy!
+ update!(expiration_policy_started_at: Time.zone.now)
+ end
+
def self.build_from_path(path)
self.new(project: path.repository_project,
name: path.repository_name)
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
index 2cee3447886..adad8e3013e 100644
--- a/app/models/data_list.rb
+++ b/app/models/data_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class DataList
- def initialize(batch_ids, data_fields_hash, klass)
- @batch_ids = batch_ids
+ def initialize(batch, data_fields_hash, klass)
+ @batch = batch
@data_fields_hash = data_fields_hash
@klass = klass
end
@@ -13,15 +13,15 @@ class DataList
private
- attr_reader :batch_ids, :data_fields_hash, :klass
+ attr_reader :batch, :data_fields_hash, :klass
def columns
data_fields_hash.keys << 'service_id'
end
def values
- batch_ids.map do |row|
- data_fields_hash.values << row['id']
+ batch.map do |record|
+ data_fields_hash.values << record['id']
end
end
end
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index 57bb250829d..62e4bd6cebc 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -167,6 +167,10 @@ module DesignManagement
end
end
+ def self.build_full_path(issue, design)
+ File.join(DesignManagement.designs_directory, "issue-#{issue.iid}", design.filename)
+ end
+
def to_ability_name
'design'
end
@@ -180,7 +184,7 @@ module DesignManagement
end
def full_path
- @full_path ||= File.join(DesignManagement.designs_directory, "issue-#{issue.iid}", filename)
+ @full_path ||= self.class.build_full_path(issue, self)
end
def diff_refs
@@ -224,6 +228,10 @@ module DesignManagement
!interloper.exists?
end
+ def notes_with_associations
+ notes.includes(:author)
+ end
+
private
def head_version
diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb
index c48b36588c9..6deba14a6ba 100644
--- a/app/models/design_management/design_collection.rb
+++ b/app/models/design_management/design_collection.rb
@@ -5,6 +5,7 @@ module DesignManagement
attr_reader :issue
delegate :designs, :project, to: :issue
+ delegate :empty?, to: :designs
state_machine :copy_state, initial: :ready, namespace: :copy do
after_transition any => any, do: :update_stored_copy_state!
diff --git a/app/models/environment.rb b/app/models/environment.rb
index cfdcb0499e6..f64776a6991 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -86,6 +86,7 @@ class Environment < ApplicationRecord
scope :with_rank, -> do
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
end
+ scope :for_id, -> (id) { where(id: id) }
state_machine :state, initial: :available do
event :start do
diff --git a/app/models/event.rb b/app/models/event.rb
index 92609144576..671def16151 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -242,6 +242,8 @@ class Event < ApplicationRecord
target if note?
end
+ # rubocop: disable Metrics/CyclomaticComplexity
+ # rubocop: disable Metrics/PerceivedComplexity
def action_name
if push_action?
push_action_name
@@ -267,10 +269,14 @@ class Event < ApplicationRecord
'updated'
elsif created_project_action?
created_project_action_name
+ elsif approved_action?
+ 'approved'
else
"opened"
end
end
+ # rubocop: enable Metrics/CyclomaticComplexity
+ # rubocop: enable Metrics/PerceivedComplexity
def target_iid
target.respond_to?(:iid) ? target.iid : target_id
@@ -323,14 +329,6 @@ class Event < ApplicationRecord
end
end
- def note_target_type
- if target.noteable_type.present?
- target.noteable_type.titleize
- else
- "Wall"
- end.downcase
- end
-
def body?
if push_action?
push_with_commits?
diff --git a/app/models/group.rb b/app/models/group.rb
index c0f145997cc..1dec831606b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -15,11 +15,10 @@ class Group < Namespace
include WithUploads
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
+ include EachBatch
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
- UpdateSharedRunnersError = Class.new(StandardError)
-
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -140,6 +139,15 @@ class Group < Namespace
end
end
+ def without_integration(integration)
+ services = Service
+ .select('1')
+ .where('services.group_id = namespaces.id')
+ .where(type: integration.type)
+
+ where('NOT EXISTS (?)', services)
+ end
+
private
def public_to_user_arel(user)
@@ -348,6 +356,7 @@ class Group < Namespace
end
group_hierarchy_members = GroupMember.active_without_invites_and_requests
+ .non_minimal_access
.where(source_id: source_ids)
GroupMember.from_union([group_hierarchy_members,
@@ -528,57 +537,26 @@ class Group < Namespace
preloader.preload(self, shared_with_group_links: [shared_with_group: :route])
end
- def shared_runners_allowed?
- shared_runners_enabled? || allow_descendants_override_disabled_shared_runners?
- end
-
- def parent_allows_shared_runners?
- return true unless has_parent?
+ def update_shared_runners_setting!(state)
+ raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state)
- parent.shared_runners_allowed?
- end
-
- def parent_enabled_shared_runners?
- return true unless has_parent?
-
- parent.shared_runners_enabled?
- end
-
- def enable_shared_runners!
- raise UpdateSharedRunnersError, 'Shared Runners disabled for the parent group' unless parent_enabled_shared_runners?
-
- update_column(:shared_runners_enabled, true)
- end
-
- def disable_shared_runners!
- group_ids = self_and_descendants
- return if group_ids.empty?
-
- Group.by_id(group_ids).update_all(shared_runners_enabled: false)
-
- all_projects.update_all(shared_runners_enabled: false)
+ case state
+ when 'disabled_and_unoverridable' then disable_shared_runners! # also disallows override
+ when 'disabled_with_override' then disable_shared_runners_and_allow_override!
+ when 'enabled' then enable_shared_runners! # set both to true
+ end
end
- def allow_descendants_override_disabled_shared_runners!
- raise UpdateSharedRunnersError, 'Shared Runners enabled' if shared_runners_enabled?
- raise UpdateSharedRunnersError, 'Group level shared Runners not allowed' unless parent_allows_shared_runners?
-
- update_column(:allow_descendants_override_disabled_shared_runners, true)
+ def default_owner
+ owners.first || parent&.default_owner || owner
end
- def disallow_descendants_override_disabled_shared_runners!
- raise UpdateSharedRunnersError, 'Shared Runners enabled' if shared_runners_enabled?
-
- group_ids = self_and_descendants
- return if group_ids.empty?
-
- Group.by_id(group_ids).update_all(allow_descendants_override_disabled_shared_runners: false)
-
- all_projects.update_all(shared_runners_enabled: false)
+ def access_level_roles
+ GroupMember.access_level_roles
end
- def default_owner
- owners.first || parent&.default_owner || owner
+ def access_level_values
+ access_level_roles.values
end
private
@@ -658,6 +636,45 @@ class Group < Namespace
.new(Group.where(id: group_ids))
.base_and_descendants
end
+
+ def disable_shared_runners!
+ update!(
+ shared_runners_enabled: false,
+ allow_descendants_override_disabled_shared_runners: false)
+
+ group_ids = descendants
+ unless group_ids.empty?
+ Group.by_id(group_ids).update_all(
+ shared_runners_enabled: false,
+ allow_descendants_override_disabled_shared_runners: false)
+ end
+
+ all_projects.update_all(shared_runners_enabled: false)
+ end
+
+ def disable_shared_runners_and_allow_override!
+ # enabled -> disabled_with_override
+ if shared_runners_enabled?
+ update!(
+ shared_runners_enabled: false,
+ allow_descendants_override_disabled_shared_runners: true)
+
+ group_ids = descendants
+ unless group_ids.empty?
+ Group.by_id(group_ids).update_all(shared_runners_enabled: false)
+ end
+
+ all_projects.update_all(shared_runners_enabled: false)
+
+ # disabled_and_unoverridable -> disabled_with_override
+ else
+ update!(allow_descendants_override_disabled_shared_runners: true)
+ end
+ end
+
+ def enable_shared_runners!
+ update!(shared_runners_enabled: true)
+ end
end
Group.prepend_if_ee('EE::Group')
diff --git a/app/models/group_import_state.rb b/app/models/group_import_state.rb
index d22c1ac5550..89602e40357 100644
--- a/app/models/group_import_state.rb
+++ b/app/models/group_import_state.rb
@@ -4,8 +4,9 @@ class GroupImportState < ApplicationRecord
self.primary_key = :group_id
belongs_to :group, inverse_of: :import_state
+ belongs_to :user, optional: false
- validates :group, :status, presence: true
+ validates :group, :status, :user, presence: true
validates :jid, presence: true, if: -> { started? || finished? }
state_machine :status, initial: :created do
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
index d68b3dc48ee..35d03a544bd 100644
--- a/app/models/issuable_severity.rb
+++ b/app/models/issuable_severity.rb
@@ -2,6 +2,13 @@
class IssuableSeverity < ApplicationRecord
DEFAULT = 'unknown'
+ SEVERITY_LABELS = {
+ unknown: 'Unknown',
+ low: 'Low - S4',
+ medium: 'Medium - S3',
+ high: 'High - S2',
+ critical: 'Critical - S1'
+ }.freeze
belongs_to :issue
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5a5de371301..621b1a83b82 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -19,6 +19,8 @@ class Issue < ApplicationRecord
include WhereComposite
include StateEventable
include IdInOrdered
+ include Presentable
+ include IssueAvailableFeatures
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -54,6 +56,7 @@ class Issue < ApplicationRecord
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
+ has_many :issue_email_participants
has_many :assignees, class_name: "User", through: :issue_assignees
has_many :zoom_meetings
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -101,6 +104,8 @@ class Issue < ApplicationRecord
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
+ scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
+ scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_web_entity_associations, -> { preload(:author, :project) }
@@ -122,6 +127,7 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
+ scope :inc_relations_for_view, -> { includes(author: :status) }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
@@ -145,6 +151,7 @@ class Issue < ApplicationRecord
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
+ after_create_commit :record_create_action, unless: :importing?
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -232,6 +239,8 @@ class Issue < ApplicationRecord
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
+ when 'severity_asc' then order_severity_asc.with_order_id_desc
+ when 'severity_desc' then order_severity_desc.with_order_id_desc
else
super
end
@@ -413,6 +422,10 @@ class Issue < ApplicationRecord
IssueLink.inverse_link_type(type)
end
+ def relocation_target
+ moved_to || duplicated_to
+ end
+
private
def ensure_metrics
@@ -420,6 +433,10 @@ class Issue < ApplicationRecord
metrics.record!
end
+ def record_create_action
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author)
+ end
+
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
diff --git a/app/models/issue_email_participant.rb b/app/models/issue_email_participant.rb
new file mode 100644
index 00000000000..8eb9b6a8152
--- /dev/null
+++ b/app/models/issue_email_participant.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class IssueEmailParticipant < ApplicationRecord
+ belongs_to :issue
+
+ validates :email, presence: true, uniqueness: { scope: [:issue_id] }
+ validates :issue, presence: true
+ validate :validate_email_format
+
+ def validate_email_format
+ self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
+ end
+end
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index d223c80fca0..bd245de411c 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -94,13 +94,25 @@ class Iteration < ApplicationRecord
private
+ def parent_group
+ group || project.group
+ end
+
def start_or_due_dates_changed?
start_date_changed? || due_date_changed?
end
- # ensure dates do not overlap with other Iterations in the same group/project
+ # ensure dates do not overlap with other Iterations in the same group/project tree
def dates_do_not_overlap
- return unless resource_parent.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
+ iterations = if parent_group.present? && resource_parent.is_a?(Project)
+ Iteration.where(group: parent_group.self_and_ancestors).or(project.iterations)
+ elsif parent_group.present?
+ Iteration.where(group: parent_group.self_and_ancestors)
+ else
+ project.iterations
+ end
+
+ return unless iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations"))
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 7ea9caa45d3..498e03b2c1a 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -80,7 +80,10 @@ class Member < ApplicationRecord
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
- scope :not_accepted_invitations_by_user, -> (user) { invite.where(invite_accepted_at: nil, created_by: user) }
+ scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
+ scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
+ scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
+ scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
scope :has_access, -> { active.where('access_level > 0') }
@@ -372,6 +375,14 @@ class Member < ApplicationRecord
send_invite
end
+ def send_invitation_reminder(reminder_index)
+ return unless invite?
+
+ generate_invite_token! unless @raw_invite_token
+
+ run_after_commit_or_now { notification_service.invite_member_reminder(self, @raw_invite_token, reminder_index) }
+ end
+
def create_notification_setting
user.notification_settings.find_or_create_for(source)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 3fdc501644d..22c6777fca3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -31,6 +31,7 @@ class MergeRequest < ApplicationRecord
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
self.reactive_cache_lifetime = 10.minutes
+ self.reactive_cache_work_type = :no_dependency
SORTING_PREFERENCE_FIELD = :merge_requests_sort
@@ -121,6 +122,8 @@ class MergeRequest < ApplicationRecord
# when creating new merge request
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
+ participant :reviewers
+
# Keep states definition to be evaluated before the state_machine block to avoid spec failures.
# If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil.
def self.available_state_names
@@ -255,11 +258,7 @@ class MergeRequest < ApplicationRecord
scope :join_project, -> { joins(:target_project) }
scope :join_metrics, -> do
query = joins(:metrics)
-
- if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
- query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
- end
-
+ query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
query
end
scope :references_project, -> { references(:target_project) }
@@ -271,6 +270,8 @@ class MergeRequest < ApplicationRecord
metrics: [:latest_closed_by, :merged_by])
}
+ scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
+
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
end
@@ -629,7 +630,7 @@ class MergeRequest < ApplicationRecord
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
- merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size
+ merge_request_diff&.real_size || diff_stats&.real_size(project: project) || diffs.real_size
end
def modified_paths(past_merge_request_diff: nil, fallback_on_overflow: false)
@@ -1301,6 +1302,14 @@ class MergeRequest < ApplicationRecord
unlock_mr
end
+ def update_and_mark_in_progress_merge_commit_sha(commit_id)
+ self.update(in_progress_merge_commit_sha: commit_id)
+ # Since another process checks for matching merge request, we need
+ # to make it possible to detect whether the query should go to the
+ # primary.
+ target_project.mark_primary_write_location
+ end
+
def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
@@ -1375,8 +1384,6 @@ class MergeRequest < ApplicationRecord
end
def has_coverage_reports?
- return false unless Feature.enabled?(:coverage_report_view, project, default_enabled: true)
-
actual_head_pipeline&.has_coverage_reports?
end
@@ -1511,6 +1518,7 @@ class MergeRequest < ApplicationRecord
metrics&.merged_at ||
merge_event&.created_at ||
+ resource_state_events.find_by(state: :merged)&.created_at ||
notes.system.reorder(nil).find_by(note: 'merged')&.created_at
end
end
@@ -1680,6 +1688,10 @@ class MergeRequest < ApplicationRecord
Feature.enabled?(:merge_request_reviewers, project)
end
+ def allows_multiple_reviewers?
+ false
+ end
+
private
def with_rebase_lock
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index a2982a5dd73..59cc82cfaf5 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -22,8 +22,8 @@ class MergeRequestContextCommit < ApplicationRecord
end
# create MergeRequestContextCommit by given commit sha and it's diff file record
- def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert
+ def self.bulk_insert(rows, **args)
+ Gitlab::Database.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 880e3cc1ba5..3cf0db9403d 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -509,6 +509,8 @@ class MergeRequestDiff < ApplicationRecord
end
def encode_in_base64?(diff_text)
+ return false if diff_text.nil?
+
(diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?) ||
diff_text.include?("\0")
end
@@ -536,7 +538,7 @@ class MergeRequestDiff < ApplicationRecord
rows.each do |row|
data = row.delete(:diff)
row[:external_diff_offset] = file.pos
- row[:external_diff_size] = data.bytesize
+ row[:external_diff_size] = data&.bytesize || 0
file.write(data)
end
@@ -651,7 +653,7 @@ class MergeRequestDiff < ApplicationRecord
if compare.commits.empty?
new_attributes[:state] = :empty
else
- diff_collection = compare.diffs(Commit.max_diff_options)
+ diff_collection = compare.diffs(Commit.max_diff_options(project: merge_request.project))
new_attributes[:real_size] = diff_collection.real_size
if diff_collection.any?
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 527fa9d52d0..f0550713d01 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -18,6 +18,8 @@ class Namespace < ApplicationRecord
# Android repo (15) + some extra backup.
NUMBER_OF_ANCESTORS_ALLOWED = 20
+ SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
+
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -59,6 +61,8 @@ class Namespace < ApplicationRecord
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
validate :nesting_level_allowed
+ validate :changing_shared_runners_enabled_is_allowed
+ validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed
validates_associated :runners
@@ -378,6 +382,52 @@ class Namespace < ApplicationRecord
actual_plan.name
end
+ def changing_shared_runners_enabled_is_allowed
+ return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
+ return unless new_record? || changes.has_key?(:shared_runners_enabled)
+
+ if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
+ errors.add(:shared_runners_enabled, _('cannot be enabled because parent group has shared Runners disabled'))
+ end
+ end
+
+ def changing_allow_descendants_override_disabled_shared_runners_is_allowed
+ return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
+ return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
+
+ if shared_runners_enabled && !new_record?
+ errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled'))
+ end
+
+ if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
+ errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be enabled because parent group does not allow it'))
+ end
+ end
+
+ def shared_runners_setting
+ if shared_runners_enabled
+ 'enabled'
+ else
+ if allow_descendants_override_disabled_shared_runners
+ 'disabled_with_override'
+ else
+ 'disabled_and_unoverridable'
+ end
+ end
+ end
+
+ def shared_runners_setting_higher_than?(other_setting)
+ if other_setting == 'enabled'
+ false
+ elsif other_setting == 'disabled_with_override'
+ shared_runners_setting == 'enabled'
+ elsif other_setting == 'disabled_and_unoverridable'
+ shared_runners_setting == 'enabled' || shared_runners_setting == 'disabled_with_override'
+ else
+ raise ArgumentError
+ end
+ end
+
private
def all_projects_with_pages
diff --git a/app/models/note.rb b/app/models/note.rb
index 812d77d5f86..954843505d4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -322,8 +322,6 @@ class Note < ApplicationRecord
end
def contributor?
- return false unless ::Feature.enabled?(:show_contributor_on_note, project)
-
project&.team&.contributor?(self.author_id)
end
diff --git a/app/models/notification_reason.rb b/app/models/notification_reason.rb
index a7967239417..c227626af9e 100644
--- a/app/models/notification_reason.rb
+++ b/app/models/notification_reason.rb
@@ -5,6 +5,7 @@
class NotificationReason
OWN_ACTIVITY = 'own_activity'
ASSIGNED = 'assigned'
+ REVIEW_REQUESTED = 'review_requested'
MENTIONED = 'mentioned'
SUBSCRIBED = 'subscribed'
@@ -12,6 +13,7 @@ class NotificationReason
REASON_PRIORITY = [
OWN_ACTIVITY,
ASSIGNED,
+ REVIEW_REQUESTED,
MENTIONED,
SUBSCRIBED
].freeze
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 6a6b2bb1b58..79a84231083 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -5,7 +5,7 @@ class NotificationRecipient
attr_reader :user, :type, :reason
- def initialize(user, type, **opts)
+ def initialize(user, type, opts = {})
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index c003a20f0fc..6066046a722 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -43,6 +43,7 @@ class NotificationSetting < ApplicationRecord
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
+ :change_reviewer_merge_request,
:merge_merge_request,
:failed_pipeline,
:fixed_pipeline,
diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb
index ff68af9741e..c70e10c72d5 100644
--- a/app/models/operations/feature_flags/strategy.rb
+++ b/app/models/operations/feature_flags/strategy.rb
@@ -6,14 +6,17 @@ module Operations
STRATEGY_DEFAULT = 'default'
STRATEGY_GITLABUSERLIST = 'gitlabUserList'
STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'
+ STRATEGY_FLEXIBLEROLLOUT = 'flexibleRollout'
STRATEGY_USERWITHID = 'userWithId'
STRATEGIES = {
STRATEGY_DEFAULT => [].freeze,
STRATEGY_GITLABUSERLIST => [].freeze,
STRATEGY_GRADUALROLLOUTUSERID => %w[groupId percentage].freeze,
+ STRATEGY_FLEXIBLEROLLOUT => %w[groupId rollout stickiness].freeze,
STRATEGY_USERWITHID => ['userIds'].freeze
}.freeze
USERID_MAX_LENGTH = 256
+ STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze
self.table_name = 'operations_strategies'
@@ -67,16 +70,25 @@ module Operations
case name
when STRATEGY_GRADUALROLLOUTUSERID
gradual_rollout_user_id_parameters_validation
+ when STRATEGY_FLEXIBLEROLLOUT
+ flexible_rollout_parameters_validation
when STRATEGY_USERWITHID
FeatureFlagUserXidsValidator.validate_user_xids(self, :parameters, parameters['userIds'], 'userIds')
end
end
+ def within_range?(value, min, max)
+ return false unless value.is_a?(String)
+ return false unless value.match?(/\A\d+\z/)
+
+ value.to_i.between?(min, max)
+ end
+
def gradual_rollout_user_id_parameters_validation
percentage = parameters['percentage']
group_id = parameters['groupId']
- unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/)
+ unless within_range?(percentage, 0, 100)
parameters_error('percentage must be a string between 0 and 100 inclusive')
end
@@ -85,6 +97,25 @@ module Operations
end
end
+ def flexible_rollout_parameters_validation
+ stickiness = parameters['stickiness']
+ group_id = parameters['groupId']
+ rollout = parameters['rollout']
+
+ unless STICKINESS_SETTINGS.include?(stickiness)
+ options = STICKINESS_SETTINGS.to_sentence(last_word_connector: ', or ')
+ parameters_error("stickiness parameter must be #{options}")
+ end
+
+ unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/)
+ parameters_error('groupId parameter is invalid')
+ end
+
+ unless within_range?(rollout, 0, 100)
+ parameters_error('rollout must be a string between 0 and 100 inclusive')
+ end
+ end
+
def parameters_error(message)
errors.add(:parameters, message)
false
diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb
new file mode 100644
index 00000000000..730ce267273
--- /dev/null
+++ b/app/models/packages/event.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class Packages::Event < ApplicationRecord
+ belongs_to :package, optional: true
+
+ # FIXME: Remove debian: 9 from here when it's added to the types in package.rb model
+ EVENT_SCOPES = ::Packages::Package.package_types.merge(debian: 9, container: 1000, tag: 1001).freeze
+
+ enum event_scope: EVENT_SCOPES
+
+ enum event_type: {
+ push_package: 0,
+ delete_package: 1,
+ pull_package: 2,
+ search_package: 3,
+ list_package: 4,
+ list_repositories: 5,
+ delete_repository: 6,
+ delete_tag: 7,
+ delete_tag_bulk: 8,
+ list_tags: 9,
+ cli_metadata: 10
+ }
+
+ enum originator_type: { user: 0, deploy_token: 1, guest: 2 }
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index bda11160957..caf2522e3dd 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -26,7 +26,7 @@ class Packages::Package < ApplicationRecord
validates :project, presence: true
validates :name, presence: true
- validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: :conan?
+ validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: -> { conan? || generic? }
validates :name,
uniqueness: { scope: %i[project_id version package_type] }, unless: :conan?
@@ -35,17 +35,19 @@ class Packages::Package < ApplicationRecord
validate :valid_npm_package_name, if: :npm?
validate :valid_composer_global_name, if: :composer?
validate :package_already_taken, if: :npm?
- validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? }
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
+ validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
+ validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? }
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
+ validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang?
validates :version,
presence: true,
format: { with: Gitlab::Regex.generic_package_version_regex },
if: :generic?
- enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7 }
+ enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7, golang: 8 }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
@@ -119,6 +121,10 @@ class Packages::Package < ApplicationRecord
.where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last!
end
+ def self.by_name_and_version!(name, version)
+ find_by!(name: name, version: version)
+ end
+
def self.pluck_names
pluck(:name)
end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index 78e0f185a11..d8f122cfb23 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -2,10 +2,14 @@
# PagesDeployment stores a zip archive containing GitLab Pages web-site
class PagesDeployment < ApplicationRecord
+ include FileStoreMounter
+
belongs_to :project, optional: false
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
validates :size, presence: true, numericality: { greater_than: 0, only_integer: true }
+
+ mount_file_store_uploader ::Pages::DeploymentUploader
end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index a4370eda5ba..c96786423e5 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -22,8 +22,8 @@ module Postgresql
def self.lag_too_great?(max = 100.megabytes)
return false unless in_use?
- lag_function = "#{Gitlab::Database.pg_wal_lsn_diff}" \
- "(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)::bigint"
+ lag_function = "pg_wal_lsn_diff" \
+ "(pg_current_wal_insert_lsn(), restart_lsn)::bigint"
# We force the use of a transaction here so the query always goes to the
# primary, even when using the EE DB load balancer.
diff --git a/app/models/project.rb b/app/models/project.rb
index 4db0eaa0442..52cad50809f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -33,6 +33,7 @@ class Project < ApplicationRecord
include FromUnion
include IgnorableColumns
include Integration
+ include EachBatch
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -198,6 +199,7 @@ class Project < ApplicationRecord
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :export_jobs, class_name: 'ProjectExportJob'
has_one :project_repository, inverse_of: :project
+ has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
@@ -268,6 +270,7 @@ class Project < ApplicationRecord
has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :project
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
+ has_many :alert_management_http_integrations, class_name: 'AlertManagement::HttpIntegration', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -336,6 +339,8 @@ class Project < ApplicationRecord
has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :reviews, inverse_of: :project
+ has_many :terraform_states, class_name: 'Terraform::State', inverse_of: :project
+
# GitLab Pages
has_many :pages_domains
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
@@ -432,6 +437,7 @@ class Project < ApplicationRecord
validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
+ validate :changing_shared_runners_enabled_is_allowed
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
@@ -560,6 +566,7 @@ class Project < ApplicationRecord
}
scope :imported_from, -> (type) { where(import_type: type) }
+ scope :with_tracing_enabled, -> { joins(:tracing_setting) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -1186,6 +1193,15 @@ class Project < ApplicationRecord
end
end
+ def changing_shared_runners_enabled_is_allowed
+ return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
+ return unless new_record? || changes.has_key?(:shared_runners_enabled)
+
+ if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
+ errors.add(:shared_runners_enabled, _('cannot be enabled because parent group does not allow it'))
+ end
+ end
+
def to_param
if persisted? && errors.include?(:path)
path_was
@@ -2292,6 +2308,10 @@ class Project < ApplicationRecord
[]
end
+ def mark_primary_write_location
+ # Overriden in EE
+ end
+
def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute)
end
@@ -2501,6 +2521,15 @@ class Project < ApplicationRecord
GroupDeployKey.for_groups(group.self_and_ancestors_ids)
end
+ def feature_flags_client_token
+ instance = operations_feature_flags_client || create_operations_feature_flags_client!
+ instance.token
+ end
+
+ def tracing_external_url
+ tracing_setting&.external_url
+ end
+
private
def find_service(services, name)
@@ -2509,10 +2538,10 @@ class Project < ApplicationRecord
def build_from_instance_or_template(name)
instance = find_service(services_instances, name)
- return Service.build_from_integration(id, instance) if instance
+ return Service.build_from_integration(instance, project_id: id) if instance
template = find_service(services_templates, name)
- return Service.build_from_integration(id, template) if template
+ return Service.build_from_integration(template, project_id: id) if template
end
def services_templates
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 4e4955b45d8..5a49f780d46 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -42,7 +42,7 @@ class DroneCiService < CiService
def commit_status_path(sha, ref)
Gitlab::Utils.append_path(
drone_url,
- "gitlab/#{project.full_path}/commits/#{sha}?branch=#{URI.encode(ref.to_s)}&access_token=#{token}")
+ "gitlab/#{project.full_path}/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}&access_token=#{token}")
end
def commit_status(sha, ref)
@@ -75,7 +75,7 @@ class DroneCiService < CiService
def build_page(sha, ref)
Gitlab::Utils.append_path(
drone_url,
- "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{URI.encode(ref.to_s)}")
+ "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
def title
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 67ab2c0ce8a..0d2f89fb18d 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -2,6 +2,7 @@
class ProjectStatistics < ApplicationRecord
include AfterCommitQueue
+ include CounterAttribute
belongs_to :project
belongs_to :namespace
@@ -9,6 +10,13 @@ class ProjectStatistics < ApplicationRecord
default_value_for :wiki_size, 0
default_value_for :snippets_size, 0
+ counter_attribute :build_artifacts_size
+ counter_attribute :storage_size
+
+ counter_attribute_after_flush do |project_statistic|
+ Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id)
+ end
+
before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze
@@ -29,6 +37,8 @@ class ProjectStatistics < ApplicationRecord
end
def refresh!(only: [])
+ return if Gitlab::Database.read_only?
+
COLUMNS_TO_REFRESH.each do |column, generator|
if only.empty? || only.include?(column)
public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
@@ -96,12 +106,27 @@ class ProjectStatistics < ApplicationRecord
# Additional columns are updated depending on key => [columns], which allows
# to update statistics which are and also those which aren't included in storage_size
# or any other additional summary column in the future.
- def self.increment_statistic(project_id, key, amount)
+ def self.increment_statistic(project, key, amount)
raise ArgumentError, "Cannot increment attribute: #{key}" unless INCREMENTABLE_COLUMNS.key?(key)
return if amount == 0
- where(project_id: project_id)
- .columns_to_increment(key, amount)
+ project.statistics.try do |project_statistics|
+ if project_statistics.counter_attribute_enabled?(key)
+ statistics_to_increment = [key] + INCREMENTABLE_COLUMNS[key].to_a
+ statistics_to_increment.each do |statistic|
+ project_statistics.delayed_increment_counter(statistic, amount)
+ end
+ else
+ legacy_increment_statistic(project, key, amount)
+ end
+ end
+ end
+
+ def self.legacy_increment_statistic(project, key, amount)
+ where(project_id: project.id).columns_to_increment(key, amount)
+
+ Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker
+ project.namespace_id)
end
def self.columns_to_increment(key, amount)
diff --git a/app/models/project_tracing_setting.rb b/app/models/project_tracing_setting.rb
new file mode 100644
index 00000000000..93fa80aed67
--- /dev/null
+++ b/app/models/project_tracing_setting.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ProjectTracingSetting < ApplicationRecord
+ belongs_to :project
+
+ validates :external_url, length: { maximum: 255 }, public_url: true
+
+ before_validation :sanitize_external_url
+
+ private
+
+ def sanitize_external_url
+ self.external_url = Rails::Html::FullSanitizer.new.sanitize(self.external_url)
+ end
+end
diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb
index f0441d4a3cb..684f50d5f58 100644
--- a/app/models/prometheus_alert.rb
+++ b/app/models/prometheus_alert.rb
@@ -4,6 +4,7 @@ class PrometheusAlert < ApplicationRecord
include Sortable
include UsageStatistics
include Presentable
+ include EachBatch
OPERATORS_MAP = {
lt: "<",
@@ -35,6 +36,7 @@ class PrometheusAlert < ApplicationRecord
scope :for_metric, -> (metric) { where(prometheus_metric: metric) }
scope :for_project, -> (project) { where(project_id: project) }
scope :for_environment, -> (environment) { where(environment_id: environment) }
+ scope :get_environment_id, -> { select(:environment_id).pluck(:environment_id) }
def self.distinct_projects
sub_query = self.group(:project_id).select(1)
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index 9ddf66cd388..590eda62c11 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class PrometheusMetric < ApplicationRecord
+ include EachBatch
+
belongs_to :project, validate: true, inverse_of: :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :prometheus_metric
diff --git a/app/models/repository.rb b/app/models/repository.rb
index ef17e010ba8..6bc2ec24811 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -853,7 +853,7 @@ class Repository
def merge(user, source_sha, merge_request, message)
with_cache_hooks do
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
- merge_request.update(in_progress_merge_commit_sha: commit_id)
+ merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
nil # Return value does not matter.
end
end
@@ -873,7 +873,7 @@ class Repository
their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil?
- merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
+ merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id)
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index cc96698be09..18e2944a9ca 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -15,6 +15,7 @@ class ResourceLabelEvent < ResourceEvent
validate :exactly_one_issuable
after_save :expire_etag_cache
+ after_save :usage_metrics
after_destroy :expire_etag_cache
enum action: {
@@ -113,6 +114,16 @@ class ResourceLabelEvent < ResourceEvent
def discussion_id_key
[self.class.name, created_at, user_id]
end
+
+ def for_issue?
+ issue_id.present?
+ end
+
+ def usage_metrics
+ return unless for_issue?
+
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(author: user)
+ end
end
ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent')
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 1ce4e14d289..6475633868a 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -11,6 +11,8 @@ class ResourceStateEvent < ResourceEvent
# state is used for issue and merge request states.
enum state: Issue.available_states.merge(MergeRequest.available_states).merge(reopened: 5)
+ after_save :usage_metrics
+
def self.issuable_attrs
%i(issue merge_request).freeze
end
@@ -18,6 +20,29 @@ class ResourceStateEvent < ResourceEvent
def issuable
issue || merge_request
end
+
+ def for_issue?
+ issue_id.present?
+ end
+
+ private
+
+ def usage_metrics
+ return unless for_issue?
+
+ case state
+ when 'closed'
+ issue_usage_counter.track_issue_closed_action(author: user)
+ when 'reopened'
+ issue_usage_counter.track_issue_reopened_action(author: user)
+ else
+ # no-op, nothing to do, not a state we're tracking
+ end
+ end
+
+ def issue_usage_counter
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter
+ end
end
ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index 44f48915425..dbb2b428c7b 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -13,6 +13,8 @@ class ResourceTimeboxEvent < ResourceEvent
remove: 2
}
+ after_save :usage_metrics
+
def self.issuable_attrs
%i(issue merge_request).freeze
end
@@ -20,4 +22,17 @@ class ResourceTimeboxEvent < ResourceEvent
def issuable
issue || merge_request
end
+
+ private
+
+ def usage_metrics
+ case self
+ when ResourceMilestoneEvent
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user)
+ when ResourceIterationEvent
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_iteration_changed_action(author: user)
+ else
+ # no-op
+ end
+ end
end
diff --git a/app/models/resource_weight_event.rb b/app/models/resource_weight_event.rb
index bbabd54325e..a261f3a1bd7 100644
--- a/app/models/resource_weight_event.rb
+++ b/app/models/resource_weight_event.rb
@@ -1,7 +1,15 @@
# frozen_string_literal: true
class ResourceWeightEvent < ResourceEvent
+ include IssueResourceEvent
+
validates :issue, presence: true
- include IssueResourceEvent
+ after_save :usage_metrics
+
+ private
+
+ def usage_metrics
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_weight_changed_action(author: user)
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index e63e06bf46f..48fa62d3d46 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -7,9 +7,7 @@ class Service < ApplicationRecord
include Importable
include ProjectServicesLoggable
include DataFields
- include IgnorableColumns
-
- ignore_columns %i[default], remove_with: '13.5', remove_after: '2020-10-22'
+ include FromUnion
SERVICE_NAMES = %w[
alerts asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
@@ -65,6 +63,7 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) }
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
+ scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
scope :for_group, -> (group) { where(group_id: group, type: available_services_types) }
scope :for_template, -> { where(template: true, type: available_services_types) }
scope :for_instance, -> { where(instance: true, type: available_services_types) }
@@ -217,7 +216,7 @@ class Service < ApplicationRecord
services_names.map { |service_name| "#{service_name}_service".camelize }
end
- def self.build_from_integration(project_id, integration)
+ def self.build_from_integration(integration, project_id: nil, group_id: nil)
service = integration.dup
if integration.supports_data_fields?
@@ -227,8 +226,9 @@ class Service < ApplicationRecord
service.template = false
service.instance = false
- service.inherit_from_id = integration.id if integration.instance?
service.project_id = project_id
+ service.group_id = group_id
+ service.inherit_from_id = integration.id if integration.instance? || integration.group
service.active = false if service.invalid?
service
end
@@ -256,6 +256,19 @@ class Service < ApplicationRecord
end
private_class_method :instance_level_integration
+ def self.create_from_active_default_integrations(scope, association, with_templates: false)
+ group_ids = scope.ancestors.select(:id)
+ array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
+
+ from_union([
+ with_templates ? active.where(template: true) : none,
+ active.where(instance: true),
+ active.where(group_id: group_ids)
+ ]).order(Arel.sql("type ASC, array_position(#{array}::bigint[], services.group_id), instance DESC")).group_by(&:type).each do |type, records|
+ build_from_integration(records.first, association => scope.id).save!
+ end
+ end
+
def activated?
active
end
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
index 9cbc5e68059..5eca5f2bda1 100644
--- a/app/models/service_list.rb
+++ b/app/models/service_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class ServiceList
- def initialize(batch_ids, service_hash, association)
- @batch_ids = batch_ids
+ def initialize(batch, service_hash, association)
+ @batch = batch
@service_hash = service_hash
@association = association
end
@@ -13,15 +13,15 @@ class ServiceList
private
- attr_reader :batch_ids, :service_hash, :association
+ attr_reader :batch, :service_hash, :association
def columns
- (service_hash.keys << "#{association}_id")
+ service_hash.keys << "#{association}_id"
end
def values
- batch_ids.map do |id|
- (service_hash.values << id)
+ batch.select(:id).map do |record|
+ service_hash.values << record.id
end
end
end
diff --git a/app/models/snippet_input_action_collection.rb b/app/models/snippet_input_action_collection.rb
index 38313e3a980..1e886e98083 100644
--- a/app/models/snippet_input_action_collection.rb
+++ b/app/models/snippet_input_action_collection.rb
@@ -8,7 +8,11 @@ class SnippetInputActionCollection
delegate :empty?, :any?, :[], to: :actions
def initialize(actions = [], allowed_actions: nil)
- @actions = actions.map { |action| SnippetInputAction.new(action.merge(allowed_actions: allowed_actions)) }
+ @actions = actions.map do |action|
+ params = action.merge(allowed_actions: allowed_actions)
+
+ SnippetInputAction.new(**params)
+ end
end
def to_commit_actions
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 2cfb201191d..fa25a6f8441 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -12,7 +12,7 @@ class SnippetRepository < ApplicationRecord
belongs_to :snippet, inverse_of: :snippet_repository
- delegate :repository, to: :snippet
+ delegate :repository, :repository_storage, to: :snippet
class << self
def find_snippet(disk_path)
diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb
index 8545296d076..6fb6f0ef713 100644
--- a/app/models/snippet_statistics.rb
+++ b/app/models/snippet_statistics.rb
@@ -34,6 +34,8 @@ class SnippetStatistics < ApplicationRecord
end
def refresh!
+ return if Gitlab::Database.read_only?
+
update_commit_count
update_repository_size
update_file_count
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 961212d0295..0ddf2c5fbcd 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -18,9 +18,9 @@ class SystemNoteMetadata < ApplicationRecord
commit description merge confidential visible label assignee cross_reference
designs_added designs_modified designs_removed designs_discussion_added
title time_tracking branch milestone discussion task moved
- opened closed merged duplicate locked unlocked outdated
+ opened closed merged duplicate locked unlocked outdated reviewer
tag due_date pinned_embed cherry_pick health_status approved unapproved
- status alert_issue_added relate unrelate new_alert_added
+ status alert_issue_added relate unrelate new_alert_added severity
].freeze
validates :note, presence: true
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 419fffcb666..2ff2e3d66c0 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -4,6 +4,12 @@ module Terraform
class State < ApplicationRecord
include UsageStatistics
include FileStoreMounter
+ include IgnorableColumns
+ # These columns are being removed since geo replication falls to the versioned state
+ # Tracking in https://gitlab.com/gitlab-org/gitlab/-/issues/258262
+ ignore_columns %i[verification_failure verification_retry_at verified_at verification_retry_count verification_checksum],
+ remove_with: '13.7',
+ remove_after: '2020-12-22'
HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32
@@ -15,6 +21,7 @@ module Terraform
has_one :latest_version, -> { ordered_by_version_desc }, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
+ scope :ordered_by_name, -> { order(:name) }
validates :project_id, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
@@ -33,10 +40,6 @@ module Terraform
versioning_enabled ? latest_version&.file : file
end
- def local?
- file_store == ObjectStorage::Store::LOCAL
- end
-
def locked?
self.lock_xid.present?
end
@@ -53,5 +56,3 @@ module Terraform
end
end
end
-
-Terraform::State.prepend_if_ee('EE::Terraform::State')
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index d5e315d18a1..eff44485401 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -14,5 +14,11 @@ module Terraform
mount_file_store_uploader VersionedStateUploader
delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
+
+ def local?
+ file_store == ObjectStorage::Store::LOCAL
+ end
end
end
+
+Terraform::StateVersion.prepend_if_ee('EE::Terraform::StateVersion')
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 6c8e085762d..0d893b25253 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -227,7 +227,7 @@ class Todo < ApplicationRecord
end
def self_assigned?
- assigned? && self_added?
+ self_added? && (assigned? || review_requested?)
end
private
diff --git a/app/models/user.rb b/app/models/user.rb
index 0a784b30d8f..5bbdb2b2e9a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -64,14 +64,7 @@ class User < ApplicationRecord
# and should be added after Devise modules are initialized.
include AsyncDeviseEmail
- BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
- "administrator if you think this is an error."
- LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \
- "administrator if you think this is an error."
-
- MINIMUM_INACTIVE_DAYS = 180
-
- ignore_column :bio, remove_with: '13.4', remove_after: '2020-09-22'
+ MINIMUM_INACTIVE_DAYS = 90
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
@@ -134,6 +127,8 @@ class User < ApplicationRecord
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
source: :group
+ has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
+ has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
# Projects
has_many :groups_projects, through: :groups, source: :projects
@@ -381,11 +376,12 @@ class User < ApplicationRecord
super && can?(:log_in)
end
+ # The messages for these keys are defined in `devise.en.yml`
def inactive_message
if blocked?
- BLOCKED_MESSAGE
+ :blocked
elsif internal?
- LOGIN_FORBIDDEN
+ :forbidden
else
super
end
@@ -1676,6 +1672,8 @@ class User < ApplicationRecord
end
def terms_accepted?
+ return true if project_bot?
+
accepted_term_id.present?
end
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 0ba319aa444..e39ff8712fc 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -19,7 +19,7 @@ class UserCallout < ApplicationRecord
webhooks_moved: 13,
service_templates_deprecated: 14,
admin_integrations_moved: 15,
- web_ide_alert_dismissed: 16,
+ web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
personal_access_token_expiry: 21, # EE-only
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 1c615777018..7e7a387d3d4 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -21,7 +21,7 @@ class UserInteractedProject < ApplicationRecord
user_id: event.author_id
}
- cached_exists?(attributes) do
+ cached_exists?(**attributes) do
transaction(requires_new: true) do
where(attributes).select(1).first || create!(attributes)
true # not caching the whole record here for now
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index d3b3a46bf74..c05bc80415a 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -8,6 +8,9 @@ class UserPreference < ApplicationRecord
belongs_to :user
+ scope :with_user, -> { joins(:user) }
+ scope :gitpod_enabled, -> { where(gitpod_enabled: true) }
+
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
validates :tab_width, numericality: {
only_integer: true,
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 9462f7401c4..9114de0e965 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -103,10 +103,10 @@ class Wiki
limited = pages.size > limit
pages = pages.first(limit) if limited
- [WikiPage.group_by_directory(pages), limited]
+ [WikiDirectory.group_pages(pages), limited]
end
- # Finds a page within the repository based on a tile
+ # Finds a page within the repository based on a title
# or slug.
#
# title - The human readable or parameterized title of
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
index df2fe25b08b..3a2613e15d9 100644
--- a/app/models/wiki_directory.rb
+++ b/app/models/wiki_directory.rb
@@ -3,13 +3,46 @@
class WikiDirectory
include ActiveModel::Validations
- attr_accessor :slug, :pages
+ attr_accessor :slug, :entries
validates :slug, presence: true
- def initialize(slug, pages = [])
+ # Groups a list of wiki pages into a nested collection of WikiPage and WikiDirectory objects,
+ # preserving the order of the passed pages.
+ #
+ # Returns an array with all entries for the toplevel directory.
+ #
+ # @param [Array<WikiPage>] pages
+ # @return [Array<WikiPage, WikiDirectory>]
+ #
+ def self.group_pages(pages)
+ # Build a hash to map paths to created WikiDirectory objects,
+ # and recursively create them for each level of the path.
+ # For the toplevel directory we use '' as path, as that's what WikiPage#directory returns.
+ directories = Hash.new do |_, path|
+ directories[path] = new(path).tap do |directory|
+ if path.present?
+ parent = File.dirname(path)
+ parent = '' if parent == '.'
+ directories[parent].entries << directory
+ end
+ end
+ end
+
+ pages.each do |page|
+ directories[page.directory].entries << page
+ end
+
+ directories[''].entries
+ end
+
+ def initialize(slug, entries = [])
@slug = slug
- @pages = pages
+ @entries = entries
+ end
+
+ def title
+ WikiPage.unhyphenize(File.basename(slug))
end
# Relative path to the partial to be used when rendering collections
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index faf3d19d936..989128987d5 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -31,29 +31,6 @@ class WikiPage
alias_method :==, :eql?
- # Sorts and groups pages by directory.
- #
- # pages - an array of WikiPage objects.
- #
- # Returns an array of WikiPage and WikiDirectory objects. The entries are
- # sorted by alphabetical order (directories and pages inside each directory).
- # Pages at the root level come before everything.
- def self.group_by_directory(pages)
- return [] if pages.blank?
-
- pages.each_with_object([]) do |page, grouped_pages|
- next grouped_pages << page unless page.directory.present?
-
- directory = grouped_pages.find do |obj|
- obj.is_a?(WikiDirectory) && obj.slug == page.directory
- end
-
- next directory.pages << page if directory
-
- grouped_pages << WikiDirectory.new(page.directory, [page])
- end
- end
-
def self.unhyphenize(name)
name.gsub(/-+/, ' ')
end