diff options
Diffstat (limited to 'app/models')
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 |