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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-20 02:18:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-20 02:18:09 +0300
commit6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch)
treedc4d20fe6064752c0bd323187252c77e0a89144b /app/models
parent9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff)
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/active_session.rb8
-rw-r--r--app/models/application_setting.rb17
-rw-r--r--app/models/ci/bridge.rb8
-rw-r--r--app/models/ci/build.rb137
-rw-r--r--app/models/ci/build_metadata.rb3
-rw-r--r--app/models/ci/freeze_period_status.rb28
-rw-r--r--app/models/ci/job_artifact.rb40
-rw-r--r--app/models/ci/job_token/scope.rb12
-rw-r--r--app/models/ci/namespace_mirror.rb14
-rw-r--r--app/models/ci/partition.rb6
-rw-r--r--app/models/ci/pipeline.rb81
-rw-r--r--app/models/ci/pipeline_artifact.rb6
-rw-r--r--app/models/ci/pipeline_variable.rb5
-rw-r--r--app/models/ci/processable.rb1
-rw-r--r--app/models/ci/runner.rb17
-rw-r--r--app/models/ci/stage.rb21
-rw-r--r--app/models/ci/trigger.rb6
-rw-r--r--app/models/clusters/applications/ingress.rb4
-rw-r--r--app/models/commit.rb16
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/approvable.rb (renamed from app/models/concerns/approvable_base.rb)9
-rw-r--r--app/models/concerns/ci/artifactable.rb11
-rw-r--r--app/models/concerns/ci/has_deployment_name.rb15
-rw-r--r--app/models/concerns/ci/lockable.rb20
-rw-r--r--app/models/concerns/ci/metadatable.rb2
-rw-r--r--app/models/concerns/ci/partitionable.rb47
-rw-r--r--app/models/concerns/ci/track_environment_usage.rb31
-rw-r--r--app/models/concerns/counter_attribute.rb20
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb6
-rw-r--r--app/models/concerns/enums/internal_id.rb3
-rw-r--r--app/models/concerns/from_set_operator.rb25
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb14
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb11
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb1
-rw-r--r--app/models/concerns/project_features_compatibility.rb4
-rw-r--r--app/models/concerns/sortable.rb27
-rw-r--r--app/models/container_repository.rb9
-rw-r--r--app/models/customer_relations/contact.rb45
-rw-r--r--app/models/customer_relations/organization.rb33
-rw-r--r--app/models/deployment.rb25
-rw-r--r--app/models/environment.rb27
-rw-r--r--app/models/environment_status.rb2
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb16
-rw-r--r--app/models/group.rb85
-rw-r--r--app/models/group_group_link.rb4
-rw-r--r--app/models/hooks/web_hook_log.rb2
-rw-r--r--app/models/incident_management/timeline_event.rb2
-rw-r--r--app/models/integration.rb6
-rw-r--r--app/models/integrations/datadog.rb26
-rw-r--r--app/models/integrations/discord.rb46
-rw-r--r--app/models/integrations/hangouts_chat.rb25
-rw-r--r--app/models/integrations/harbor.rb4
-rw-r--r--app/models/integrations/shimo.rb2
-rw-r--r--app/models/internal_id.rb5
-rw-r--r--app/models/issue.rb43
-rw-r--r--app/models/jira_connect_installation.rb6
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb4
-rw-r--r--app/models/member.rb101
-rw-r--r--app/models/merge_request.rb116
-rw-r--r--app/models/merge_request/predictions.rb7
-rw-r--r--app/models/merge_request_assignee.rb5
-rw-r--r--app/models/merge_request_reviewer.rb2
-rw-r--r--app/models/ml/candidate.rb13
-rw-r--r--app/models/ml/experiment.rb26
-rw-r--r--app/models/namespace.rb22
-rw-r--r--app/models/namespace_setting.rb15
-rw-r--r--app/models/namespaces/traversal/linear.rb10
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb29
-rw-r--r--app/models/note.rb7
-rw-r--r--app/models/notification_recipient.rb10
-rw-r--r--app/models/oauth_access_token.rb9
-rw-r--r--app/models/onboarding/completion.rb70
-rw-r--r--app/models/onboarding/learn_gitlab.rb38
-rw-r--r--app/models/onboarding/progress.rb118
-rw-r--r--app/models/onboarding_progress.rb114
-rw-r--r--app/models/packages/package.rb43
-rw-r--r--app/models/packages/policies/group.rb15
-rw-r--r--app/models/packages/policies/project.rb15
-rw-r--r--app/models/packages/rpm.rb8
-rw-r--r--app/models/packages/rpm/metadatum.rb51
-rw-r--r--app/models/pages_domain.rb12
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/pool_repository.rb4
-rw-r--r--app/models/preloaders/environments/deployment_preloader.rb10
-rw-r--r--app/models/preloaders/group_policy_preloader.rb2
-rw-r--r--app/models/preloaders/project_policy_preloader.rb23
-rw-r--r--app/models/preloaders/project_root_ancestor_preloader.rb37
-rw-r--r--app/models/preloaders/users_max_access_level_in_projects_preloader.rb2
-rw-r--r--app/models/project.rb121
-rw-r--r--app/models/project_feature.rb1
-rw-r--r--app/models/project_setting.rb11
-rw-r--r--app/models/project_statistics.rb34
-rw-r--r--app/models/projects/build_artifacts_size_refresh.rb11
-rw-r--r--app/models/projects/topic.rb8
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/repository.rb28
-rw-r--r--app/models/resource_state_event.rb7
-rw-r--r--app/models/resource_timebox_event.rb6
-rw-r--r--app/models/route.rb18
-rw-r--r--app/models/snippet.rb10
-rw-r--r--app/models/snippet_repository.rb2
-rw-r--r--app/models/system_note_metadata.rb1
-rw-r--r--app/models/todo.rb9
-rw-r--r--app/models/user.rb99
-rw-r--r--app/models/user_status.rb4
-rw-r--r--app/models/users/callout.rb14
-rw-r--r--app/models/users/credit_card_validation.rb6
-rw-r--r--app/models/users/ghost_user_migration.rb12
-rw-r--r--app/models/users/group_callout.rb8
-rw-r--r--app/models/users/namespace_callout.rb8
-rw-r--r--app/models/users/project_callout.rb4
-rw-r--r--app/models/users_star_project.rb2
-rw-r--r--app/models/wiki.rb96
-rw-r--r--app/models/work_item.rb4
-rw-r--r--app/models/work_items/widgets/description.rb8
115 files changed, 1640 insertions, 935 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 9f634e70ff4..7dbc95c251b 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -83,21 +83,21 @@ class ActiveSession
is_impersonated: request.session[:impersonator_id].present?
)
- redis.pipelined do
- redis.setex(
+ redis.pipelined do |pipeline|
+ pipeline.setex(
key_name(user.id, session_private_id),
expiry,
active_user_session.dump
)
# Deprecated legacy format - temporary to support mixed deployments
- redis.setex(
+ pipeline.setex(
key_name_v1(user.id, session_private_id),
expiry,
Marshal.dump(active_user_session)
)
- redis.sadd(
+ pipeline.sadd(
lookup_key_name(user.id),
session_private_id
)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 579f2c38ae6..edb9a2053b1 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -10,11 +10,7 @@ class ApplicationSetting < ApplicationRecord
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22'
- ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18'
- ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22'
- ignore_column :enforce_ssh_key_expiration, remove_with: '15.2', remove_after: '2022-07-22'
- ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -221,6 +217,10 @@ class ApplicationSetting < ApplicationRecord
numericality: { only_integer: true, greater_than_or_equal_to: 0,
less_than: ::Gitlab::Pages::MAX_SIZE / 1.megabyte }
+ validates :max_pages_custom_domains_per_project,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :jobs_per_stage_page_size,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -406,6 +406,10 @@ class ApplicationSetting < ApplicationRecord
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: _('must be a boolean value') }
+ validates :invitation_flow_enforcement,
+ allow_nil: false,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+
Gitlab::SSHPublicKey.supported_types.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -621,6 +625,10 @@ class ApplicationSetting < ApplicationRecord
validates :inactive_projects_send_warning_email_after_months,
numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months }
+ validates :cube_api_base_url,
+ addressable_url: { allow_localhost: true, allow_local_network: false },
+ allow_blank: true
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -658,6 +666,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :database_grafana_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :arkose_labs_public_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
+ attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm
validates :disable_feed_token,
inclusion: { in: [true, false], message: _('must be a boolean value') }
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 3fda8693a58..323d759510e 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -75,9 +75,9 @@ module Ci
def self.clone_accessors
%i[pipeline project ref tag options name
- allow_failure stage stage_id stage_idx
+ allow_failure stage stage_idx
yaml_variables when description needs_attributes
- scheduling_type].freeze
+ scheduling_type ci_stage partition_id].freeze
end
def inherit_status_from_downstream!(pipeline)
@@ -183,6 +183,10 @@ module Ci
false
end
+ def prevent_rollback_deployment?
+ false
+ end
+
def expanded_environment_name
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf8817e6e78..4e58f877217 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,7 +11,7 @@ module Ci
include Presentable
include Importable
include Ci::HasRef
- include HasDeploymentName
+ include Ci::TrackEnvironmentUsage
extend ::Gitlab::Utils::Override
@@ -34,7 +34,7 @@ module Ci
DEPLOYMENT_NAMES = %w[deploy release rollout].freeze
- has_one :deployment, as: :deployable, class_name: 'Deployment'
+ has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id
@@ -194,7 +194,7 @@ module Ci
after_save :stick_build_if_status_changed
after_create unless: :importing? do |build|
- run_after_commit { build.feature_flagged_execute_hooks }
+ run_after_commit { build.execute_hooks }
end
class << self
@@ -214,10 +214,11 @@ module Ci
def clone_accessors
%i[pipeline project ref tag options name
- allow_failure stage stage_id stage_idx trigger_request
+ allow_failure stage stage_idx trigger_request
yaml_variables when environment coverage_regex
description tag_list protected needs_attributes
- job_variables_attributes resource_group scheduling_type].freeze
+ job_variables_attributes resource_group scheduling_type
+ ci_stage partition_id].freeze
end
end
@@ -285,7 +286,7 @@ module Ci
build.run_after_commit do
BuildQueueWorker.perform_async(id)
- build.feature_flagged_execute_hooks
+ build.execute_hooks
end
end
@@ -313,7 +314,7 @@ module Ci
build.run_after_commit do
build.ensure_persistent_ref
- build.feature_flagged_execute_hooks
+ build.execute_hooks
end
end
@@ -442,6 +443,15 @@ module Ci
manual? && starts_environment? && deployment&.blocked?
end
+ def prevent_rollback_deployment?
+ strong_memoize(:prevent_rollback_deployment) do
+ Feature.enabled?(:prevent_outdated_deployment_jobs, project) &&
+ starts_environment? &&
+ project.ci_forward_deployment_enabled? &&
+ deployment&.older_than_last_successful_deployment?
+ end
+ end
+
def schedulable?
self.when == 'delayed' && options[:start_in].present?
end
@@ -703,25 +713,7 @@ module Ci
end
def has_test_reports?
- job_artifacts.test_reports.exists?
- end
-
- def has_old_trace?
- old_trace.present?
- end
-
- def trace=(data)
- raise NotImplementedError
- end
-
- def old_trace
- read_attribute(:trace)
- end
-
- def erase_old_trace!
- return unless has_old_trace?
-
- update_column(:trace, nil)
+ job_artifacts.of_report_type(:test).exists?
end
def ensure_trace_metadata!
@@ -780,14 +772,6 @@ module Ci
pending? && !any_runners_online?
end
- def feature_flagged_execute_hooks
- if Feature.enabled?(:execute_build_hooks_inline, project)
- execute_hooks
- else
- BuildHooksWorker.perform_async(self)
- end
- end
-
def execute_hooks
return unless project
return if user&.blocked?
@@ -823,41 +807,6 @@ module Ci
end
end
- def erase_erasable_artifacts!
- if project.refreshing_build_artifacts_size?
- Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
- method: 'Ci::Build#erase_erasable_artifacts!',
- project_id: project_id
- )
- end
-
- destroyed_artifacts = job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll
-
- Gitlab::Ci::Artifacts::Logger.log_deleted(destroyed_artifacts, 'Ci::Build#erase_erasable_artifacts!')
-
- destroyed_artifacts
- end
-
- def erase(opts = {})
- return false unless erasable?
-
- if project.refreshing_build_artifacts_size?
- Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
- method: 'Ci::Build#erase',
- project_id: project_id
- )
- end
-
- # TODO: We should use DestroyBatchService here
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/369132
- destroyed_artifacts = job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll
-
- Gitlab::Ci::Artifacts::Logger.log_deleted(destroyed_artifacts, 'Ci::Build#erase')
-
- erase_trace!
- update_erased!(opts[:erased_by])
- end
-
def erasable?
complete? && (artifacts? || has_job_artifacts? || has_trace?)
end
@@ -1004,15 +953,11 @@ module Ci
end
def collect_test_reports!(test_reports)
- test_reports.get_suite(test_suite_name).tap do |test_suite|
- each_report(Ci::JobArtifact.file_types_for_report(:test)) do |file_type, blob|
- Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
- blob,
- test_suite,
- job: self
- )
- end
+ each_report(Ci::JobArtifact.file_types_for_report(:test)) do |file_type, blob|
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_reports, job: self)
end
+
+ test_reports
end
def collect_accessibility_reports!(accessibility_report)
@@ -1154,18 +1099,6 @@ module Ci
.include?(exit_code)
end
- def track_deployment_usage
- Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) if user_id.present? && count_user_deployment?
- end
-
- def track_verify_usage
- Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_verify_environment_job', user_id) if user_id.present? && count_user_verification?
- end
-
- def count_user_verification?
- has_environment? && environment_action == 'verify'
- end
-
def each_report(report_types)
job_artifacts_for_types(report_types).each do |report_artifact|
report_artifact.each_blob do |blob|
@@ -1189,6 +1122,14 @@ module Ci
job_artifacts.map(&:file_type)
end
+ def test_suite_name
+ if matrix_build?
+ name
+ else
+ group_name
+ end
+ end
+
protected
def run_status_commit_hooks!
@@ -1199,14 +1140,6 @@ module Ci
private
- def test_suite_name
- if matrix_build?
- name
- else
- group_name
- end
- end
-
def matrix_build?
options.dig(:parallel, :matrix).present?
end
@@ -1245,14 +1178,6 @@ module Ci
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
end
- def erase_trace!
- trace.erase!
- end
-
- def update_erased!(user = nil)
- self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil)
- end
-
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
@@ -1298,7 +1223,7 @@ module Ci
end
def observe_report_types
- return unless ::Gitlab.com? && Feature.enabled?(:report_artifact_build_completed_metrics_on_build_completion)
+ return unless ::Gitlab.com?
report_types = options&.dig(:artifacts, :reports)&.keys || []
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 5fc21ba3f28..3bdf2f90acb 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -9,7 +9,6 @@ module Ci
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
- include IgnorableColumns
self.table_name = 'ci_builds_metadata'
@@ -39,8 +38,6 @@ module Ci
job_timeout_source: 4
}
- ignore_columns :runner_features, remove_with: '15.1', remove_after: '2022-05-22'
-
def update_timeout_state
timeout = timeout_with_highest_precedence
diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb
index befa935e750..e810bb3f229 100644
--- a/app/models/ci/freeze_period_status.rb
+++ b/app/models/ci/freeze_period_status.rb
@@ -13,32 +13,16 @@ module Ci
end
def within_freeze_period?(period)
- # previous_freeze_end, ..., previous_freeze_start, ..., NOW, ..., next_freeze_end, ..., next_freeze_start
- # Current time is within a freeze period if
- # it falls between a previous freeze start and next freeze end
- start_freeze = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
- end_freeze = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
-
- previous_freeze_start = previous_time(start_freeze)
- previous_freeze_end = previous_time(end_freeze)
- next_freeze_start = next_time(start_freeze)
- next_freeze_end = next_time(end_freeze)
-
- previous_freeze_end < previous_freeze_start &&
- previous_freeze_start <= time_zone_now &&
- time_zone_now <= next_freeze_end &&
- next_freeze_end < next_freeze_start
- end
+ start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
+ end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
- private
+ start_freeze = start_freeze_cron.previous_time_from(time_zone_now)
+ end_freeze = end_freeze_cron.next_time_from(start_freeze)
- def previous_time(cron_parser)
- cron_parser.previous_time_from(time_zone_now)
+ start_freeze <= time_zone_now && time_zone_now <= end_freeze
end
- def next_time(cron_parser)
- cron_parser.next_time_from(time_zone_now)
- end
+ private
def time_zone_now
@time_zone_now ||= Time.zone.now
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 71d33f0bb63..922806a21c3 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -2,6 +2,7 @@
module Ci
class JobArtifact < Ci::ApplicationRecord
+ include Ci::Partitionable
include IgnorableColumns
include AfterCommitQueue
include ObjectStorage::BackgroundMove
@@ -9,6 +10,7 @@ module Ci
include UsageStatistics
include Sortable
include Artifactable
+ include Lockable
include FileStoreMounter
include EachBatch
include Gitlab::Utils::StrongMemoize
@@ -22,8 +24,7 @@ module Ci
accessibility: %w[accessibility],
coverage: %w[cobertura],
codequality: %w[codequality],
- terraform: %w[terraform],
- sbom: %w[cyclonedx]
+ terraform: %w[terraform]
}.freeze
DEFAULT_FILE_NAMES = {
@@ -54,7 +55,7 @@ module Ci
requirements: 'requirements.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json',
api_fuzzing: 'gl-api-fuzzing-report.json',
- cyclonedx: 'gl-sbom.cdx.zip'
+ cyclonedx: 'gl-sbom.cdx.json'
}.freeze
INTERNAL_TYPES = {
@@ -72,6 +73,7 @@ module Ci
cobertura: :gzip,
cluster_applications: :gzip, # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
lsif: :zip,
+ cyclonedx: :gzip,
# Security reports and license scanning reports are raw artifacts
# because they used to be fetched by the frontend, but this is not the case anymore.
@@ -94,8 +96,7 @@ module Ci
terraform: :raw,
requirements: :raw,
coverage_fuzzing: :raw,
- api_fuzzing: :raw,
- cyclonedx: :zip
+ api_fuzzing: :raw
}.freeze
DOWNLOADABLE_TYPES = %w[
@@ -134,14 +135,16 @@ module Ci
mount_file_store_uploader JobArtifactUploader, skip_store_file: true
+ before_save :set_size, if: :file_changed?
after_save :store_file_in_transaction!, unless: :store_after_commit?
after_commit :store_file_after_transaction!, on: [:create, :update], if: :store_after_commit?
+ validates :job, presence: true
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_file_format!, unless: :trace?, on: :create
- before_save :set_size, if: :file_changed?
update_project_statistics project_statistics_name: :build_artifacts_size
+ partitionable scope: :job
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
@@ -160,12 +163,6 @@ module Ci
where(file_type: types)
end
- REPORT_FILE_TYPES.each do |report_type, file_types|
- scope "#{report_type}_reports", -> do
- with_file_types(file_types)
- end
- end
-
scope :all_reports, -> do
with_file_types(REPORT_TYPES.keys.map(&:to_s))
end
@@ -229,25 +226,20 @@ module Ci
hashed_path: 2
}
- # `locked` will be populated from the source of truth on Ci::Pipeline
- # in order to clean up expired job artifacts in a performant way.
- # The values should be the same as `Ci::Pipeline.lockeds` with the
- # additional value of `unknown` to indicate rows that have not
- # yet been populated from the parent Ci::Pipeline
- enum locked: {
- unlocked: 0,
- artifacts_locked: 1,
- unknown: 2
- }, _prefix: :artifact
-
def validate_file_format!
unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym
errors.add(:base, _('Invalid file format with specified file type'))
end
end
+ def self.of_report_type(report_type)
+ file_types = file_types_for_report(report_type)
+
+ with_file_types(file_types)
+ end
+
def self.file_types_for_report(report_type)
- REPORT_FILE_TYPES.fetch(report_type)
+ REPORT_FILE_TYPES.fetch(report_type) { raise ArgumentError, "Unrecognized report type: #{report_type}" }
end
def self.associated_file_types_for(file_type)
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index 3a5765aa00c..26a49d6a730 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -30,10 +30,7 @@ module Ci
end
def all_projects
- Project.from_union([
- Project.id_in(source_project),
- Project.id_in(target_project_ids)
- ], remove_duplicates: false)
+ Project.from_union(target_projects, remove_duplicates: false)
end
private
@@ -41,6 +38,13 @@ module Ci
def target_project_ids
Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
end
+
+ def target_projects
+ [
+ Project.id_in(source_project),
+ Project.id_in(target_project_ids)
+ ]
+ end
end
end
end
diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb
index e8f08db597f..5ea51fbe0a7 100644
--- a/app/models/ci/namespace_mirror.rb
+++ b/app/models/ci/namespace_mirror.rb
@@ -43,20 +43,6 @@ module Ci
upsert({ namespace_id: event.namespace_id, traversal_ids: traversal_ids },
unique_by: :namespace_id)
-
- # It won't be necessary once we remove `sync_traversal_ids`.
- # More info: https://gitlab.com/gitlab-org/gitlab/-/issues/347541
- sync_children_namespaces!(event.namespace_id, traversal_ids)
- end
-
- private
-
- def sync_children_namespaces!(namespace_id, traversal_ids)
- by_group_and_descendants(namespace_id)
- .where.not(namespace_id: namespace_id)
- .update_all(
- "traversal_ids = ARRAY[#{sanitize_sql(traversal_ids.join(','))}]::int[] || traversal_ids[array_position(traversal_ids, #{sanitize_sql(namespace_id)}) + 1:]"
- )
end
end
end
diff --git a/app/models/ci/partition.rb b/app/models/ci/partition.rb
new file mode 100644
index 00000000000..d773038df01
--- /dev/null
+++ b/app/models/ci/partition.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module Ci
+ class Partition < Ci::ApplicationRecord
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index a94330270e2..1e328c3c573 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -2,6 +2,7 @@
module Ci
class Pipeline < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::HasStatus
include Importable
include AfterCommitQueue
@@ -31,7 +32,7 @@ module Ci
sha_attribute :source_sha
sha_attribute :target_sha
-
+ partitionable scope: ->(_) { Ci::Pipeline.current_partition_value }
# Ci::CreatePipelineService returns Ci::Pipeline so this is the only place
# where we can pass additional information from the service. This accessor
# is used for storing the processed metadata for linting purposes.
@@ -296,6 +297,12 @@ module Ci
end
end
+ after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
+ pipeline.run_after_commit do
+ ::Ci::JobArtifacts::TrackArtifactReportWorker.perform_async(pipeline.id)
+ end
+ end
+
after_transition any => ::Ci::Pipeline.stopped_statuses do |pipeline|
pipeline.run_after_commit do
pipeline.persistent_ref.delete
@@ -422,6 +429,10 @@ module Ci
end
def self.jobs_count_in_alive_pipelines
+ created_after(24.hours.ago).alive.joins(:statuses).count
+ end
+
+ def self.builds_count_in_alive_pipelines
created_after(24.hours.ago).alive.joins(:builds).count
end
@@ -472,8 +483,12 @@ module Ci
@auto_devops_pipelines_completed_total ||= Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines')
end
+ def self.current_partition_value
+ 100
+ end
+
def uses_needs?
- builds.where(scheduling_type: :dag).any?
+ processables.where(scheduling_type: :dag).any?
end
def stages_count
@@ -605,7 +620,7 @@ module Ci
if cascade_to_children
# cancel any bridges that could spin up new child pipelines
- cancel_jobs(bridges_in_self_and_descendants.cancelable, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id)
+ cancel_jobs(bridges_in_self_and_project_descendants.cancelable, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id)
cancel_children(auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id, execute_async: execute_async)
end
end
@@ -937,26 +952,26 @@ module Ci
).base_and_descendants.select(:id)
end
- def build_with_artifacts_in_self_and_descendants(name)
- builds_in_self_and_descendants
+ def build_with_artifacts_in_self_and_project_descendants(name)
+ builds_in_self_and_project_descendants
.ordered_by_pipeline # find job in hierarchical order
.with_downloadable_artifacts
.find_by_name(name)
end
- def builds_in_self_and_descendants
- Ci::Build.latest.where(pipeline: self_and_descendants)
+ def builds_in_self_and_project_descendants
+ Ci::Build.latest.where(pipeline: self_and_project_descendants)
end
- def bridges_in_self_and_descendants
- Ci::Bridge.latest.where(pipeline: self_and_descendants)
+ def bridges_in_self_and_project_descendants
+ Ci::Bridge.latest.where(pipeline: self_and_project_descendants)
end
- def environments_in_self_and_descendants(deployment_status: nil)
+ def environments_in_self_and_project_descendants(deployment_status: nil)
# We limit to 100 unique environments for application safety.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700
expanded_environment_names =
- builds_in_self_and_descendants.joins(:metadata)
+ builds_in_self_and_project_descendants.joins(:metadata)
.where.not('ci_builds_metadata.expanded_environment_name' => nil)
.distinct('ci_builds_metadata.expanded_environment_name')
.limit(100)
@@ -971,17 +986,22 @@ module Ci
end
# With multi-project and parent-child pipelines
- def all_pipelines_in_hierarchy
+ def self_and_downstreams
+ object_hierarchy.base_and_descendants
+ end
+
+ # With multi-project and parent-child pipelines
+ def upstream_and_all_downstreams
object_hierarchy.all_objects
end
# With only parent-child pipelines
- def self_and_ancestors
+ def self_and_project_ancestors
object_hierarchy(project_condition: :same).base_and_ancestors
end
# With only parent-child pipelines
- def self_and_descendants
+ def self_and_project_descendants
object_hierarchy(project_condition: :same).base_and_descendants
end
@@ -990,8 +1010,8 @@ module Ci
object_hierarchy(project_condition: :same).descendants
end
- def self_and_descendants_complete?
- self_and_descendants.all?(&:complete?)
+ def self_and_project_descendants_complete?
+ self_and_project_descendants.all?(&:complete?)
end
# Follow the parent-child relationships and return the top-level parent
@@ -1006,7 +1026,12 @@ module Ci
# Follow the upstream pipeline relationships, regardless of multi-project or
# parent-child, and return the top-level ancestor.
def upstream_root
- object_hierarchy.base_and_ancestors(hierarchy_order: :desc).first
+ @upstream_root ||= object_hierarchy.base_and_ancestors(hierarchy_order: :desc).first
+ end
+
+ # Applies to all parent-child and multi-project pipelines
+ def complete_hierarchy_count
+ upstream_root.self_and_downstreams.count
end
def bridge_triggered?
@@ -1052,11 +1077,11 @@ module Ci
end
def latest_test_report_builds
- latest_report_builds(Ci::JobArtifact.test_reports).preload(:project, :metadata)
+ latest_report_builds(Ci::JobArtifact.of_report_type(:test)).preload(:project, :metadata)
end
- def latest_report_builds_in_self_and_descendants(reports_scope = ::Ci::JobArtifact.all_reports)
- builds_in_self_and_descendants.with_artifacts(reports_scope)
+ def latest_report_builds_in_self_and_project_descendants(reports_scope = ::Ci::JobArtifact.all_reports)
+ builds_in_self_and_project_descendants.with_artifacts(reports_scope)
end
def builds_with_coverage
@@ -1068,10 +1093,14 @@ module Ci
end
def has_reports?(reports_scope)
+ latest_report_builds(reports_scope).exists?
+ end
+
+ def complete_and_has_reports?(reports_scope)
if Feature.enabled?(:mr_show_reports_immediately, project, type: :development)
latest_report_builds(reports_scope).exists?
else
- complete? && latest_report_builds(reports_scope).exists?
+ complete? && has_reports?(reports_scope)
end
end
@@ -1084,7 +1113,7 @@ module Ci
end
def can_generate_codequality_reports?
- has_reports?(Ci::JobArtifact.codequality_reports)
+ complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end
def test_report_summary
@@ -1103,7 +1132,7 @@ module Ci
def accessibility_reports
Gitlab::Ci::Reports::AccessibilityReports.new.tap do |accessibility_reports|
- latest_report_builds(Ci::JobArtifact.accessibility_reports).each do |build|
+ latest_report_builds(Ci::JobArtifact.of_report_type(:accessibility)).each do |build|
build.collect_accessibility_reports!(accessibility_reports)
end
end
@@ -1111,7 +1140,7 @@ module Ci
def codequality_reports
Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
- latest_report_builds(Ci::JobArtifact.codequality_reports).each do |build|
+ latest_report_builds(Ci::JobArtifact.of_report_type(:codequality)).each do |build|
build.collect_codequality_reports!(codequality_reports)
end
end
@@ -1119,7 +1148,7 @@ module Ci
def terraform_reports
::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports|
- latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build|
+ latest_report_builds(::Ci::JobArtifact.of_report_type(:terraform)).each do |build|
build.collect_terraform_reports!(terraform_reports)
end
end
@@ -1307,7 +1336,7 @@ module Ci
def has_test_reports?
strong_memoize(:has_test_reports) do
- has_reports?(::Ci::JobArtifact.test_reports)
+ has_reports?(::Ci::JobArtifact.of_report_type(:test))
end
end
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index cdc3d69f754..6d22a875aab 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -7,6 +7,7 @@ module Ci
include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
+ include Lockable
include Presentable
FILE_SIZE_LIMIT = 10.megabytes.freeze
@@ -52,7 +53,7 @@ module Ci
find_by(file_type: file_type)
end
- def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:)
+ def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:, locked: :unknown)
transaction do
pipeline.pipeline_artifacts.find_by_file_type(file_type)&.destroy!
@@ -62,7 +63,8 @@ module Ci
size: size,
file: file,
file_format: REPORT_TYPES[file_type],
- expire_at: EXPIRATION_DATE.from_now
+ expire_at: EXPIRATION_DATE.from_now,
+ locked: locked
)
end
rescue ActiveRecord::ActiveRecordError => err
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 3dca77af051..6e4418bc360 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -2,13 +2,16 @@
module Ci
class PipelineVariable < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::HasVariable
belongs_to :pipeline
+ partitionable scope: :pipeline
+
alias_attribute :secret_value, :value
- validates :key, presence: true
+ validates :key, :pipeline, presence: true
def hook_attrs
{ key: key, value: value }
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index a2ff49077be..09dc9d4bce1 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -3,6 +3,7 @@
module Ci
class Processable < ::CommitStatus
include Gitlab::Utils::StrongMemoize
+ include FromUnion
extend ::Gitlab::Utils::Override
has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 6c3754d84d0..28d9edcc135 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -8,15 +8,12 @@ module Ci
include ChronicDurationAttribute
include FromUnion
include TokenAuthenticatable
- include IgnorableColumns
include FeatureGate
include Gitlab::Utils::StrongMemoize
include TaggableQueries
include Presentable
include EachBatch
- ignore_column :semver, remove_with: '15.4', remove_after: '2022-08-22'
-
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
enum access_level: {
@@ -351,6 +348,12 @@ module Ci
end
end
+ def owner_project
+ return unless project_type?
+
+ runner_projects.order(:id).first.project
+ end
+
def belongs_to_one_project?
runner_projects.count == 1
end
@@ -359,14 +362,6 @@ module Ci
runner_projects.limit(2).count(:all) > 1
end
- def assigned_to_group?
- runner_namespaces.any?
- end
-
- def assigned_to_project?
- runner_projects.any?
- end
-
def match_build_if_online?(build)
active? && online? && matches_build?(build)
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index f03d1e96a4b..46a9e3f6494 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -2,22 +2,31 @@
module Ci
class Stage < Ci::ApplicationRecord
+ include Ci::Partitionable
include Importable
include Ci::HasStatus
include Gitlab::OptimisticLocking
include Presentable
+ partitionable scope: :pipeline
+
enum status: Ci::HasStatus::STATUSES_ENUM
belongs_to :project
belongs_to :pipeline
- has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
- has_many :latest_statuses, -> { ordered.latest }, class_name: 'CommitStatus', foreign_key: :stage_id
- has_many :retried_statuses, -> { ordered.retried }, class_name: 'CommitStatus', foreign_key: :stage_id
- has_many :processables, class_name: 'Ci::Processable', foreign_key: :stage_id
- has_many :builds, foreign_key: :stage_id
- has_many :bridges, foreign_key: :stage_id
+ has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id, inverse_of: :ci_stage
+ has_many :latest_statuses, -> { ordered.latest },
+ class_name: 'CommitStatus',
+ foreign_key: :stage_id,
+ inverse_of: :ci_stage
+ has_many :retried_statuses, -> { ordered.retried },
+ class_name: 'CommitStatus',
+ foreign_key: :stage_id,
+ inverse_of: :ci_stage
+ has_many :processables, class_name: 'Ci::Processable', foreign_key: :stage_id, inverse_of: :ci_stage
+ has_many :builds, foreign_key: :stage_id, inverse_of: :ci_stage
+ has_many :bridges, foreign_key: :stage_id, inverse_of: :ci_stage
scope :ordered, -> { order(position: :asc) }
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index c4db4754c52..1092b9c9564 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -6,6 +6,8 @@ module Ci
include Limitable
include IgnorableColumns
+ TRIGGER_TOKEN_PREFIX = 'glptt-'
+
ignore_column :ref, remove_with: '15.4', remove_after: '2022-08-22'
self.limit_name = 'pipeline_triggers'
@@ -22,7 +24,7 @@ module Ci
before_validation :set_default_values
def set_default_values
- self.token = SecureRandom.hex(15) if self.token.blank?
+ self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
end
def last_trigger_request
@@ -34,7 +36,7 @@ module Ci
end
def short_token
- token[0...4] if token.present?
+ token.delete_prefix(TRIGGER_TOKEN_PREFIX)[0...4] if token.present?
end
def can_access_project?
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 3a8c314efe4..27550616002 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -16,14 +16,10 @@ module Clusters
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
include UsageStatistics
- include IgnorableColumns
default_value_for :ingress_type, :nginx
default_value_for :version, VERSION
- ignore_column :modsecurity_enabled, remove_with: '14.2', remove_after: '2021-07-22'
- ignore_column :modsecurity_mode, remove_with: '14.2', remove_after: '2021-07-22'
-
enum ingress_type: {
nginx: 1
}
diff --git a/app/models/commit.rb b/app/models/commit.rb
index bd60f02b532..54de45ebba7 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -133,6 +133,22 @@ class Commit
def parent_class
::Project
end
+
+ def build_from_sidekiq_hash(project, hash)
+ hash = hash.dup
+ date_suffix = '_date'
+
+ # When processing Sidekiq payloads various timestamps are stored as Strings.
+ # Commit in turn expects Time-like instances upon input, so we have to
+ # manually parse these values.
+ hash.each do |key, value|
+ if key.to_s.end_with?(date_suffix) && value.is_a?(String)
+ hash[key] = Time.zone.parse(value)
+ end
+ end
+
+ from_hash(hash, project)
+ end
end
attr_accessor :raw
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index afe4927ee73..05a258e6e26 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class CommitStatus < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::HasStatus
include Importable
include AfterCommitQueue
@@ -11,13 +12,14 @@ class CommitStatus < Ci::ApplicationRecord
include IgnorableColumns
self.table_name = 'ci_builds'
-
- ignore_column :token, remove_with: '15.4', remove_after: '2022-08-22'
+ partitionable scope: :pipeline
+ ignore_column :trace, remove_with: '15.6', remove_after: '2022-10-22'
belongs_to :user
belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
+ belongs_to :ci_stage, class_name: 'Ci::Stage', foreign_key: :stage_id
has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
@@ -318,6 +320,10 @@ class CommitStatus < Ci::ApplicationRecord
Gitlab::EtagCaching::Store.new.touch(job_path)
end
+ def stage_name
+ ci_stage&.name
+ end
+
private
def unrecoverable_failure?
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable.rb
index 8240f9bd6ea..1566c53217d 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module ApprovableBase
+module Approvable
extend ActiveSupport::Concern
include FromUnion
@@ -27,12 +27,11 @@ module ApprovableBase
scope :not_approved_by_users_with_usernames, -> (usernames) do
users = User.where(username: usernames).select(:id)
- self_table = self.arel_table
app_table = Approval.arel_table
where(
Approval.where(approvals: { user_id: users })
- .where(app_table[:merge_request_id].eq(self_table[:id]))
+ .where(app_table[:merge_request_id].eq(arel_table[:id]))
.select('true')
.arel.exists.not
)
@@ -48,7 +47,7 @@ module ApprovableBase
def approved_by?(user)
return false unless user
- approved_by_users.include?(user)
+ approvals.where(user: user).any?
end
def can_be_approved_by?(user)
@@ -59,3 +58,5 @@ module ApprovableBase
user && approved_by?(user) && user.can?(:approve_merge_request, self)
end
end
+
+Approvable.prepend_mod
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index ee8e98ec1bf..3fdbd6a8789 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -10,8 +10,17 @@ module Ci
STORE_COLUMN = :file_store
NotSupportedAdapterError = Class.new(StandardError)
FILE_FORMAT_ADAPTERS = {
+ # While zip is a streamable file format, performing streaming
+ # reads requires that each entry in the zip has certain headers
+ # present at the front of the entry. These headers are OPTIONAL
+ # according to the file format specification. GitLab Runner uses
+ # Go's `archive/zip` to create zip archives, which does not include
+ # these headers. Go maintainers have expressed that they don't intend
+ # to support them: https://github.com/golang/go/issues/23301#issuecomment-363240781
+ #
+ # If you need GitLab to be able to read Artifactables, store them in
+ # raw or gzip format instead of zip.
gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream,
- zip: Gitlab::Ci::Build::Artifacts::Adapters::ZipStream,
raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
}.freeze
diff --git a/app/models/concerns/ci/has_deployment_name.rb b/app/models/concerns/ci/has_deployment_name.rb
deleted file mode 100644
index 887653e846e..00000000000
--- a/app/models/concerns/ci/has_deployment_name.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module HasDeploymentName
- extend ActiveSupport::Concern
-
- def count_user_deployment?
- deployment_name?
- end
-
- def deployment_name?
- self.class::DEPLOYMENT_NAMES.any? { |n| name.downcase.include?(n) }
- end
- end
-end
diff --git a/app/models/concerns/ci/lockable.rb b/app/models/concerns/ci/lockable.rb
new file mode 100644
index 00000000000..31ba93775e2
--- /dev/null
+++ b/app/models/concerns/ci/lockable.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Ci
+ module Lockable
+ extend ActiveSupport::Concern
+
+ included do
+ # `locked` will be populated from the source of truth on Ci::Pipeline
+ # in order to clean up expired job artifacts in a performant way.
+ # The values should be the same as `Ci::Pipeline.lockeds` with the
+ # additional value of `unknown` to indicate rows that have not
+ # yet been populated from the parent Ci::Pipeline
+ enum locked: {
+ unlocked: 0,
+ artifacts_locked: 1,
+ unknown: 2
+ }, _prefix: :artifact
+ end
+ end
+end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 8c3a05c23f0..71b26b70bbf 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -34,7 +34,7 @@ module Ci
end
def ensure_metadata
- metadata || build_metadata(project: project)
+ metadata || build_metadata(project: project, partition_id: partition_id)
end
def degenerated?
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
new file mode 100644
index 00000000000..710ee1ba64f
--- /dev/null
+++ b/app/models/concerns/ci/partitionable.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements a way to set the `partion_id` value on a dependent
+ # resource from a parent record.
+ # Usage:
+ #
+ # class PipelineVariable < Ci::ApplicationRecord
+ # include Ci::Partitionable
+ #
+ # belongs_to :pipeline
+ # partitionable scope: :pipeline
+ # # Or
+ # partitionable scope: ->(record) { record.partition_value }
+ #
+ #
+ module Partitionable
+ extend ActiveSupport::Concern
+ include ::Gitlab::Utils::StrongMemoize
+
+ included do
+ before_validation :set_partition_id, on: :create
+ validates :partition_id, presence: true
+
+ def set_partition_id
+ return if partition_id_changed? && partition_id.present?
+ return unless partition_scope_value
+
+ self.partition_id = partition_scope_value
+ end
+ end
+
+ class_methods do
+ private
+
+ def partitionable(scope:)
+ define_method(:partition_scope_value) do
+ strong_memoize(:partition_scope_value) do
+ record = scope.to_proc.call(self)
+ record.respond_to?(:partition_id) ? record.partition_id : record
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ci/track_environment_usage.rb b/app/models/concerns/ci/track_environment_usage.rb
new file mode 100644
index 00000000000..45d9cdeeb59
--- /dev/null
+++ b/app/models/concerns/ci/track_environment_usage.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module TrackEnvironmentUsage
+ extend ActiveSupport::Concern
+
+ def track_deployment_usage
+ return unless user_id.present? && count_user_deployment?
+
+ Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id)
+ end
+
+ def track_verify_environment_usage
+ return unless user_id.present? && verifies_environment?
+
+ Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_verify_environment_job', user_id)
+ end
+
+ def verifies_environment?
+ has_environment? && environment_action == 'verify'
+ end
+
+ def count_user_deployment?
+ deployment_name?
+ end
+
+ def deployment_name?
+ self.class::DEPLOYMENT_NAMES.any? { |n| name.downcase.include?(n) }
+ end
+ end
+end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 65cf3246d11..64d178b7507 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -65,6 +65,10 @@ module CounterAttribute
def counter_attribute_after_flush(&callback)
after_flush_callbacks << callback
end
+
+ def counter_attribute_enabled?(attribute)
+ counter_attributes.include?(attribute)
+ end
end
# This method must only be called by FlushCounterIncrementsWorker
@@ -103,16 +107,14 @@ module CounterAttribute
end
def delayed_increment_counter(attribute, increment)
+ raise ArgumentError, "#{attribute} is not a counter attribute" unless counter_attribute_enabled?(attribute)
+
return if increment == 0
run_after_commit_or_now do
- if counter_attribute_enabled?(attribute)
- increment_counter(attribute, increment)
+ increment_counter(attribute, increment)
- FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
- else
- legacy_increment!(attribute, increment)
- end
+ FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
end
true
@@ -157,7 +159,7 @@ module CounterAttribute
end
def counter_attribute_enabled?(attribute)
- self.class.counter_attributes.include?(attribute)
+ self.class.counter_attribute_enabled?(attribute)
end
private
@@ -168,10 +170,6 @@ module CounterAttribute
end
end
- def legacy_increment!(attribute, increment)
- increment!(attribute, increment)
- end
-
def unsafe_update_counters(id, increments)
self.class.update_counters(id, increments)
end
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index ecb120d8013..9de2da5aac3 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -19,7 +19,7 @@ module Enums
unmet_prerequisites: 10,
scheduler_failure: 11,
data_integrity_failure: 12,
- forward_deployment_failure: 13,
+ forward_deployment_failure: 13, # Deprecated in favor of failed_outdated_deployment_job.
user_blocked: 14,
project_deleted: 15,
ci_quota_exceeded: 16,
@@ -29,6 +29,7 @@ module Enums
builds_disabled: 20,
environment_creation_failure: 21,
deployment_rejected: 22,
+ failed_outdated_deployment_job: 23,
protected_environment_failure: 1_000,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
@@ -39,7 +40,8 @@ module Enums
downstream_pipeline_creation_failed: 1_007,
secrets_provider_not_found: 1_008,
reached_max_descendant_pipelines_depth: 1_009,
- ip_restriction_failure: 1_010
+ ip_restriction_failure: 1_010,
+ reached_max_pipeline_hierarchy_size: 1_011
}
end
end
diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb
index 71c86bab136..a8227363a22 100644
--- a/app/models/concerns/enums/internal_id.rb
+++ b/app/models/concerns/enums/internal_id.rb
@@ -16,7 +16,8 @@ module Enums
alert_management_alerts: 8,
sprints: 9, # iterations
design_management_designs: 10,
- incident_management_oncall_schedules: 11
+ incident_management_oncall_schedules: 11,
+ ml_experiments: 12
}
end
end
diff --git a/app/models/concerns/from_set_operator.rb b/app/models/concerns/from_set_operator.rb
index ce3a83e9fa1..56b788eb1ab 100644
--- a/app/models/concerns/from_set_operator.rb
+++ b/app/models/concerns/from_set_operator.rb
@@ -10,7 +10,9 @@ module FromSetOperator
raise "Trying to redefine method '#{method(method_name)}'" if methods.include?(method_name)
- define_method(method_name) do |members, remove_duplicates: true, remove_order: true, alias_as: table_name|
+ define_method(method_name) do |*members, remove_duplicates: true, remove_order: true, alias_as: table_name|
+ members = flatten_ar_array(members)
+
operator_sql =
if members.any?
operator.new(members, remove_duplicates: remove_duplicates, remove_order: remove_order).to_sql
@@ -20,5 +22,26 @@ module FromSetOperator
from(Arel.sql("(#{operator_sql}) #{alias_as}"))
end
+
+ # Array#flatten with ActiveRecord::Relation items will load the ActiveRecord::Relation.
+ # Therefore we need to roll our own flatten method.
+ unless method_defined?(:flatten_ar_array) # rubocop:disable Style/GuardClause
+ define_method :flatten_ar_array do |ary|
+ arrays = ary.dup
+ result = []
+
+ until arrays.empty?
+ item = arrays.shift
+ if item.is_a?(Array)
+ arrays.concat(item.dup)
+ else
+ result.push(item)
+ end
+ end
+
+ result
+ end
+ private :flatten_ar_array
+ end
end
end
diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb
index 142e62bb501..1ecddc015ab 100644
--- a/app/models/concerns/integrations/slack_mattermost_notifier.rb
+++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb
@@ -21,13 +21,13 @@ module Integrations
)
responses.each do |response|
- unless response.success?
- log_error('SlackMattermostNotifier HTTP error response',
- request_host: response.request.uri.host,
- response_code: response.code,
- response_body: response.body
- )
- end
+ next if response.success?
+
+ log_error('SlackMattermostNotifier HTTP error response',
+ request_host: response.request.uri.host,
+ response_code: response.code,
+ response_body: response.body
+ )
end
end
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
index 18ec1c253e1..412b1da55da 100644
--- a/app/models/concerns/merge_request_reviewer_state.rb
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -6,20 +6,11 @@ module MergeRequestReviewerState
included do
enum state: {
unreviewed: 0,
- reviewed: 1,
- attention_requested: 2
+ reviewed: 1
}
validates :state,
presence: true,
inclusion: { in: self.states.keys }
-
- belongs_to :updated_state_by, class_name: 'User', foreign_key: :updated_state_by_user_id
-
- def attention_requested_by
- return unless attention_requested?
-
- updated_state_by
- end
end
end
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
index 813827478da..335fcec2611 100644
--- a/app/models/concerns/pg_full_text_searchable.rb
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -108,6 +108,7 @@ module PgFullTextSearchable
# This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs
# See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920
search_term = remove_url_scheme(search_term)
+ search_term = ActiveSupport::Inflector.transliterate(search_term)
joins(:search_data).where(
Arel::Nodes::InfixOperation.new(
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 7613691bc2e..2976b6f02a7 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -86,6 +86,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:operations_access_level, value)
end
+ def monitor_access_level=(value)
+ write_feature_attribute_string(:monitor_access_level, value)
+ end
+
def security_and_compliance_access_level=(value)
write_feature_attribute_string(:security_and_compliance_access_level, value)
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 65fb62a814f..eccb004b503 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -43,6 +43,33 @@ module Sortable
}
end
+ def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
+ reversed_direction = direction == :asc ? :desc : :asc
+
+ # rubocop: disable GitlabSecurity/PublicSend
+ order = ::Gitlab::Pagination::Keyset::Order.build(
+ [
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: attribute_name,
+ column_expression: column,
+ order_expression: column.send(direction).send(nullable),
+ reversed_order_expression: column.send(reversed_direction).send(nullable),
+ order_direction: direction,
+ distinct: false,
+ add_to_projections: true,
+ nullable: nullable
+ ),
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table['id'].desc
+ )
+ ]
+ )
+ # rubocop: enable GitlabSecurity/PublicSend
+
+ order.apply_cursor_conditions(scope).reorder(order)
+ end
+
private
def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index e10452c1081..14520b2da26 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -263,10 +263,10 @@ class ContainerRepository < ApplicationRecord
.with_migration_import_started_at_nil_or_before(before_timestamp)
union = ::Gitlab::SQL::Union.new([
- stale_pre_importing,
- stale_pre_import_done,
- stale_importing
- ])
+ stale_pre_importing,
+ stale_pre_import_done,
+ stale_importing
+ ])
from("(#{union.to_sql}) #{ContainerRepository.table_name}")
end
@@ -598,6 +598,7 @@ class ContainerRepository < ApplicationRecord
tags_response_body.map do |raw_tag|
tag = ContainerRegistry::Tag.new(self, raw_tag['name'])
tag.force_created_at_from_iso8601(raw_tag['created_at'])
+ tag.updated_at = raw_tag['updated_at']
tag
end
end
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index f6455da890b..16c741d340f 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -79,22 +79,23 @@ class CustomerRelations::Contact < ApplicationRecord
end
def self.sort_by_name
- order(Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'last_name',
- order_expression: arel_table[:last_name].asc,
- distinct: false
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'first_name',
- order_expression: arel_table[:first_name].asc,
- distinct: false
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'id',
- order_expression: arel_table[:id].asc
- )
- ]))
+ order(Gitlab::Pagination::Keyset::Order.build(
+ [
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'last_name',
+ order_expression: arel_table[:last_name].asc,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'first_name',
+ order_expression: arel_table[:first_name].asc,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table[:id].asc
+ )
+ ]))
end
def self.find_ids_by_emails(group, emails)
@@ -117,22 +118,14 @@ class CustomerRelations::Contact < ApplicationRecord
JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id
SQL
- connection.execute(sanitize_sql([
- update_query,
- old_group_id: group.root_ancestor.id,
- new_group_id: group.id
- ]))
+ connection.execute(sanitize_sql([update_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
dupes_query = <<~SQL
DELETE FROM #{table_name} AS existing_contacts
USING #{table_name} AS new_contacts
WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
SQL
- connection.execute(sanitize_sql([
- dupes_query,
- old_group_id: group.root_ancestor.id,
- new_group_id: group.id
- ]))
+ connection.execute(sanitize_sql([dupes_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
where(group: group).update_all(group_id: group.root_ancestor.id)
end
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index 705e84250c9..5eda9b4bf15 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -23,6 +23,9 @@ class CustomerRelations::Organization < ApplicationRecord
validates :description, length: { maximum: 1024 }
validate :validate_root_group
+ scope :order_scope_asc, ->(field) { order(arel_table[field].asc.nulls_last) }
+ scope :order_scope_desc, ->(field) { order(arel_table[field].desc.nulls_last) }
+
# Searches for organizations with a matching name or description.
#
# This method uses ILIKE on PostgreSQL
@@ -38,6 +41,14 @@ class CustomerRelations::Organization < ApplicationRecord
where(state: state)
end
+ def self.sort_by_field(field, direction)
+ if direction == :asc
+ order_scope_asc(field)
+ else
+ order_scope_desc(field)
+ end
+ end
+
def self.sort_by_name
order(name: :asc)
end
@@ -55,28 +66,30 @@ class CustomerRelations::Organization < ApplicationRecord
JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id
SQL
- connection.execute(sanitize_sql([
- update_query,
- old_group_id: group.root_ancestor.id,
- new_group_id: group.id
- ]))
+ connection.execute(sanitize_sql([update_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
dupes_query = <<~SQL
DELETE FROM #{table_name} AS existing_organizations
USING #{table_name} AS new_organizations
WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
SQL
- connection.execute(sanitize_sql([
- dupes_query,
- old_group_id: group.root_ancestor.id,
- new_group_id: group.id
- ]))
+ connection.execute(sanitize_sql([dupes_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
where(group: group).update_all(group_id: group.root_ancestor.id)
end
+ def self.counts_by_state
+ default_state_counts.merge(group(:state).count)
+ end
+
private
+ def self.default_state_counts
+ states.keys.each_with_object({}) do |key, memo|
+ memo[key] = 0
+ end
+ end
+
def validate_root_group
return if group&.root?
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index a3213a59bed..dafcbc593be 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -18,7 +18,7 @@ class Deployment < ApplicationRecord
belongs_to :environment, optional: false
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user
- belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :deployable, polymorphic: true, optional: true, inverse_of: :deployment # rubocop:disable Cop/PolymorphicAssociations
has_many :deployment_merge_requests
has_many :merge_requests,
@@ -36,6 +36,7 @@ class Deployment < ApplicationRecord
delegate :name, to: :environment, prefix: true
delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true
+ scope :for_iid, -> (project, iid) { where(project: project, iid: iid) }
scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :for_environment_name, -> (project, name) do
where('deployments.environment_id = (?)',
@@ -58,9 +59,11 @@ class Deployment < ApplicationRecord
scope :finished_before, ->(date) { where('finished_at < ?', date) }
scope :ordered, -> { order(finished_at: :desc) }
+ scope :ordered_as_upcoming, -> { order(id: :desc) }
VISIBLE_STATUSES = %i[running success failed canceled blocked].freeze
FINISHED_STATUSES = %i[success failed canceled].freeze
+ UPCOMING_STATUSES = %i[created blocked running].freeze
state_machine :status, initial: :created do
event :run do
@@ -220,6 +223,10 @@ class Deployment < ApplicationRecord
Ci::Build.where(id: deployable_ids)
end
+ def build
+ deployable if deployable.is_a?(::Ci::Build)
+ end
+
class << self
##
# FastDestroyAll concerns
@@ -310,6 +317,16 @@ class Deployment < ApplicationRecord
project.repository.ancestor?(ancestor_sha, sha)
end
+ def older_than_last_successful_deployment?
+ last_deployment_id = environment.last_deployment&.id
+
+ return false unless last_deployment_id.present?
+
+ return false if self.id == last_deployment_id
+
+ self.id < last_deployment_id
+ end
+
def update_merge_request_metrics!
return unless environment.production? && success?
@@ -436,6 +453,12 @@ class Deployment < ApplicationRecord
deployable.environment_tier_from_options
end
+ # default tag limit is 100, 0 means no limit
+ def tags(limit: 100)
+ project.repository.tag_names_contains(sha, limit: limit)
+ end
+ strong_memoize_attr :tags
+
private
def update_status!(status)
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 1950431446b..4b98cd02e3b 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -13,6 +13,7 @@ class Environment < ApplicationRecord
self.reactive_cache_work_type = :external_dependency
belongs_to :project, optional: false
+ belongs_to :merge_request, optional: true
use_fast_destroy :all_deployments
nullify_if_blank :external_url
@@ -30,6 +31,16 @@ class Environment < ApplicationRecord
has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment
has_one :last_visible_deployment, -> { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
+ Deployment::FINISHED_STATUSES.each do |status|
+ has_one :"last_#{status}_deployment", -> { where(status: status).ordered },
+ class_name: 'Deployment', inverse_of: :environment
+ end
+
+ Deployment::UPCOMING_STATUSES.each do |status|
+ has_one :"last_#{status}_deployment", -> { where(status: status).ordered_as_upcoming },
+ class_name: 'Deployment', inverse_of: :environment
+ end
+
has_one :upcoming_deployment, -> { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
@@ -58,6 +69,7 @@ class Environment < ApplicationRecord
allow_nil: true
validate :safe_external_url
+ validate :merge_request_not_changed
delegate :manual_actions, :other_manual_actions, to: :last_deployment, allow_nil: true
delegate :auto_rollback_enabled?, to: :project
@@ -84,11 +96,12 @@ class Environment < ApplicationRecord
# Search environments which have names like the given query.
# Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
scope :for_name_like, -> (query, limit: 5) do
- where(arel_table[:name].matches("#{sanitize_sql_like query}%")).limit(limit)
+ where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit)
end
scope :for_project, -> (project) { where(project_id: project) }
scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) }
+ scope :for_type, -> (type) { where(environment_type: type) }
scope :unfoldered, -> { where(environment_type: nil) }
scope :with_rank, -> do
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
@@ -431,9 +444,13 @@ class Environment < ApplicationRecord
return unless value
parser = ::Gitlab::Ci::Build::DurationParser.new(value)
- return if parser.seconds_from_now.nil?
+
+ return if parser.seconds_from_now.nil? && auto_stop_at.nil?
self.auto_stop_at = parser.seconds_from_now
+ rescue ChronicDuration::DurationParseError => ex
+ Gitlab::ErrorTracking.track_exception(ex, project_id: self.project_id, environment_id: self.id)
+ raise ex
end
def rollout_status
@@ -509,6 +526,12 @@ class Environment < ApplicationRecord
self.tier ||= guess_tier
end
+ def merge_request_not_changed
+ if merge_request_id_changed? && persisted?
+ errors.add(:merge_request, 'merge_request cannot be changed')
+ end
+ end
+
# Guessing the tier of the environment if it's not explicitly specified by users.
# See https://en.wikipedia.org/wiki/Deployment_environment for industry standard deployment environments
def guess_tier
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 43b2c7899a1..d06d0a99948 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -100,7 +100,7 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
+ pipeline.environments_in_self_and_project_descendants.includes(:project).available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 4953f24755c..12d73ef0d72 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -23,6 +23,7 @@ module ErrorTracking
self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] }
self.reactive_cache_work_type = :external_dependency
+ self.reactive_cache_hard_limit = ErrorTracking::SentryClient::RESPONSE_SIZE_LIMIT
self.table_name = 'project_error_tracking_settings'
@@ -103,9 +104,18 @@ module ErrorTracking
api_host
end
+ def sentry_response_limit_enabled?
+ Feature.enabled?(:error_tracking_sentry_limit, project)
+ end
+
+ def reactive_cache_limit_enabled?
+ sentry_response_limit_enabled?
+ end
+
def sentry_client
strong_memoize(:sentry_client) do
- ::ErrorTracking::SentryClient.new(api_url, token)
+ ::ErrorTracking::SentryClient
+ .new(api_url, token, validate_size_guarded_by_feature_flag: sentry_response_limit_enabled?)
end
end
@@ -127,14 +137,14 @@ module ErrorTracking
def issue_details(opts = {})
with_reactive_cache('issue_details', opts.stringify_keys) do |result|
- ensure_issue_belongs_to_project!(result[:issue].project_id)
+ ensure_issue_belongs_to_project!(result[:issue].project_id) if result[:issue]
result
end
end
def issue_latest_event(opts = {})
with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result|
- ensure_issue_belongs_to_project!(result[:latest_event].project_id)
+ ensure_issue_belongs_to_project!(result[:latest_event].project_id) if result[:latest_event]
result
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 55455d85531..1445e71b0bc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -153,7 +153,7 @@ class Group < Namespace
after_create :post_create_hook
after_destroy :post_destroy_hook
- after_save :update_two_factor_requirement
+ after_commit :update_two_factor_requirement
after_update :path_changed_hook, if: :saved_change_to_path?
after_create -> { create_or_load_association(:group_feature) }
@@ -186,6 +186,27 @@ class Group < Namespace
where(project_creation_level: permitted_levels)
end
+ scope :shared_into_ancestors, -> (group) do
+ joins(:shared_group_links)
+ .where(group_group_links: { shared_group_id: group.self_and_ancestors })
+ end
+
+ # WARNING: This method should never be used on its own
+ # please do make sure the number of rows you are filtering is small
+ # enough for this query
+ #
+ # It's a replacement for `public_or_visible_to_user` that correctly
+ # supports subgroup permissions
+ scope :accessible_to_user, -> (user) do
+ if user
+ Preloaders::GroupPolicyPreloader.new(self, user).execute
+
+ select { |group| user.can?(:read_group, group) }
+ else
+ public_to_user
+ end
+ end
+
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'
@@ -614,11 +635,11 @@ class Group < Namespace
# 4. They belong to an ancestor group
def direct_and_indirect_users
User.from_union([
- User
- .where(id: direct_and_indirect_members.select(:user_id))
- .reorder(nil),
- project_users_with_descendants
- ])
+ User
+ .where(id: direct_and_indirect_members.select(:user_id))
+ .reorder(nil),
+ project_users_with_descendants
+ ])
end
# Returns all users (also inactive) that are members of the group because:
@@ -628,11 +649,11 @@ class Group < Namespace
# 4. They belong to an ancestor group
def direct_and_indirect_users_with_inactive
User.from_union([
- User
- .where(id: direct_and_indirect_members_with_inactive.select(:user_id))
- .reorder(nil),
- project_users_with_descendants
- ])
+ User
+ .where(id: direct_and_indirect_members_with_inactive.select(:user_id))
+ .reorder(nil),
+ project_users_with_descendants
+ ])
end
def users_count
@@ -672,14 +693,6 @@ class Group < Namespace
}
end
- def ci_variables_for(ref, project, environment: nil)
- cache_key = "ci_variables_for:group:#{self&.id}:project:#{project&.id}:ref:#{ref}:environment:#{environment}"
-
- ::Gitlab::SafeRequestStore.fetch(cache_key) do
- uncached_ci_variables_for(ref, project, environment: environment)
- end
- end
-
def member(user)
if group_members.loaded?
group_members.find { |gm| gm.user_id == user.id }
@@ -890,6 +903,18 @@ class Group < Namespace
end
end
+ def packages_policy_subject
+ if Feature.enabled?(:read_package_policy_rule, self)
+ ::Packages::Policies::Group.new(self)
+ else
+ self
+ end
+ end
+
+ def update_two_factor_requirement_for_members
+ direct_and_indirect_members.find_each(&:update_two_factor_requirement)
+ end
+
private
def feature_flag_enabled_for_self_or_ancestor?(feature_flag)
@@ -912,7 +937,7 @@ class Group < Namespace
def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
- direct_and_indirect_members.find_each(&:update_two_factor_requirement)
+ Groups::UpdateTwoFactorRequirementForMembersWorker.perform_async(self.id)
end
def path_changed_hook
@@ -1031,26 +1056,6 @@ class Group < Namespace
def enable_shared_runners!
update!(shared_runners_enabled: true)
end
-
- def uncached_ci_variables_for(ref, project, environment: nil)
- list_of_ids = if root_ancestor.use_traversal_ids?
- [self] + ancestors(hierarchy_order: :asc)
- else
- [self] + ancestors
- end
-
- variables = Ci::GroupVariable.where(group: list_of_ids)
- variables = variables.unprotected unless project.protected_for?(ref)
-
- variables = if environment
- variables.on_environment(environment)
- else
- variables.where(environment_scope: '*')
- end
-
- variables = variables.group_by(&:group_id)
- list_of_ids.reverse.flat_map { |group| variables[group.id] }.compact
- end
end
Group.prepend_mod_with('Group')
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index 8dd245a6ab5..7005c8593bd 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -19,6 +19,10 @@ class GroupGroupLink < ApplicationRecord
where(group_access: [Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER])
end
+ scope :with_owner_access, -> do
+ where(group_access: [Gitlab::Access::OWNER])
+ end
+
scope :groups_accessible_via, -> (shared_with_group_ids) do
links = where(shared_with_group_id: shared_with_group_ids)
# a group share also gives you access to the descendants of the group being shared,
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index 24e5f193a32..3fc3f193f19 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -25,7 +25,7 @@ class WebHookLog < ApplicationRecord
before_save :redact_author_email
def self.recent
- where('created_at >= ?', 2.days.ago.beginning_of_day)
+ where(created_at: 2.days.ago.beginning_of_day..Time.zone.now)
.order(created_at: :desc)
end
diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb
index d30d6906e14..dd0d3c6585d 100644
--- a/app/models/incident_management/timeline_event.rb
+++ b/app/models/incident_management/timeline_event.rb
@@ -20,6 +20,6 @@ module IncidentManagement
validates :action, presence: true, length: { maximum: 128 }
validates :note, :note_html, presence: true, length: { maximum: 10_000 }
- scope :order_occurred_at_asc, -> { reorder(occurred_at: :asc) }
+ scope :order_occurred_at_asc_id_asc, -> { reorder(occurred_at: :asc, id: :asc) }
end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 6d755016380..aecf9529a14 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -401,9 +401,9 @@ class Integration < ApplicationRecord
.or(where(type: integration.type, instance: true)).select(:id)
from_union([
- where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
- where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
- ])
+ where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
+ where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
+ ])
end
def activated?
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index bb0fb6b9079..4479725a33b 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -10,7 +10,7 @@ module Integrations
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
- pipeline job
+ pipeline job archive_trace
].freeze
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
@@ -38,14 +38,6 @@ module Integrations
SUPPORTED_EVENTS
end
- def supported_events
- events = super
-
- return events + ['archive_trace'] if Feature.enabled?(:datadog_integration_logs_collection, parent)
-
- events
- end
-
def self.default_test_event
'pipeline'
end
@@ -77,7 +69,7 @@ module Integrations
end
def fields
- f = [
+ [
{
type: 'text',
name: 'datadog_site',
@@ -110,21 +102,15 @@ module Integrations
linkClose: '</a>'.html_safe
},
required: true
- }
- ]
-
- if Feature.enabled?(:datadog_integration_logs_collection, parent)
- f.append({
+ },
+ {
type: 'checkbox',
name: 'archive_trace_events',
title: s_('Logs'),
checkbox_label: s_('Enable logs collection'),
help: s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.'),
required: false
- })
- end
-
- f += [
+ },
{
type: 'text',
name: 'datadog_service',
@@ -161,8 +147,6 @@ module Integrations
}
}
]
-
- f
end
override :hook_url
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index ec8a12e4760..d0389b82410 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -6,6 +6,24 @@ module Integrations
class Discord < BaseChatNotification
ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
+ undef :notify_only_broken_pipelines
+
+ field :webhook,
+ section: SECTION_TYPE_CONNECTION,
+ placeholder: 'https://discordapp.com/api/webhooks/…',
+ help: 'URL to the webhook for the Discord channel.',
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION
+
+ field :branches_to_be_notified,
+ type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
def title
s_("DiscordService|Discord Notifications")
end
@@ -18,6 +36,10 @@ module Integrations
"discord"
end
+ def fields
+ self.class.fields + build_event_channels
+ end
+
def help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
@@ -31,30 +53,6 @@ module Integrations
%w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
- def default_fields
- [
- {
- type: 'text',
- section: SECTION_TYPE_CONNECTION,
- name: 'webhook',
- placeholder: 'https://discordapp.com/api/webhooks/…',
- help: 'URL to the webhook for the Discord channel.'
- },
- {
- type: 'checkbox',
- section: SECTION_TYPE_CONFIGURATION,
- name: 'notify_only_broken_pipelines'
- },
- {
- type: 'select',
- section: SECTION_TYPE_CONFIGURATION,
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }
- ]
- end
-
def sections
[
{
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index df112ad6ca8..6e7f31aa030 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -47,8 +47,31 @@ module Integrations
private
def notify(message, opts)
+ url = webhook.dup
+
+ key = parse_thread_key(message)
+ url = Gitlab::Utils.add_url_parameters(url, { threadKey: key }) if key
+
simple_text = parse_simple_text_message(message)
- ::HangoutsChat::Sender.new(webhook).simple(simple_text)
+ ::HangoutsChat::Sender.new(url).simple(simple_text)
+ end
+
+ # Returns an appropriate key for threading messages in google chat
+ def parse_thread_key(message)
+ case message
+ when Integrations::ChatMessage::NoteMessage
+ message.target
+ when Integrations::ChatMessage::IssueMessage
+ "issue #{Issue.reference_prefix}#{message.issue_iid}"
+ when Integrations::ChatMessage::MergeMessage
+ "merge request #{MergeRequest.reference_prefix}#{message.merge_request_iid}"
+ when Integrations::ChatMessage::PushMessage
+ "push #{message.project_name}_#{message.ref}"
+ when Integrations::ChatMessage::PipelineMessage
+ "pipeline #{message.pipeline_id}"
+ when Integrations::ChatMessage::WikiPageMessage
+ "wiki_page #{message.wiki_page_url}"
+ end
end
def parse_simple_text_message(message)
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 03913a71d47..58eabcfd378 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -24,6 +24,10 @@ module Integrations
s_("HarborIntegration|After the Harbor integration is activated, global variables '$HARBOR_USERNAME', '$HARBOR_HOST', '$HARBOR_OCI', '$HARBOR_PASSWORD', '$HARBOR_URL' and '$HARBOR_PROJECT' will be created for CI/CD use.")
end
+ def hostname
+ Gitlab::Utils.parse_url(url).hostname
+ end
+
class << self
def to_param
name.demodulize.downcase
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index 8bc296e0320..f5b6595fff2 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -9,8 +9,6 @@ module Integrations
required: true
def render?
- return false unless Feature.enabled?(:shimo_integration, project)
-
valid? && activated?
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index b502d5e354d..d141061062a 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -143,10 +143,7 @@ class InternalId < ApplicationRecord
def track_greatest(new_value)
InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
- function = Arel::Nodes::NamedFunction.new('GREATEST', [
- arel_table[:last_value],
- new_value.to_i
- ])
+ function = Arel::Nodes::NamedFunction.new('GREATEST', [arel_table[:last_value], new_value.to_i])
next_iid = update_record!(subject, scope, usage, function)
return next_iid if next_iid
diff --git a/app/models/issue.rb b/app/models/issue.rb
index df8ee34b3c3..153747c75df 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -254,31 +254,6 @@ class Issue < ApplicationRecord
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
- def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
- reversed_direction = direction == :asc ? :desc : :asc
-
- # rubocop: disable GitlabSecurity/PublicSend
- order = ::Gitlab::Pagination::Keyset::Order.build([
- ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: attribute_name,
- column_expression: column,
- order_expression: column.send(direction).send(nullable),
- reversed_order_expression: column.send(reversed_direction).send(nullable),
- order_direction: direction,
- distinct: false,
- add_to_projections: true,
- nullable: nullable
- ),
- ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'id',
- order_expression: arel_table['id'].desc
- )
- ])
- # rubocop: enable GitlabSecurity/PublicSend
-
- order.apply_cursor_conditions(scope).order(order)
- end
-
override :order_upvotes_desc
def order_upvotes_desc
reorder(upvotes_count: :desc)
@@ -293,16 +268,6 @@ class Issue < ApplicationRecord
def pg_full_text_search(search_term)
super.where('issue_search_data.project_id = issues.project_id')
end
-
- override :full_search
- def full_search(query, matched_columns: nil, use_minimum_char_limit: true)
- return super if query.match?(IssuableFinder::FULL_TEXT_SEARCH_TERM_REGEX)
-
- super.where(
- 'issues.title NOT SIMILAR TO :pattern OR issues.description NOT SIMILAR TO :pattern',
- pattern: IssuableFinder::FULL_TEXT_SEARCH_TERM_PATTERN
- )
- end
end
def next_object_by_relative_position(ignoring: nil, order: :asc)
@@ -406,8 +371,6 @@ class Issue < ApplicationRecord
attribute_name: 'relative_position',
column_expression: arel_table[:relative_position],
order_expression: Issue.arel_table[:relative_position].asc.nulls_last,
- reversed_order_expression: Issue.arel_table[:relative_position].desc.nulls_last,
- order_direction: :asc,
nullable: :nulls_last,
distinct: false
)
@@ -695,11 +658,11 @@ class Issue < ApplicationRecord
return unless persisted?
if confidential? && WorkItems::ParentLink.has_public_children?(id)
- errors.add(:confidential, _('confidential parent can not be used if there are non-confidential children.'))
+ errors.add(:base, _('A confidential issue cannot have a parent that already has non-confidential children.'))
end
if !confidential? && WorkItems::ParentLink.has_confidential_parent?(id)
- errors.add(:confidential, _('associated parent is confidential and can not have non-confidential children.'))
+ errors.add(:base, _('A non-confidential issue cannot have a confidential parent.'))
end
end
@@ -722,7 +685,7 @@ class Issue < ApplicationRecord
end
def record_create_action
- Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author)
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_created_action(author: author, project: project)
end
# Returns `true` if this Issue is visible to everybody.
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 8befe9a9230..0a2d3ba0749 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -24,4 +24,10 @@ class JiraConnectInstallation < ApplicationRecord
def client
Atlassian::JiraConnect::Client.new(base_url, shared_secret)
end
+
+ def oauth_authorization_url
+ return Gitlab.config.gitlab.url if instance_url.blank? || Feature.disabled?(:jira_connect_oauth_self_managed)
+
+ instance_url
+ end
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index 94444f4b6d3..f28e8f81b40 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -12,7 +12,7 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
next_partition_if: -> (active_partition) do
oldest_record_in_partition = LooseForeignKeys::DeletedRecord
.select(:id, :created_at)
- .for_partition(active_partition)
+ .for_partition(active_partition.value)
.order(:id)
.limit(1)
.take
@@ -22,7 +22,7 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
end,
detach_partition_if: -> (partition) do
!LooseForeignKeys::DeletedRecord
- .for_partition(partition)
+ .for_partition(partition.value)
.status_pending
.exists?
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 0cd1e022617..c5351d5447b 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -60,6 +60,7 @@ class Member < ApplicationRecord
if: :project_bot?
validate :access_level_inclusion
validate :validate_member_role_access_level
+ validate :validate_access_level_locked_for_member_role, on: :update
scope :with_invited_user_state, -> do
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
@@ -73,10 +74,7 @@ class Member < ApplicationRecord
projects = source.root_ancestor.all_projects
project_members = Member.default_scoped.where(source: projects).select(*Member.cached_column_list)
- Member.default_scoped.from_union([
- group_members,
- project_members
- ]).merge(self)
+ Member.default_scoped.from_union([group_members, project_members]).merge(self)
end
scope :excluding_users, ->(user_ids) do
@@ -186,14 +184,85 @@ class Member < ApplicationRecord
unscoped.from(distinct_members, :members)
end
- scope :order_name_asc, -> { left_join_users.reorder(User.arel_table[:name].asc.nulls_last) }
- scope :order_name_desc, -> { left_join_users.reorder(User.arel_table[:name].desc.nulls_last) }
- scope :order_recent_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].desc.nulls_last) }
- scope :order_oldest_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].asc.nulls_last) }
- scope :order_recent_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].desc.nulls_last) }
- scope :order_oldest_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].asc.nulls_first) }
- scope :order_recent_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].desc.nulls_last) }
- scope :order_oldest_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].asc.nulls_first) }
+ scope :order_name_asc, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_full_name',
+ column: User.arel_table[:name],
+ direction: :asc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_name_desc, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_full_name',
+ column: User.arel_table[:name],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_oldest_sign_in, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_sign_in_at',
+ column: User.arel_table[:last_sign_in_at],
+ direction: :asc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_recent_sign_in, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_sign_in_at',
+ column: User.arel_table[:last_sign_in_at],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_oldest_last_activity, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_activity_on',
+ column: User.arel_table[:last_activity_on],
+ direction: :asc,
+ nullable: :nulls_first
+ )
+ end
+
+ scope :order_recent_last_activity, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_activity_on',
+ column: User.arel_table[:last_activity_on],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_oldest_created_user, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_created_at',
+ column: User.arel_table[:created_at],
+ direction: :asc,
+ nullable: :nulls_first
+ )
+ end
+
+ scope :order_recent_created_user, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_created_at',
+ column: User.arel_table[:created_at],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
@@ -438,6 +507,14 @@ class Member < ApplicationRecord
end
end
+ def validate_access_level_locked_for_member_role
+ return unless member_role_id
+
+ if access_level_changed?
+ errors.add(:access_level, _("cannot be changed since member is associated with a custom role"))
+ end
+ end
+
def send_invite
# override in subclass
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 3c06e1aa983..a57cb97e936 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -20,7 +20,7 @@ class MergeRequest < ApplicationRecord
include IgnorableColumns
include MilestoneEventable
include StateEventable
- include ApprovableBase
+ include Approvable
include IdInOrdered
include Todoable
@@ -67,6 +67,8 @@ class MergeRequest < ApplicationRecord
has_one :merge_head_diff,
-> { merge_head }, inverse_of: :merge_request, class_name: 'MergeRequestDiff'
has_one :cleanup_schedule, inverse_of: :merge_request
+ has_one :predictions, inverse_of: :merge_request
+ delegate :suggested_reviewers, to: :predictions
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
manual_inverse_association :latest_merge_request_diff, :merge_request
@@ -116,6 +118,7 @@ class MergeRequest < ApplicationRecord
has_many :draft_notes
has_many :reviews, inverse_of: :merge_request
+ has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request
KNOWN_MERGE_PARAMS = [
:auto_merge_strategy,
@@ -343,23 +346,24 @@ class MergeRequest < ApplicationRecord
column_expression = MergeRequest::Metrics.arel_table[metric]
column_expression_with_direction = direction == 'ASC' ? column_expression.asc : column_expression.desc
- order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: "merge_request_metrics_#{metric}",
- column_expression: column_expression,
- order_expression: column_expression_with_direction.nulls_last,
- reversed_order_expression: column_expression_with_direction.reverse.nulls_first,
- order_direction: direction,
- nullable: :nulls_last,
- distinct: false,
- add_to_projections: true
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'merge_request_metrics_id',
- order_expression: MergeRequest::Metrics.arel_table[:id].desc,
- add_to_projections: true
- )
- ])
+ order = Gitlab::Pagination::Keyset::Order.build(
+ [
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: "merge_request_metrics_#{metric}",
+ column_expression: column_expression,
+ order_expression: column_expression_with_direction.nulls_last,
+ reversed_order_expression: column_expression_with_direction.reverse.nulls_first,
+ order_direction: direction,
+ nullable: :nulls_last,
+ distinct: false,
+ add_to_projections: true
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'merge_request_metrics_id',
+ order_expression: MergeRequest::Metrics.arel_table[:id].desc,
+ add_to_projections: true
+ )
+ ])
order.apply_cursor_conditions(join_metrics).order(order)
end
@@ -417,17 +421,6 @@ class MergeRequest < ApplicationRecord
)
end
- scope :attention, ->(user) do
- # rubocop: disable Gitlab/Union
- union = Gitlab::SQL::Union.new([
- MergeRequestReviewer.select(:merge_request_id).where(user_id: user.id, state: MergeRequestReviewer.states[:attention_requested]),
- MergeRequestAssignee.select(:merge_request_id).where(user_id: user.id, state: MergeRequestAssignee.states[:attention_requested])
- ])
- # rubocop: enable Gitlab/Union
-
- with(Gitlab::SQL::CTE.new(:reviewers_and_assignees, union).to_arel).where('merge_requests.id in (select merge_request_id from reviewers_and_assignees)')
- end
-
def self.total_time_to_merge
join_metrics
.merge(MergeRequest::Metrics.with_valid_time_to_merge)
@@ -1187,41 +1180,13 @@ class MergeRequest < ApplicationRecord
]
end
- def detailed_merge_status
- if cannot_be_merged_rechecking? || preparing? || checking?
- return :checking
- elsif unchecked?
- return :unchecked
- end
-
- checks = execute_merge_checks
-
- if checks.success?
- :mergeable
- else
- checks.failure_reason
- end
- end
-
- # rubocop: disable CodeReuse/ServiceClass
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
- if Feature.enabled?(:improved_mergeability_checks, self.project)
- additional_checks = execute_merge_checks(params: {
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check
- })
- additional_checks.execute.success?
- else
- return false unless open?
- return false if draft?
- return false if broken?
- return false unless skip_discussions_check || mergeable_discussions_state?
- return false unless skip_ci_check || mergeable_ci_state?
-
- true
- end
+ additional_checks = execute_merge_checks(params: {
+ skip_ci_check: skip_ci_check,
+ skip_discussions_check: skip_discussions_check
+ })
+ additional_checks.success?
end
- # rubocop: enable CodeReuse/ServiceClass
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
@@ -1318,7 +1283,6 @@ class MergeRequest < ApplicationRecord
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author)
- return unless project.issues_enabled?
return if closed? || merged?
transaction do
@@ -1489,7 +1453,7 @@ class MergeRequest < ApplicationRecord
end
def environments_in_head_pipeline(deployment_status: nil)
- actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none
+ actual_head_pipeline&.environments_in_self_and_project_descendants(deployment_status: deployment_status) || Environment.none
end
def fetch_ref!
@@ -1589,7 +1553,7 @@ class MergeRequest < ApplicationRecord
end
def has_test_reports?
- actual_head_pipeline&.has_reports?(Ci::JobArtifact.test_reports)
+ actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test))
end
def predefined_variables
@@ -1619,7 +1583,7 @@ class MergeRequest < ApplicationRecord
end
def has_accessibility_reports?
- actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports)
+ actual_head_pipeline.present? && actual_head_pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:accessibility))
end
def has_coverage_reports?
@@ -1627,7 +1591,7 @@ class MergeRequest < ApplicationRecord
end
def has_terraform_reports?
- actual_head_pipeline&.has_reports?(Ci::JobArtifact.terraform_reports)
+ actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform))
end
def compare_accessibility_reports
@@ -1667,7 +1631,7 @@ class MergeRequest < ApplicationRecord
end
def has_codequality_reports?
- actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports)
+ actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end
def compare_codequality_reports
@@ -1717,11 +1681,11 @@ class MergeRequest < ApplicationRecord
end
def has_sast_reports?
- !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports)
+ !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:sast))
end
def has_secret_detection_reports?
- !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports)
+ !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:secret_detection))
end
def compare_sast_reports(current_user)
@@ -2019,6 +1983,12 @@ class MergeRequest < ApplicationRecord
false # Overridden in EE
end
+ def execute_merge_checks(params: {})
+ # rubocop: disable CodeReuse/ServiceClass
+ MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute
+ # rubocop: enable CodeReuse/ServiceClass
+ end
+
private
attr_accessor :skip_fetch_ref
@@ -2072,12 +2042,6 @@ class MergeRequest < ApplicationRecord
def report_type_enabled?(report_type)
!!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type)
end
-
- def execute_merge_checks(params: {})
- # rubocop: disable CodeReuse/ServiceClass
- MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute
- # rubocop: enable CodeReuse/ServiceClass
- end
end
MergeRequest.prepend_mod_with('MergeRequest')
diff --git a/app/models/merge_request/predictions.rb b/app/models/merge_request/predictions.rb
new file mode 100644
index 00000000000..ef9e00b5f74
--- /dev/null
+++ b/app/models/merge_request/predictions.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequest::Predictions < ApplicationRecord # rubocop:disable Style/ClassAndModuleChildren
+ belongs_to :merge_request, inverse_of: :predictions
+
+ validates :suggested_reviewers, json_schema: { filename: 'merge_request_predictions_suggested_reviewers' }
+end
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index fd8e5860040..be3a1d42eac 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class MergeRequestAssignee < ApplicationRecord
- include MergeRequestReviewerState
+ include IgnorableColumns
+ ignore_column %i[state updated_state_by_user_id], remove_with: '15.6', remove_after: '2022-10-22'
belongs_to :merge_request, touch: true
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
@@ -11,6 +12,6 @@ class MergeRequestAssignee < ApplicationRecord
scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
def cache_key
- [model_name.cache_key, id, state, assignee.cache_key]
+ [model_name.cache_key, id, assignee.cache_key]
end
end
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index 4abf0fa09f0..4b5b71481d3 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -2,6 +2,8 @@
class MergeRequestReviewer < ApplicationRecord
include MergeRequestReviewerState
+ include IgnorableColumns
+ ignore_column :updated_state_by_user_id, remove_with: '15.6', remove_after: '2022-10-22'
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index e181217f01c..29e1ba88528 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -2,11 +2,24 @@
module Ml
class Candidate < ApplicationRecord
+ enum status: { running: 0, scheduled: 1, finished: 2, failed: 3, killed: 4 }
+
validates :iid, :experiment, presence: true
+ validates :status, inclusion: { in: statuses.keys }
belongs_to :experiment, class_name: 'Ml::Experiment'
belongs_to :user
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
+
+ default_value_for(:iid) { SecureRandom.uuid }
+
+ class << self
+ def with_project_id_and_iid(project_id, iid)
+ return unless project_id.present? && iid.present?
+
+ joins(:experiment).find_by(experiment: { project_id: project_id }, iid: iid)
+ end
+ end
end
end
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index 7ef9c70ba7e..e4e9baac4c8 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -2,11 +2,33 @@
module Ml
class Experiment < ApplicationRecord
- validates :name, :iid, :project, presence: true
- validates :iid, :name, uniqueness: { scope: :project, message: "should be unique in the project" }
+ include AtomicInternalId
+
+ validates :name, :project, presence: true
+ validates :name, uniqueness: { scope: :project, message: "should be unique in the project" }
belongs_to :project
belongs_to :user
has_many :candidates, class_name: 'Ml::Candidate'
+
+ has_internal_id :iid, scope: :project
+
+ def artifact_location
+ 'not_implemented'
+ end
+
+ class << self
+ def by_project_id_and_iid(project_id, iid)
+ find_by(project_id: project_id, iid: iid)
+ end
+
+ def by_project_id_and_name(project_id, name)
+ find_by(project_id: project_id, name: name)
+ end
+
+ def has_record?(project_id, name)
+ where(project_id: project_id, name: name).exists?
+ end
+ end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 06f49f16d66..0ffd5c446d3 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -43,6 +43,8 @@ class Namespace < ApplicationRecord
# The first date in https://docs.gitlab.com/ee/user/usage_quotas.html#namespace-storage-limit-enforcement-schedule
# Determines when we start enforcing namespace storage
MIN_STORAGE_ENFORCEMENT_DATE = Date.new(2022, 10, 19)
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/367531
+ MIN_STORAGE_ENFORCEMENT_USAGE = 5.gigabytes
cache_markdown_field :description, pipeline: :description
@@ -59,7 +61,7 @@ class Namespace < ApplicationRecord
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
has_many :pending_builds, class_name: 'Ci::PendingBuild'
- has_one :onboarding_progress
+ has_one :onboarding_progress, class_name: 'Onboarding::Progress'
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
@@ -126,8 +128,9 @@ class Namespace < ApplicationRecord
delegate :avatar_url, to: :owner, allow_nil: true
delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=,
to: :namespace_settings, allow_nil: true
+ delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=,
+ to: :namespace_settings
- after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? }
after_save :reload_namespace_details
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
@@ -136,6 +139,7 @@ class Namespace < ApplicationRecord
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? }
+ after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear
# Legacy Storage specific hooks
@@ -172,13 +176,17 @@ class Namespace < ApplicationRecord
end
scope :sorted_by_similarity_and_parent_id_desc, -> (search) do
- order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
- { column: arel_table["path"], multiplier: 1 },
- { column: arel_table["name"], multiplier: 0.7 }
- ])
+ order_expression = Gitlab::Database::SimilarityScore.build_expression(
+ search: search,
+ rules: [
+ { column: arel_table["path"], multiplier: 1 },
+ { column: arel_table["name"], multiplier: 0.7 }
+ ])
reorder(order_expression.desc, Namespace.arel_table['parent_id'].desc.nulls_last, Namespace.arel_table['id'].desc)
end
+ scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) }
+
# Make sure that the name is same as strong_memoize name in root_ancestor
# method
attr_writer :root_ancestor, :emails_disabled_memoized
@@ -362,7 +370,7 @@ class Namespace < ApplicationRecord
end
def any_project_with_shared_runners_enabled?
- projects.with_shared_runners.any?
+ projects.with_shared_runners_enabled.any?
end
def user_ids_for_project_authorizations
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 595e34821af..6a87fba57ac 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -4,6 +4,11 @@ class NamespaceSetting < ApplicationRecord
include CascadingNamespaceSettingAttribute
include Sanitizable
include ChronicDurationAttribute
+ include IgnorableColumns
+
+ ignore_columns %i[exclude_from_free_user_cap include_for_free_user_cap_preview],
+ remove_with: '15.5',
+ remove_after: '2022-09-23'
cascading_attr :delayed_project_removal
@@ -53,8 +58,18 @@ class NamespaceSetting < ApplicationRecord
namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
end
+ def show_diff_preview_in_email?
+ return show_diff_preview_in_email unless namespace.has_parent?
+
+ all_ancestors_allow_diff_preview_in_email?
+ end
+
private
+ def all_ancestors_allow_diff_preview_in_email?
+ !self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists?
+ end
+
def normalize_default_branch_name
self.default_branch_name = default_branch_name.presence
end
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 687fa6a5334..16a9c20dfdc 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -47,6 +47,8 @@ module Namespaces
# This uses rails internal before_commit API to sync traversal_ids on namespace create, right before transaction is committed.
# This helps reduce the time during which the root namespace record is locked to ensure updated traversal_ids are valid
before_commit :sync_traversal_ids, on: [:create]
+
+ define_model_callbacks :sync_traversal_ids
end
class_methods do
@@ -208,10 +210,12 @@ module Namespaces
#
# NOTE: self.traversal_ids will be stale. Reload for a fresh record.
def sync_traversal_ids
- # Clear any previously memoized root_ancestor as our ancestors have changed.
- clear_memoization(:root_ancestor)
+ run_callbacks :sync_traversal_ids do
+ # Clear any previously memoized root_ancestor as our ancestors have changed.
+ clear_memoization(:root_ancestor)
- Namespace::TraversalHierarchy.for_namespace(self).sync_traversal_ids!
+ Namespace::TraversalHierarchy.for_namespace(self).sync_traversal_ids!
+ end
end
# Lock the root of the hierarchy we just left, and lock the root of the hierarchy
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 81ac026d7ff..843de9bce33 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -41,24 +41,13 @@ module Namespaces
def self_and_descendants(include_self: true)
return super unless use_traversal_ids_for_descendants_scopes?
- if Feature.enabled?(:traversal_ids_btree)
- self_and_descendants_with_comparison_operators(include_self: include_self)
- else
- records = self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
- distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
- distinct.normal_select
- end
+ self_and_descendants_with_comparison_operators(include_self: include_self)
end
def self_and_descendant_ids(include_self: true)
return super unless use_traversal_ids_for_descendants_scopes?
- if Feature.enabled?(:traversal_ids_btree)
- self_and_descendants_with_comparison_operators(include_self: include_self).as_ids
- else
- self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
- .select('DISTINCT namespaces.id')
- end
+ self_and_descendants(include_self: include_self).as_ids
end
def self_and_hierarchy
@@ -181,20 +170,6 @@ module Namespaces
Arel::Nodes::NamedFunction.new('unnest', args)
end
- def self_and_descendants_with_duplicates_with_array_operator(include_self: true)
- base_ids = select(:id)
-
- records = unscoped
- .from("namespaces, (#{base_ids.to_sql}) base")
- .where('namespaces.traversal_ids @> ARRAY[base.id]')
-
- if include_self
- records
- else
- records.where('namespaces.id <> base.id')
- end
- end
-
def superset_cte(base_name)
superset_sql = <<~SQL
SELECT d1.traversal_ids
diff --git a/app/models/note.rb b/app/models/note.rb
index 1715f7cdc3b..daac489757b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -164,6 +164,9 @@ class Note < ApplicationRecord
scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
before_validation :nullify_blank_type, :nullify_blank_line_code
+ # Syncs `confidential` with `internal` as we rename the column.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/367923
+ before_create :set_internal_flag
after_save :keep_around_commit, if: :for_project_noteable?, unless: -> { importing? || skip_keep_around_commits }
after_save :expire_etag_cache, unless: :importing?
after_save :touch_noteable, unless: :importing?
@@ -813,6 +816,10 @@ class Note < ApplicationRecord
def noteable_can_have_confidential_note?
for_issue?
end
+
+ def set_internal_flag
+ self.internal = confidential if confidential
+ end
end
Note.prepend_mod_with('Note')
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index b3eaed154e2..caa24377791 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -38,6 +38,7 @@ class NotificationRecipient
return !unsubscribed? if @type == :subscription
return false unless suitable_notification_level?
+ return false if email_blocked?
# check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed
@@ -95,6 +96,15 @@ class NotificationRecipient
end
end
+ def email_blocked?
+ return false if Feature.disabled?(:block_emails_with_failures)
+
+ recipient_email = user.notification_email_for(@group)
+
+ Gitlab::ApplicationRateLimiter.peek(:permanent_email_failure, scope: recipient_email) ||
+ Gitlab::ApplicationRateLimiter.peek(:temporary_email_failure, scope: recipient_email)
+ end
+
def has_access?
DeclarativePolicy.subject_scope do
break false unless user.can?(:receive_notifications)
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 7d71e15d3c5..eac99e8d441 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -26,4 +26,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
super
end
+
+ # Override Doorkeeper::AccessToken.matching_token_for since we
+ # have `reuse_access_tokens` disabled and we also hash tokens.
+ # This ensures we don't accidentally return a hashed token value.
+ def self.matching_token_for(application, resource_owner, scopes)
+ return if Feature.enabled?(:hash_oauth_tokens)
+
+ super
+ end
end
diff --git a/app/models/onboarding/completion.rb b/app/models/onboarding/completion.rb
new file mode 100644
index 00000000000..49fdb102209
--- /dev/null
+++ b/app/models/onboarding/completion.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Onboarding
+ class Completion
+ include Gitlab::Utils::StrongMemoize
+ include Gitlab::Experiment::Dsl
+
+ ACTION_ISSUE_IDS = {
+ pipeline_created: 7,
+ trial_started: 2,
+ required_mr_approvals_enabled: 11,
+ code_owners_enabled: 10
+ }.freeze
+
+ ACTION_PATHS = [
+ :issue_created,
+ :git_write,
+ :merge_request_created,
+ :user_added
+ ].freeze
+
+ def initialize(namespace, current_user = nil)
+ @namespace = namespace
+ @current_user = current_user
+ end
+
+ def percentage
+ return 0 unless onboarding_progress
+
+ attributes = onboarding_progress.attributes.symbolize_keys
+
+ total_actions = action_columns.count
+ completed_actions = action_columns.count { |column| attributes[column].present? }
+
+ (completed_actions.to_f / total_actions * 100).round
+ end
+
+ private
+
+ def onboarding_progress
+ strong_memoize(:onboarding_progress) do
+ ::Onboarding::Progress.find_by(namespace: namespace)
+ end
+ end
+
+ def action_columns
+ strong_memoize(:action_columns) do
+ tracked_actions.map { |action_key| ::Onboarding::Progress.column_name(action_key) }
+ end
+ end
+
+ def tracked_actions
+ ACTION_ISSUE_IDS.keys + ACTION_PATHS + deploy_section_tracked_actions
+ end
+
+ def deploy_section_tracked_actions
+ experiment(
+ :security_actions_continuous_onboarding,
+ namespace: namespace,
+ user: current_user,
+ sticky_to: current_user
+ ) do |e|
+ e.control { [:security_scan_enabled] }
+ e.candidate { [:license_scanning_run, :secure_dependency_scanning_run, :secure_dast_run] }
+ end.run
+ end
+
+ attr_reader :namespace, :current_user
+ end
+end
diff --git a/app/models/onboarding/learn_gitlab.rb b/app/models/onboarding/learn_gitlab.rb
new file mode 100644
index 00000000000..d7a189ed6e2
--- /dev/null
+++ b/app/models/onboarding/learn_gitlab.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Onboarding
+ class LearnGitlab
+ PROJECT_NAME = 'Learn GitLab'
+ PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial'
+ BOARD_NAME = 'GitLab onboarding'
+ LABEL_NAME = 'Novice'
+
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def available?
+ project && board && label
+ end
+
+ def project
+ @project ||= current_user.projects.find_by_name([PROJECT_NAME, PROJECT_NAME_ULTIMATE_TRIAL])
+ end
+
+ def board
+ return unless project
+
+ @board ||= project.boards.find_by_name(BOARD_NAME)
+ end
+
+ def label
+ return unless project
+
+ @label ||= project.labels.find_by_name(LABEL_NAME)
+ end
+
+ private
+
+ attr_reader :current_user
+ end
+end
diff --git a/app/models/onboarding/progress.rb b/app/models/onboarding/progress.rb
new file mode 100644
index 00000000000..ecc78418256
--- /dev/null
+++ b/app/models/onboarding/progress.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module Onboarding
+ class Progress < ApplicationRecord
+ self.table_name = 'onboarding_progresses'
+
+ belongs_to :namespace, optional: false
+
+ validate :namespace_is_root_namespace
+
+ ACTIONS = [
+ :git_pull,
+ :git_write,
+ :merge_request_created,
+ :pipeline_created,
+ :user_added,
+ :trial_started,
+ :subscription_created,
+ :required_mr_approvals_enabled,
+ :code_owners_enabled,
+ :scoped_label_created,
+ :security_scan_enabled,
+ :issue_created,
+ :issue_auto_closed,
+ :repository_imported,
+ :repository_mirrored,
+ :secure_dependency_scanning_run,
+ :secure_container_scanning_run,
+ :secure_dast_run,
+ :secure_secret_detection_run,
+ :secure_coverage_fuzzing_run,
+ :secure_api_fuzzing_run,
+ :secure_cluster_image_scanning_run,
+ :license_scanning_run
+ ].freeze
+
+ scope :incomplete_actions, ->(actions) do
+ Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) }
+ end
+
+ scope :completed_actions, ->(actions) do
+ Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) }
+ end
+
+ scope :completed_actions_with_latest_in_range, ->(actions, range) do
+ actions = Array(actions)
+ if actions.size == 1
+ where(column_name(actions[0]) => range)
+ else
+ action_columns = actions.map { |action| arel_table[column_name(action)] }
+ completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range))
+ end
+ end
+
+ class << self
+ def onboard(namespace)
+ return unless root_namespace?(namespace)
+
+ create(namespace: namespace)
+ end
+
+ def onboarding?(namespace)
+ where(namespace: namespace).any?
+ end
+
+ def register(namespace, actions)
+ actions = Array(actions)
+ return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty?
+
+ onboarding_progress = find_by(namespace: namespace)
+ return unless onboarding_progress
+
+ now = Time.current
+ nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? }
+ return if nil_actions.empty?
+
+ updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) }
+ onboarding_progress.update!(updates)
+ end
+
+ def completed?(namespace, action)
+ return unless root_namespace?(namespace) && ACTIONS.include?(action)
+
+ action_column = column_name(action)
+ where(namespace: namespace).where.not(action_column => nil).exists?
+ end
+
+ def not_completed?(namespace_id, action)
+ return unless ACTIONS.include?(action)
+
+ action_column = column_name(action)
+ exists?(namespace_id: namespace_id, action_column => nil)
+ end
+
+ def column_name(action)
+ :"#{action}_at"
+ end
+
+ private
+
+ def root_namespace?(namespace)
+ namespace&.root?
+ end
+ end
+
+ def number_of_completed_actions
+ attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size
+ end
+
+ private
+
+ def namespace_is_root_namespace
+ return unless namespace
+
+ errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent?
+ end
+ end
+end
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
deleted file mode 100644
index e5851c5cfc5..00000000000
--- a/app/models/onboarding_progress.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-# frozen_string_literal: true
-
-class OnboardingProgress < ApplicationRecord
- belongs_to :namespace, optional: false
-
- validate :namespace_is_root_namespace
-
- ACTIONS = [
- :git_pull,
- :git_write,
- :merge_request_created,
- :pipeline_created,
- :user_added,
- :trial_started,
- :subscription_created,
- :required_mr_approvals_enabled,
- :code_owners_enabled,
- :scoped_label_created,
- :security_scan_enabled,
- :issue_created,
- :issue_auto_closed,
- :repository_imported,
- :repository_mirrored,
- :secure_dependency_scanning_run,
- :secure_container_scanning_run,
- :secure_dast_run,
- :secure_secret_detection_run,
- :secure_coverage_fuzzing_run,
- :secure_api_fuzzing_run,
- :secure_cluster_image_scanning_run,
- :license_scanning_run
- ].freeze
-
- scope :incomplete_actions, -> (actions) do
- Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) }
- end
-
- scope :completed_actions, -> (actions) do
- Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) }
- end
-
- scope :completed_actions_with_latest_in_range, -> (actions, range) do
- actions = Array(actions)
- if actions.size == 1
- where(column_name(actions[0]) => range)
- else
- action_columns = actions.map { |action| arel_table[column_name(action)] }
- completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range))
- end
- end
-
- class << self
- def onboard(namespace)
- return unless root_namespace?(namespace)
-
- create(namespace: namespace)
- end
-
- def onboarding?(namespace)
- where(namespace: namespace).any?
- end
-
- def register(namespace, actions)
- actions = Array(actions)
- return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty?
-
- onboarding_progress = find_by(namespace: namespace)
- return unless onboarding_progress
-
- now = Time.current
- nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? }
- return if nil_actions.empty?
-
- updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) }
- onboarding_progress.update!(updates)
- end
-
- def completed?(namespace, action)
- return unless root_namespace?(namespace) && ACTIONS.include?(action)
-
- action_column = column_name(action)
- where(namespace: namespace).where.not(action_column => nil).exists?
- end
-
- def not_completed?(namespace_id, action)
- return unless ACTIONS.include?(action)
-
- action_column = column_name(action)
- where(namespace_id: namespace_id).where(action_column => nil).exists?
- end
-
- def column_name(action)
- :"#{action}_at"
- end
-
- private
-
- def root_namespace?(namespace)
- namespace && namespace.root?
- end
- end
-
- def number_of_completed_actions
- attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size
- end
-
- private
-
- def namespace_is_root_namespace
- return unless namespace
-
- errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent?
- end
-end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index afd55b4f143..b4c09d99bb0 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -22,7 +22,8 @@ class Packages::Package < ApplicationRecord
debian: 9,
rubygems: 10,
helm: 11,
- terraform_module: 12
+ terraform_module: 12,
+ rpm: 13
}
enum status: { default: 0, hidden: 1, processing: 2, error: 3, pending_destruction: 4 }
@@ -43,6 +44,7 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
+ has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum'
has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
has_many :build_infos, inverse_of: :package
has_many :pipelines, through: :build_infos, disable_joins: true
@@ -242,22 +244,23 @@ class Packages::Package < ApplicationRecord
reverse_order_direction = direction == :asc ? desc_order_expression : asc_order_expression
arel_order_classes = ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::AREL_ORDER_CLASSES.invert
- ::Gitlab::Pagination::Keyset::Order.build([
- ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: "#{join_table}_#{column_name}",
- column_expression: join_class.arel_table[column_name],
- order_expression: order_direction,
- reversed_order_expression: reverse_order_direction,
- order_direction: direction,
- distinct: false,
- add_to_projections: true
- ),
- ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'id',
- order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]),
- add_to_projections: true
- )
- ])
+ ::Gitlab::Pagination::Keyset::Order.build(
+ [
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: "#{join_table}_#{column_name}",
+ column_expression: join_class.arel_table[column_name],
+ order_expression: order_direction,
+ reversed_order_expression: reverse_order_direction,
+ order_direction: direction,
+ distinct: false,
+ add_to_projections: true
+ ),
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]),
+ add_to_projections: true
+ )
+ ])
end
def versions
@@ -330,6 +333,12 @@ class Packages::Package < ApplicationRecord
name.gsub(/#{Gitlab::Regex::Packages::PYPI_NORMALIZED_NAME_REGEX_STRING}/o, '-').downcase
end
+ def touch_last_downloaded_at
+ ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
+ update_column(:last_downloaded_at, Time.zone.now)
+ end
+ end
+
private
def composer_tag_version?
diff --git a/app/models/packages/policies/group.rb b/app/models/packages/policies/group.rb
new file mode 100644
index 00000000000..66cd361f2ed
--- /dev/null
+++ b/app/models/packages/policies/group.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Packages
+ module Policies
+ class Group
+ attr_accessor :group
+
+ delegate_missing_to :group
+
+ def initialize(group)
+ @group = group
+ end
+ end
+ end
+end
diff --git a/app/models/packages/policies/project.rb b/app/models/packages/policies/project.rb
new file mode 100644
index 00000000000..a5c6703be42
--- /dev/null
+++ b/app/models/packages/policies/project.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Packages
+ module Policies
+ class Project
+ attr_accessor :project
+
+ delegate_missing_to :project
+
+ def initialize(project)
+ @project = project
+ end
+ end
+ end
+end
diff --git a/app/models/packages/rpm.rb b/app/models/packages/rpm.rb
new file mode 100644
index 00000000000..fc66e7ec5c8
--- /dev/null
+++ b/app/models/packages/rpm.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Rpm
+ def self.table_name_prefix
+ 'packages_rpm_'
+ end
+ end
+end
diff --git a/app/models/packages/rpm/metadatum.rb b/app/models/packages/rpm/metadatum.rb
new file mode 100644
index 00000000000..07361995a12
--- /dev/null
+++ b/app/models/packages/rpm/metadatum.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Packages
+ module Rpm
+ class Metadatum < ApplicationRecord
+ self.primary_key = :package_id
+
+ belongs_to :package, -> { where(package_type: :rpm) }, inverse_of: :rpm_metadatum
+
+ validates :package, presence: true
+
+ validates :epoch,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
+ validates :release,
+ presence: true,
+ length: { maximum: 128 }
+
+ validates :summary,
+ presence: true,
+ length: { maximum: 1000 }
+
+ validates :description,
+ presence: true,
+ length: { maximum: 5000 }
+
+ validates :arch,
+ presence: true,
+ length: { maximum: 255 }
+
+ validates :license,
+ allow_nil: true,
+ length: { maximum: 1000 }
+
+ validates :url,
+ allow_nil: true,
+ length: { maximum: 1000 }
+
+ validate :rpm_package_type
+
+ private
+
+ def rpm_package_type
+ return if package&.rpm?
+
+ errors.add(:base, _('Package type must be RPM'))
+ end
+ end
+ end
+end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 2e25839c47f..16d5492a65e 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -33,6 +33,7 @@ class PagesDomain < ApplicationRecord
validate :validate_pages_domain
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
+ validate :validate_custom_domain_count_per_project, on: :create
default_value_for(:auto_ssl_enabled, allows_nil: false) { ::Gitlab::LetsEncrypt.enabled? }
default_value_for :scope, allows_nil: false, value: :project
@@ -57,6 +58,7 @@ class PagesDomain < ApplicationRecord
where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
end
+ scope :verified, -> { where.not(verified_at: nil) }
scope :need_auto_ssl_renewal, -> do
enabled_and_not_failed = where(auto_ssl_enabled: true, auto_ssl_failed: false)
@@ -224,6 +226,16 @@ class PagesDomain < ApplicationRecord
self.auto_ssl_failed = false
end
+ def validate_custom_domain_count_per_project
+ return unless project
+
+ unless project.can_create_custom_domains?
+ self.errors.add(
+ :base,
+ _("This project reached the limit of custom domains. (Max %d)") % Gitlab::CurrentSettings.max_pages_custom_domains_per_project)
+ end
+ end
+
private
def pages_deployed?
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 7e6e366f8da..9ed25c56ed6 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -24,6 +24,8 @@ class PersonalAccessToken < ApplicationRecord
scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) }
scope :expired_today_and_not_notified, -> { where(["revoked = false AND expires_at = CURRENT_DATE AND after_expiry_notification_delivered = false"]) }
scope :inactive, -> { where("revoked = true OR expires_at < CURRENT_DATE") }
+ scope :created_before, -> (date) { where("personal_access_tokens.created_at < :date", date: date) }
+ scope :last_used_before_or_unused, -> (date) { where("personal_access_tokens.created_at < :date AND (last_used_at < :date OR last_used_at IS NULL)", date: date) }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
scope :revoked, -> { where(revoked: true) }
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 3461104ae35..f22a63ee980 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -81,8 +81,8 @@ class PoolRepository < ApplicationRecord
object_pool.link(repository.raw)
end
- def unlink_repository(repository)
- repository.disconnect_alternates
+ def unlink_repository(repository, disconnect: true)
+ repository.disconnect_alternates if disconnect
if member_projects.where.not(id: repository.project.id).exists?
true
diff --git a/app/models/preloaders/environments/deployment_preloader.rb b/app/models/preloaders/environments/deployment_preloader.rb
index 251d1837f19..84aa7bc834f 100644
--- a/app/models/preloaders/environments/deployment_preloader.rb
+++ b/app/models/preloaders/environments/deployment_preloader.rb
@@ -41,11 +41,11 @@ module Preloaders
environment.association(association_name).target = associated_deployment
environment.association(association_name).loaded!
- if associated_deployment
- # `last?` in DeploymentEntity requires this environment to be loaded
- associated_deployment.association(:environment).target = environment
- associated_deployment.association(:environment).loaded!
- end
+ next unless associated_deployment
+
+ # `last?` in DeploymentEntity requires this environment to be loaded
+ associated_deployment.association(:environment).target = environment
+ associated_deployment.association(:environment).loaded!
end
end
end
diff --git a/app/models/preloaders/group_policy_preloader.rb b/app/models/preloaders/group_policy_preloader.rb
index 44030140ce3..23632a9b6c2 100644
--- a/app/models/preloaders/group_policy_preloader.rb
+++ b/app/models/preloaders/group_policy_preloader.rb
@@ -17,4 +17,4 @@ module Preloaders
end
end
-Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader')
+Preloaders::GroupPolicyPreloader.prepend_mod
diff --git a/app/models/preloaders/project_policy_preloader.rb b/app/models/preloaders/project_policy_preloader.rb
new file mode 100644
index 00000000000..fe9db3464c7
--- /dev/null
+++ b/app/models/preloaders/project_policy_preloader.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class ProjectPolicyPreloader
+ def initialize(projects, current_user)
+ @projects = projects
+ @current_user = current_user
+ end
+
+ def execute
+ return if projects.is_a?(ActiveRecord::NullRelation)
+
+ ActiveRecord::Associations::Preloader.new.preload(projects, { group: :route, namespace: :owner })
+ ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+ end
+
+ private
+
+ attr_reader :projects, :current_user
+ end
+end
+
+Preloaders::ProjectPolicyPreloader.prepend_mod
diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb
new file mode 100644
index 00000000000..8d04e71774c
--- /dev/null
+++ b/app/models/preloaders/project_root_ancestor_preloader.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Preloaders
+ class ProjectRootAncestorPreloader
+ def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = [])
+ @projects = projects
+ @namespace_sti_name = namespace_sti_name
+ @root_ancestor_preloads = root_ancestor_preloads
+ end
+
+ def execute
+ return if @projects.is_a?(ActiveRecord::NullRelation)
+ return unless ::Feature.enabled?(:use_traversal_ids)
+
+ root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
+ .select('namespaces.*, root_query.id as source_id')
+
+ root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
+
+ root_ancestors_by_id = root_query.group_by(&:source_id)
+
+ ActiveRecord::Associations::Preloader.new.preload(@projects, :namespace)
+ @projects.each do |project|
+ project.namespace.root_ancestor = root_ancestors_by_id[project.id]&.first
+ end
+ end
+
+ private
+
+ def join_sql
+ @projects
+ .joins(@namespace_sti_name)
+ .select('projects.id, namespaces.traversal_ids[1] as root_id')
+ .to_sql
+ end
+ end
+end
diff --git a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb
index 99a31a620c5..f32184f168d 100644
--- a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb
@@ -51,4 +51,4 @@ module Preloaders
end
end
-# Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod
+Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod
diff --git a/app/models/project.rb b/app/models/project.rb
index 0c49cc24a8d..c5fad189f87 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -46,13 +46,9 @@ class Project < ApplicationRecord
extend Gitlab::ConfigHelper
- ignore_columns :container_registry_enabled, remove_after: '2021-09-22', remove_with: '14.4'
-
BoardLimitExceeded = Class.new(StandardError)
ExportLimitExceeded = Class.new(StandardError)
- ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4'
- ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4'
ignore_columns :build_coverage_regex, remove_after: '2022-10-22', remove_with: '15.5'
STATISTICS_ATTRIBUTE = 'repositories_count'
@@ -123,6 +119,7 @@ class Project < ApplicationRecord
before_validation :ensure_project_namespace_in_sync
before_validation :set_package_registry_access_level, if: :packages_enabled_changed?
+ before_validation :remove_leading_spaces_on_name
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@@ -453,7 +450,7 @@ class Project < ApplicationRecord
:metrics_dashboard_access_level, :analytics_access_level,
:operations_access_level, :security_and_compliance_access_level,
:container_registry_access_level, :environments_access_level, :feature_flags_access_level,
- :releases_access_level,
+ :monitor_access_level, :releases_access_level,
to: :project_feature, allow_nil: true
delegate :show_default_award_emojis, :show_default_award_emojis=,
@@ -461,6 +458,9 @@ class Project < ApplicationRecord
:warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=,
to: :project_setting, allow_nil: true
+ delegate :show_diff_preview_in_email, :show_diff_preview_in_email=, :show_diff_preview_in_email?,
+ to: :project_setting
+
delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
delegate :squash_option, :squash_option=, to: :project_setting
delegate :mr_default_target_self, :mr_default_target_self=, to: :project_setting
@@ -565,26 +565,29 @@ class Project < ApplicationRecord
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :sorted_by_similarity_desc, -> (search, include_in_select: false) do
- order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
- { column: arel_table["path"], multiplier: 1 },
- { column: arel_table["name"], multiplier: 0.7 },
- { column: arel_table["description"], multiplier: 0.2 }
- ])
-
- order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'similarity',
- column_expression: order_expression,
- order_expression: order_expression.desc,
- order_direction: :desc,
- distinct: false,
- add_to_projections: true
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'id',
- order_expression: Project.arel_table[:id].desc
- )
- ])
+ order_expression = Gitlab::Database::SimilarityScore.build_expression(
+ search: search,
+ rules: [
+ { column: arel_table["path"], multiplier: 1 },
+ { column: arel_table["name"], multiplier: 0.7 },
+ { column: arel_table["description"], multiplier: 0.2 }
+ ])
+
+ order = Gitlab::Pagination::Keyset::Order.build(
+ [
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'similarity',
+ column_expression: order_expression,
+ order_expression: order_expression.desc,
+ order_direction: :desc,
+ distinct: false,
+ add_to_projections: true
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: Project.arel_table[:id].desc
+ )
+ ])
order.apply_cursor_conditions(reorder(order))
end
@@ -611,7 +614,7 @@ class Project < ApplicationRecord
scope :include_integration, -> (integration_association_name) { includes(integration_association_name) }
scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) }
scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) }
- scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
+ scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
# includes(:route) which we use in ProjectsFinder.
@@ -1163,7 +1166,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
return unless latest_pipeline
- latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
+ latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name)
end
def latest_successful_build_for_sha(job_name, sha)
@@ -1172,7 +1175,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_sha(sha)
return unless latest_pipeline
- latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
+ latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name)
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)
@@ -1564,9 +1567,7 @@ class Project < ApplicationRecord
end
def disabled_integrations
- disabled_integrations = []
- disabled_integrations << 'shimo' unless Feature.enabled?(:shimo_integration, self)
- disabled_integrations
+ []
end
def find_or_initialize_integration(name)
@@ -2369,28 +2370,6 @@ class Project < ApplicationRecord
.first
end
- def ci_variables_for(ref:, environment: nil)
- cache_key = "ci_variables_for:project:#{self&.id}:ref:#{ref}:environment:#{environment}"
-
- ::Gitlab::SafeRequestStore.fetch(cache_key) do
- uncached_ci_variables_for(ref: ref, environment: environment)
- end
- end
-
- def uncached_ci_variables_for(ref:, environment: nil)
- result = if protected_for?(ref)
- variables
- else
- variables.unprotected
- end
-
- if environment
- result.on_environment(environment)
- else
- result.where(environment_scope: '*')
- end
- end
-
def protected_for?(ref)
raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
@@ -2582,10 +2561,7 @@ class Project < ApplicationRecord
def badges
return project_badges unless group
- Badge.from_union([
- project_badges,
- GroupBadge.where(group: group.self_and_ancestors)
- ])
+ Badge.from_union([project_badges, GroupBadge.where(group: group.self_and_ancestors)])
end
def merge_requests_allowing_push_to_user(user)
@@ -2631,11 +2607,7 @@ class Project < ApplicationRecord
def gitlab_deploy_token
strong_memoize(:gitlab_deploy_token) do
- if Feature.enabled?(:ci_variable_for_group_gitlab_deploy_token, self)
- deploy_tokens.gitlab_deploy_token || group&.gitlab_deploy_token
- else
- deploy_tokens.gitlab_deploy_token
- end
+ deploy_tokens.gitlab_deploy_token || group&.gitlab_deploy_token
end
end
@@ -2693,7 +2665,12 @@ class Project < ApplicationRecord
end
def leave_pool_repository
- pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil)
+ return if pool_repository.blank?
+
+ # Disconnecting the repository can be expensive, so let's skip it if
+ # this repository is being deleted anyway.
+ pool_repository.unlink_repository(repository, disconnect: !pending_delete?)
+ update_column(:pool_repository_id, nil)
end
def link_pool_repository
@@ -3045,10 +3022,24 @@ class Project < ApplicationRecord
licensed_feature_available?(:security_training)
end
+ def packages_policy_subject
+ if Feature.enabled?(:read_package_policy_rule, group)
+ ::Packages::Policies::Project.new(self)
+ else
+ self
+ end
+ end
+
def destroy_deployment_by_id(deployment_id)
deployments.where(id: deployment_id).fast_destroy_all
end
+ def can_create_custom_domains?
+ return true if Gitlab::CurrentSettings.max_pages_custom_domains_per_project == 0
+
+ pages_domains.count < Gitlab::CurrentSettings.max_pages_custom_domains_per_project
+ end
+
private
# overridden in EE
@@ -3300,6 +3291,10 @@ class Project < ApplicationRecord
end
end
+ def remove_leading_spaces_on_name
+ name&.lstrip!
+ end
+
def set_package_registry_access_level
return if !project_feature || project_feature.package_registry_access_level_changed?
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 8623e477c06..dad8aaf0625 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -17,6 +17,7 @@ class ProjectFeature < ApplicationRecord
pages
metrics_dashboard
analytics
+ monitor
operations
security_and_compliance
container_registry
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 59d2e3deb4f..f5c346eda30 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
+ include ::Gitlab::Utils::StrongMemoize
+
ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze
belongs_to :project, inverse_of: :project_setting
@@ -47,6 +49,15 @@ class ProjectSetting < ApplicationRecord
end
end
+ def show_diff_preview_in_email?
+ if project.group
+ super && project.group&.show_diff_preview_in_email?
+ else
+ !!super
+ end
+ end
+ strong_memoize_attr :show_diff_preview_in_email
+
private
def validates_mr_default_target_self
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index a0af1b47d01..a91e0291438 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -11,9 +11,10 @@ class ProjectStatistics < ApplicationRecord
default_value_for :snippets_size, 0
counter_attribute :build_artifacts_size
- counter_attribute :storage_size
counter_attribute_after_flush do |project_statistic|
+ project_statistic.refresh_storage_size!
+
Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id)
end
@@ -21,7 +22,6 @@ class ProjectStatistics < ApplicationRecord
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size, :container_registry_size].freeze
INCREMENTABLE_COLUMNS = {
- build_artifacts_size: %i[storage_size],
packages_size: %i[storage_size],
pipeline_artifacts_size: %i[storage_size],
snippets_size: %i[storage_size]
@@ -109,21 +109,25 @@ class ProjectStatistics < ApplicationRecord
self.storage_size = storage_size
end
- # Since this incremental update method does not call update_storage_size above,
- # we have to update the storage_size here as additional column.
- # 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 refresh_storage_size!
+ update_storage_size
+ save!
+ end
+
+ # Since this incremental update method does not call update_storage_size above through before_save,
+ # we have to update the storage_size separately.
+ #
+ # For counter attributes, storage_size will be refreshed after the counter is flushed,
+ # through counter_attribute_after_flush
+ #
+ # For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS
def self.increment_statistic(project, key, amount)
- raise ArgumentError, "Cannot increment attribute: #{key}" unless INCREMENTABLE_COLUMNS.key?(key)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
return if amount == 0
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
+ if counter_attribute_enabled?(key)
+ project_statistics.delayed_increment_counter(key, amount)
else
legacy_increment_statistic(project, key, amount)
end
@@ -149,6 +153,10 @@ class ProjectStatistics < ApplicationRecord
update_all(updates.join(', '))
end
+ def self.incrementable_attribute?(key)
+ INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key)
+ end
+
private
def schedule_namespace_aggregation_worker
diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb
index dee4afdefa6..e66e1d5b42f 100644
--- a/app/models/projects/build_artifacts_size_refresh.rb
+++ b/app/models/projects/build_artifacts_size_refresh.rb
@@ -2,6 +2,7 @@
module Projects
class BuildArtifactsSizeRefresh < ApplicationRecord
+ include AfterCommitQueue
include BulkInsertSafe
STALE_WINDOW = 2.hours
@@ -52,6 +53,8 @@ module Projects
scope :remaining, -> { with_state(:created, :pending).or(stale) }
scope :processing_queue, -> { remaining.order(state: :desc) }
+ after_destroy :schedule_namespace_aggregation_worker
+
def self.enqueue_refresh(projects)
now = Time.zone.now
@@ -93,5 +96,13 @@ module Projects
def started?
!created?
end
+
+ private
+
+ def schedule_namespace_aggregation_worker
+ run_after_commit do
+ Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
+ end
+ end
end
end
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index b0f138714a0..3155eede2bd 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -18,9 +18,11 @@ module Projects
scope :without_assigned_projects, -> { where(total_projects_count: 0) }
scope :order_by_non_private_projects_count, -> { order(non_private_projects_count: :desc).order(id: :asc) }
scope :reorder_by_similarity, -> (search) do
- order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
- { column: arel_table['name'] }
- ])
+ order_expression = Gitlab::Database::SimilarityScore.build_expression(
+ search: search,
+ rules: [
+ { column: arel_table['name'] }
+ ])
reorder(order_expression.desc, arel_table['non_private_projects_count'].desc, arel_table['id'])
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 76c277e4b86..b3a918d8952 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -25,10 +25,12 @@ class ProtectedBranch < ApplicationRecord
end
# Check if branch name is marked as protected in the system
- def self.protected?(project, ref_name, dry_run: true)
+ def self.protected?(project, ref_name)
return true if project.empty_repo? && project.default_branch_protected?
return false if ref_name.blank?
+ dry_run = Feature.disabled?(:rely_on_protected_branches_cache, project)
+
new_cache_result = new_cache(project, ref_name, dry_run: dry_run)
return new_cache_result unless new_cache_result.nil?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 26c3b01a46e..ee1bea0e8d2 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -194,6 +194,18 @@ class Repository
CommitCollection.new(container, commits, ref)
end
+ def list_commits_by(query, ref, author: nil, before: nil, after: nil, limit: 1000)
+ return [] unless exists?
+ return [] unless has_visible_content?
+ return [] unless query.present? && ref.present?
+
+ commits = raw_repository.list_commits_by(
+ query, ref, author: author, before: before, after: after, limit: limit).map do |c|
+ commit(c)
+ end
+ CommitCollection.new(container, commits, ref)
+ end
+
def find_branch(name)
raw_repository.find_branch(name)
end
@@ -779,8 +791,8 @@ class Repository
raw_repository.branch_names_contains_sha(sha)
end
- def tag_names_contains(sha)
- raw_repository.tag_names_contains_sha(sha)
+ def tag_names_contains(sha, limit: 0)
+ raw_repository.tag_names_contains_sha(sha, limit: limit)
end
def local_branches
@@ -796,7 +808,7 @@ class Repository
def create_dir(user, path, **options)
options[:actions] = [{ action: :create_dir, file_path: path }]
- multi_action(user, **options)
+ commit_files(user, **options)
end
def create_file(user, path, content, **options)
@@ -808,7 +820,7 @@ class Repository
options[:actions].push({ action: :chmod, file_path: path, execute_filemode: execute_filemode })
end
- multi_action(user, **options)
+ commit_files(user, **options)
end
def update_file(user, path, content, **options)
@@ -823,13 +835,13 @@ class Repository
options[:actions].push({ action: :chmod, file_path: path, execute_filemode: execute_filemode })
end
- multi_action(user, **options)
+ commit_files(user, **options)
end
def delete_file(user, path, **options)
options[:actions] = [{ action: :delete, file_path: path }]
- multi_action(user, **options)
+ commit_files(user, **options)
end
def with_cache_hooks
@@ -843,14 +855,14 @@ class Repository
result.newrev
end
- def multi_action(user, **options)
+ def commit_files(user, **options)
start_project = options.delete(:start_project)
if start_project
options[:start_repository] = start_project.repository.raw_repository
end
- with_cache_hooks { raw.multi_action(user, **options) }
+ with_cache_hooks { raw.commit_files(user, **options) }
end
def merge(user, source_sha, merge_request, message)
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 689a9d8a8ae..6ebb9d5f176 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -3,8 +3,9 @@
class ResourceStateEvent < ResourceEvent
include IssueResourceEvent
include MergeRequestResourceEvent
+ include Importable
- validate :exactly_one_issuable
+ validate :exactly_one_issuable, unless: :importing?
belongs_to :source_merge_request, class_name: 'MergeRequest', foreign_key: :source_merge_request_id
@@ -32,9 +33,9 @@ class ResourceStateEvent < ResourceEvent
case state
when 'closed'
- issue_usage_counter.track_issue_closed_action(author: user)
+ issue_usage_counter.track_issue_closed_action(author: user, project: issue.project)
when 'reopened'
- issue_usage_counter.track_issue_reopened_action(author: user)
+ issue_usage_counter.track_issue_reopened_action(author: user, project: issue.project)
else
# no-op, nothing to do, not a state we're tracking
end
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index db87ff09159..26bf2a225d4 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -5,8 +5,9 @@ class ResourceTimeboxEvent < ResourceEvent
include IssueResourceEvent
include MergeRequestResourceEvent
+ include Importable
- validate :exactly_one_issuable
+ validate :exactly_one_issuable, unless: :importing?
enum action: {
add: 1,
@@ -34,7 +35,8 @@ class ResourceTimeboxEvent < ResourceEvent
case self
when ResourceMilestoneEvent
- Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user)
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user,
+ project: issue.project)
else
# no-op
end
diff --git a/app/models/route.rb b/app/models/route.rb
index 2f6b0a8e8f1..f2fe1664f9e 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -39,17 +39,17 @@ class Route < ApplicationRecord
attributes[:name] = route.name.sub(name_before_last_save, name)
end
- if attributes.present?
- old_path = route.path
+ next if attributes.empty?
- # Callbacks must be run manually
- route.update_columns(attributes.merge(updated_at: Time.current))
+ old_path = route.path
- # We are not calling route.delete_conflicting_redirects here, in hopes
- # of avoiding deadlocks. The parent (self, in this method) already
- # called it, which deletes conflicts for all descendants.
- route.create_redirect(old_path) if attributes[:path]
- end
+ # Callbacks must be run manually
+ route.update_columns(attributes.merge(updated_at: Time.current))
+
+ # We are not calling route.delete_conflicting_redirects here, in hopes
+ # of avoiding deadlocks. The parent (self, in this method) already
+ # called it, which deletes conflicts for all descendants.
+ route.create_redirect(old_path) if attributes[:path]
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 943d09d983b..9b7c37dd23e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -84,7 +84,7 @@ class Snippet < ApplicationRecord
participant :notes_with_associations
attr_spammable :title, spam_title: true
- attr_spammable :content, spam_description: true
+ attr_spammable :description, spam_description: true
attr_encrypted :secret_token,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -269,13 +269,7 @@ class Snippet < ApplicationRecord
def check_for_spam?(user:)
visibility_level_changed?(to: Snippet::PUBLIC) ||
- (public? && (title_changed? || content_changed?))
- end
-
- # snippets are the biggest sources of spam
- override :allow_possible_spam?
- def allow_possible_spam?
- false
+ (public? && (title_changed? || description_changed?))
end
def spammable_entity_type
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 5ac159d9615..a959ad4d548 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -31,7 +31,7 @@ class SnippetRepository < ApplicationRecord
options[:actions] = transform_file_entries(files)
- capture_git_error { repository.multi_action(user, **options) }
+ capture_git_error { repository.commit_files(user, **options) }
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index cc389dbe3f4..4e86036952b 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -25,6 +25,7 @@ class SystemNoteMetadata < ApplicationRecord
tag due_date start_date_or_due_date pinned_embed cherry_pick health_status approved unapproved
status alert_issue_added relate unrelate new_alert_added severity
attention_requested attention_request_removed contact timeline_event
+ issue_type relate_to_child unrelate_from_child relate_to_parent unrelate_from_parent
].freeze
validates :note, presence: true, unless: :importing?
diff --git a/app/models/todo.rb b/app/models/todo.rb
index d165e60e4c3..634fa9e7eda 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -96,10 +96,11 @@ class Todo < ApplicationRecord
def for_group_ids_and_descendants(group_ids)
groups = Group.groups_including_descendants_by(group_ids)
- from_union([
- for_project(Project.for_group(groups)),
- for_group(groups)
- ])
+ from_union(
+ [
+ for_project(Project.for_group(groups)),
+ for_group(groups)
+ ])
end
# Returns `true` if the current user has any todos for the given target with the optional given state.
diff --git a/app/models/user.rb b/app/models/user.rb
index afee2d70844..8825c18ea48 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -92,7 +92,6 @@ class User < ApplicationRecord
include ForcedEmailConfirmation
include RequireEmailVerification
- MINIMUM_INACTIVE_DAYS = 90
MINIMUM_DAYS_CREATED = 7
# Override Devise::Models::Trackable#update_tracked_fields!
@@ -262,6 +261,7 @@ class User < ApplicationRecord
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username, presence: true
+ validate :check_password_weakness, if: :encrypted_password_changed?
validates :namespace, presence: true
validate :namespace_move_dir_allowed, if: :username_changed?
@@ -488,7 +488,7 @@ class User < ApplicationRecord
scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) }
scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) }
scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first, arel_table[:id].desc) }
- scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
+ scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', Gitlab::CurrentSettings.deactivate_dormant_users_period.day.ago.to_date) }
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
@@ -697,28 +697,29 @@ class User < ApplicationRecord
scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query)
scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit]))
- order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'users_match_priority',
- order_expression: sanitized_order_sql.asc,
- add_to_projections: true,
- distinct: false
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'users_name',
- order_expression: arel_table[:name].asc,
- add_to_projections: true,
- nullable: :not_nullable,
- distinct: false
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'users_id',
- order_expression: arel_table[:id].asc,
- add_to_projections: true,
- nullable: :not_nullable,
- distinct: true
- )
- ])
+ order = Gitlab::Pagination::Keyset::Order.build(
+ [
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'users_match_priority',
+ order_expression: sanitized_order_sql.asc,
+ add_to_projections: true,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'users_name',
+ order_expression: arel_table[:name].asc,
+ add_to_projections: true,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'users_id',
+ order_expression: arel_table[:id].asc,
+ add_to_projections: true,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
scope.reorder(order)
end
@@ -1358,10 +1359,11 @@ class User < ApplicationRecord
end
def accessible_deploy_keys
- DeployKey.from_union([
- DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
- DeployKey.are_public
- ])
+ DeployKey.from_union(
+ [
+ DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
+ DeployKey.are_public
+ ])
end
def created_by
@@ -1662,10 +1664,11 @@ class User < ApplicationRecord
strong_memoize(:forkable_namespaces) do
personal_namespace = Namespace.where(id: namespace_id)
- Namespace.from_union([
- manageable_groups(include_groups_with_developer_maintainer_access: true),
- personal_namespace
- ])
+ Namespace.from_union(
+ [
+ manageable_groups(include_groups_with_developer_maintainer_access: true),
+ personal_namespace
+ ])
end
end
@@ -2072,6 +2075,7 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
+ # Deprecated: do not use. See: https://gitlab.com/gitlab-org/gitlab/-/issues/371017
def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
source_feature_name = "#{feature_name}_#{namespace.id}"
callout = namespace_callouts_by_feature_name[source_feature_name]
@@ -2151,10 +2155,6 @@ class User < ApplicationRecord
end
end
- def mr_attention_requests_enabled?
- Feature.enabled?(:mr_attention_requests, self)
- end
-
def account_age_in_days
(Date.current - created_at.to_date).to_i
end
@@ -2247,10 +2247,11 @@ class User < ApplicationRecord
end
def authorized_groups_without_shared_membership
- Group.from_union([
- groups.select(*Namespace.cached_column_list),
- authorized_projects.joins(:namespace).select(*Namespace.cached_column_list)
- ])
+ Group.from_union(
+ [
+ groups.select(*Namespace.cached_column_list),
+ authorized_projects.joins(:namespace).select(*Namespace.cached_column_list)
+ ])
end
def authorized_groups_with_shared_membership
@@ -2260,10 +2261,10 @@ class User < ApplicationRecord
Group
.with(cte.to_arel)
.from_union([
- Group.from(cte_alias),
- Group.joins(:shared_with_group_links)
- .where(group_group_links: { shared_with_group_id: Group.from(cte_alias) })
- ])
+ Group.from(cte_alias),
+ Group.joins(:shared_with_group_links)
+ .where(group_group_links: { shared_with_group_id: Group.from(cte_alias) })
+ ])
end
def default_private_profile_to_false
@@ -2314,6 +2315,14 @@ class User < ApplicationRecord
errors.add(:username, _('ending with a reserved file extension is not allowed.'))
end
+ def check_password_weakness
+ if Feature.enabled?(:block_weak_passwords) &&
+ password.present? &&
+ Security::WeakPasswords.weak_for_user?(password, self)
+ errors.add(:password, _('must not contain commonly used combinations of words and letters'))
+ end
+ end
+
def groups_with_developer_maintainer_project_access
project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]
@@ -2325,7 +2334,7 @@ class User < ApplicationRecord
end
def no_recent_activity?
- last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i
+ last_active_at.to_i <= Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_i
end
def update_highest_role?
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
index dee976a4497..0c66f465356 100644
--- a/app/models/user_status.rb
+++ b/app/models/user_status.rb
@@ -29,6 +29,10 @@ class UserStatus < ApplicationRecord
cache_markdown_field :message, pipeline: :emoji
+ def clear_status_after
+ clear_status_at
+ end
+
def clear_status_after=(value)
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 7b5c7fef7ba..03841ee48fa 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -43,12 +43,11 @@ module Users
verification_reminder: 40, # EE-only
ci_deprecation_warning_for_types_keyword: 41,
security_training_feature_promotion: 42, # EE-only
- storage_enforcement_banner_first_enforcement_threshold: 43,
- storage_enforcement_banner_second_enforcement_threshold: 44,
- storage_enforcement_banner_third_enforcement_threshold: 45,
- storage_enforcement_banner_fourth_enforcement_threshold: 46,
- attention_requests_top_nav: 47,
- attention_requests_side_nav: 48,
+ storage_enforcement_banner_first_enforcement_threshold: 43, # EE-only
+ storage_enforcement_banner_second_enforcement_threshold: 44, # EE-only
+ storage_enforcement_banner_third_enforcement_threshold: 45, # EE-only
+ storage_enforcement_banner_fourth_enforcement_threshold: 46, # EE-only
+ # 47 and 48 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95446
# 49 was removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91533
# because the banner was no longer relevant.
# Records will be migrated with https://gitlab.com/gitlab-org/gitlab/-/issues/367293
@@ -61,7 +60,8 @@ module Users
namespace_storage_limit_banner_warning_threshold: 56, # EE-only
namespace_storage_limit_banner_alert_threshold: 57, # EE-only
namespace_storage_limit_banner_error_threshold: 58, # EE-only
- project_quality_summary_feedback: 59 # EE-only
+ project_quality_summary_feedback: 59, # EE-only
+ merge_request_settings_moved_callout: 60
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 998a5deb0fd..272f31aa9ce 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -21,5 +21,11 @@ module Users
network: network
).order(credit_card_validated_at: :desc).includes(:user)
end
+
+ def similar_holder_names_count
+ return 0 unless holder_name
+
+ self.class.where('lower(holder_name) = lower(:value)', value: holder_name).count
+ end
end
end
diff --git a/app/models/users/ghost_user_migration.rb b/app/models/users/ghost_user_migration.rb
new file mode 100644
index 00000000000..1d93498e88b
--- /dev/null
+++ b/app/models/users/ghost_user_migration.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Users
+ class GhostUserMigration < ApplicationRecord
+ self.table_name = 'ghost_user_migrations'
+
+ belongs_to :user
+ belongs_to :initiator_user, class_name: 'User'
+
+ validates :user_id, presence: true
+ end
+end
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 70498ae83e0..3e3e424e9c9 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -11,10 +11,10 @@ module Users
enum feature_name: {
invite_members_banner: 1,
approaching_seat_count_threshold: 2, # EE-only
- storage_enforcement_banner_first_enforcement_threshold: 3,
- storage_enforcement_banner_second_enforcement_threshold: 4,
- storage_enforcement_banner_third_enforcement_threshold: 5,
- storage_enforcement_banner_fourth_enforcement_threshold: 6,
+ storage_enforcement_banner_first_enforcement_threshold: 3, # EE-only
+ storage_enforcement_banner_second_enforcement_threshold: 4, # EE-only
+ storage_enforcement_banner_third_enforcement_threshold: 5, # EE-only
+ storage_enforcement_banner_fourth_enforcement_threshold: 6, # EE-only
preview_user_over_limit_free_plan_alert: 7, # EE-only
user_reached_limit_free_plan_alert: 8, # EE-only
free_group_limited_alert: 9, # EE-only
diff --git a/app/models/users/namespace_callout.rb b/app/models/users/namespace_callout.rb
index a20a196a4ef..4e655a96b57 100644
--- a/app/models/users/namespace_callout.rb
+++ b/app/models/users/namespace_callout.rb
@@ -11,10 +11,10 @@ module Users
enum feature_name: {
invite_members_banner: 1,
approaching_seat_count_threshold: 2, # EE-only
- storage_enforcement_banner_first_enforcement_threshold: 3,
- storage_enforcement_banner_second_enforcement_threshold: 4,
- storage_enforcement_banner_third_enforcement_threshold: 5,
- storage_enforcement_banner_fourth_enforcement_threshold: 6,
+ storage_enforcement_banner_first_enforcement_threshold: 3, # EE-only
+ storage_enforcement_banner_second_enforcement_threshold: 4, # EE-only
+ storage_enforcement_banner_third_enforcement_threshold: 5, # EE-only
+ storage_enforcement_banner_fourth_enforcement_threshold: 6, # EE-only
preview_user_over_limit_free_plan_alert: 7, # EE-only
user_reached_limit_free_plan_alert: 8, # EE-only
web_hook_disabled: 9
diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb
index ddc5f8fb4de..98dacbe394a 100644
--- a/app/models/users/project_callout.rb
+++ b/app/models/users/project_callout.rb
@@ -9,7 +9,9 @@ module Users
belongs_to :project
enum feature_name: {
- awaiting_members_banner: 1 # EE-only
+ awaiting_members_banner: 1, # EE-only
+ web_hook_disabled: 2,
+ ultimate_feature_removal_banner: 3
}
validates :project, presence: true
diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb
index 1549c099a64..9a514b82506 100644
--- a/app/models/users_star_project.rb
+++ b/app/models/users_star_project.rb
@@ -3,7 +3,7 @@
class UsersStarProject < ApplicationRecord
include Sortable
- belongs_to :project, counter_cache: :star_count, touch: true
+ belongs_to :project, counter_cache: :star_count
belongs_to :user
validates :user, presence: true
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index d28a73b644f..fac79a8194a 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -103,6 +103,17 @@ class Wiki
def find_by_id(container_id)
container_class.find_by_id(container_id)&.wiki
end
+
+ def sluggified_full_path(title, extension)
+ sluggified_title(title) + '.' + extension
+ end
+
+ def sluggified_title(title)
+ title = Gitlab::EncodingHelper.encode_utf8_no_detect(title)
+ title = File.expand_path(title, '/')
+ title = Pathname.new(title).relative_path_from('/').to_s
+ title.tr(' ', '-')
+ end
end
def initialize(container, user = nil)
@@ -206,10 +217,11 @@ class Wiki
#
# Returns an initialized WikiPage instance or nil
def find_page(title, version = nil, load_content: true)
- page_title, page_dir = page_title_and_dir(title)
-
- if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content)
- WikiPage.new(self, page)
+ if find_page_with_repository_rpcs?
+ create_wiki_repository unless repository_exists?
+ find_page_with_repository_rpcs(title, version, load_content: load_content)
+ else
+ find_page_with_legacy_wiki_service(title, version, load_content: load_content)
end
end
@@ -419,19 +431,83 @@ class Wiki
end
def sluggified_full_path(title, extension)
- sluggified_title(title) + '.' + extension
+ self.class.sluggified_full_path(title, extension)
end
def sluggified_title(title)
- utf8_encoded_title = Gitlab::EncodingHelper.encode_utf8_no_detect(title)
+ self.class.sluggified_title(title)
+ end
- sanitized_title(utf8_encoded_title).tr(' ', '-')
+ def canonicalize_filename(filename)
+ Gitlab::Git::Wiki::GollumSlug.canonicalize_filename(filename)
end
- def sanitized_title(title)
- clean_absolute_path = File.expand_path(title, '/')
+ def find_page_with_legacy_wiki_service(title, version, load_content: false)
+ page_title, page_dir = page_title_and_dir(title)
+
+ if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content)
+ WikiPage.new(self, page)
+ end
+ end
+
+ def find_matched_file(title, version)
+ escaped_path = RE2::Regexp.escape(sluggified_title(title))
+ # We could not use ALLOWED_EXTENSIONS_REGEX constant or similar regexp with
+ # Regexp.union. The result combination complicated modifiers:
+ # /(?i-mx:md|mkdn?|mdown|markdown)|(?i-mx:rdoc).../
+ # Regexp used by Gitaly is Go's Regexp package. It does not support those
+ # features. So, we have to compose another more-friendly regexp to pass to
+ # Gitaly side.
+ extension_regexp = Wiki::MARKUPS.map { |_, format| format[:extension_regex].source }.join("|")
+ path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{extension_regexp})$")
+
+ matched_files = repository.search_files_by_regexp(path_regexp, version)
+ return if matched_files.blank?
+
+ Gitlab::EncodingHelper.encode_utf8_no_detect(matched_files.first)
+ end
+
+ def find_page_format(path)
+ ext = File.extname(path).downcase[1..]
+ MARKUPS.find { |_, markup| markup[:extension_regex].match?(ext) }&.first
+ end
+
+ def check_page_historical(path, commit)
+ repository.last_commit_for_path('HEAD', path).id != commit.id
+ end
+
+ def find_page_with_repository_rpcs(title, version, load_content: true)
+ version = version.presence || 'HEAD'
+ path = find_matched_file(title, version)
+ return if path.blank?
+
+ blob_options = load_content ? {} : { limit: 0 }
+ blob = repository.blob_at(version, path, **blob_options)
+ commit = repository.commit(blob.commit_id)
+ format = find_page_format(path)
+
+ page = Gitlab::Git::WikiPage.new(
+ url_path: sluggified_title(path.sub(/\.[^.]+\z/, "")),
+ title: canonicalize_filename(path),
+ format: format,
+ path: sluggified_title(path),
+ raw_data: blob.data,
+ name: canonicalize_filename(path),
+ historical: version == 'HEAD' ? false : check_page_historical(path, commit),
+ version: Gitlab::Git::WikiPageVersion.new(commit, format)
+ )
+ WikiPage.new(self, page)
+ end
+
+ def find_page_with_repository_rpcs?
+ group =
+ if container.is_a?(::Group)
+ container
+ else
+ container.group
+ end
- Pathname.new(clean_absolute_path).relative_path_from('/').to_s
+ Feature.enabled?(:wiki_find_page_with_normal_repository_rpcs, group, type: :development)
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 451359c1f85..05e45fa5b29 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -37,11 +37,11 @@ class WorkItem < Issue
override :parent_link_confidentiality
def parent_link_confidentiality
if confidential? && work_item_children.public_only.exists?
- errors.add(:confidential, _('confidential parent can not be used if there are non-confidential children.'))
+ errors.add(:base, _('A confidential work item cannot have a parent that already has non-confidential children.'))
end
if !confidential? && work_item_parent&.confidential?
- errors.add(:confidential, _('associated parent is confidential and can not have non-confidential children.'))
+ errors.add(:base, _('A non-confidential work item cannot have a confidential parent.'))
end
end
diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb
index 1e84d172bef..ec3b7957c79 100644
--- a/app/models/work_items/widgets/description.rb
+++ b/app/models/work_items/widgets/description.rb
@@ -3,7 +3,13 @@
module WorkItems
module Widgets
class Description < Base
- delegate :description, to: :work_item
+ delegate :description, :edited?, :last_edited_at, to: :work_item
+
+ def last_edited_by
+ return unless work_item.edited?
+
+ work_item.last_edited_by
+ end
end
end
end