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:
authorRobert Speicher <rspeicher@gmail.com>2021-01-20 22:34:23 +0300
committerRobert Speicher <rspeicher@gmail.com>2021-01-20 22:34:23 +0300
commit6438df3a1e0fb944485cebf07976160184697d72 (patch)
tree00b09bfd170e77ae9391b1a2f5a93ef6839f2597 /app/models
parent42bcd54d971da7ef2854b896a7b34f4ef8601067 (diff)
Add latest changes from gitlab-org/gitlab@13-8-stable-eev13.8.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/alert_management/http_integration.rb4
-rw-r--r--app/models/application_record.rb5
-rw-r--r--app/models/application_setting.rb46
-rw-r--r--app/models/application_setting_implementation.rb6
-rw-r--r--app/models/audit_event.rb3
-rw-r--r--app/models/audit_event_archived.rb10
-rw-r--r--app/models/audit_event_partitioned.rb14
-rw-r--r--app/models/authentication_event.rb8
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb4
-rw-r--r--app/models/ci/bridge.rb14
-rw-r--r--app/models/ci/build.rb56
-rw-r--r--app/models/ci/build_need.rb2
-rw-r--r--app/models/ci/build_runner_session.rb4
-rw-r--r--app/models/ci/commit_with_pipeline.rb38
-rw-r--r--app/models/ci/group.rb24
-rw-r--r--app/models/ci/job_artifact.rb10
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/ref.rb7
-rw-r--r--app/models/clusters/applications/cert_manager.rb2
-rw-r--r--app/models/clusters/applications/knative.rb2
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb2
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/commit_with_pipeline.rb2
-rw-r--r--app/models/concerns/boards/listable.rb52
-rw-r--r--app/models/concerns/bulk_insert_safe.rb4
-rw-r--r--app/models/concerns/ci/artifactable.rb3
-rw-r--r--app/models/concerns/each_batch.rb16
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb36
-rw-r--r--app/models/concerns/enums/commit_status.rb35
-rw-r--r--app/models/concerns/enums/vulnerability.rb46
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/issue_available_features.rb5
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/noteable.rb21
-rw-r--r--app/models/concerns/packages/debian/architecture.rb25
-rw-r--r--app/models/concerns/packages/debian/distribution.rb115
-rw-r--r--app/models/concerns/repositories/can_housekeep_repository.rb25
-rw-r--r--app/models/cycle_analytics/level_base.rb74
-rw-r--r--app/models/cycle_analytics/project_level.rb21
-rw-r--r--app/models/cycle_analytics/project_level_stage_adapter.rb44
-rw-r--r--app/models/deployment.rb17
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/experiment.rb4
-rw-r--r--app/models/gpg_key.rb2
-rw-r--r--app/models/group.rb3
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/list.rb36
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/members/group_member.rb12
-rw-r--r--app/models/merge_request.rb33
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/namespace.rb11
-rw-r--r--app/models/namespace/package_setting.rb28
-rw-r--r--app/models/namespace/root_storage_statistics.rb2
-rw-r--r--app/models/namespace_onboarding_action.rb27
-rw-r--r--app/models/onboarding_progress.rb65
-rw-r--r--app/models/packages/conan/file_metadatum.rb4
-rw-r--r--app/models/packages/debian.rb9
-rw-r--r--app/models/packages/debian/file_metadatum.rb56
-rw-r--r--app/models/packages/debian/group_architecture.rb9
-rw-r--r--app/models/packages/debian/group_distribution.rb9
-rw-r--r--app/models/packages/debian/project_architecture.rb9
-rw-r--r--app/models/packages/debian/project_distribution.rb9
-rw-r--r--app/models/packages/dependency.rb2
-rw-r--r--app/models/packages/event.rb28
-rw-r--r--app/models/packages/package.rb7
-rw-r--r--app/models/packages/package_file.rb9
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--app/models/plan.rb2
-rw-r--r--app/models/project.rb71
-rw-r--r--app/models/project_feature_usage.rb4
-rw-r--r--app/models/project_pages_metadatum.rb3
-rw-r--r--app/models/project_services/alerts_service.rb86
-rw-r--r--app/models/project_services/alerts_service_data.rb14
-rw-r--r--app/models/project_services/datadog_service.rb8
-rw-r--r--app/models/project_services/jira_service.rb10
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb2
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb2
-rw-r--r--app/models/protectable_dropdown.rb2
-rw-r--r--app/models/release.rb2
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/repository.rb21
-rw-r--r--app/models/service.rb8
-rw-r--r--app/models/snippet.rb18
-rw-r--r--app/models/snippet_repository.rb1
-rw-r--r--app/models/snippet_repository_storage_move.rb6
-rw-r--r--app/models/terraform/state.rb9
-rw-r--r--app/models/user.rb19
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/wiki.rb1
92 files changed, 1075 insertions, 450 deletions
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index 0c916c576cb..2d9a2d7031c 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -52,6 +52,10 @@ module AlertManagement
endpoint_identifier == LEGACY_IDENTIFIER
end
+ def token_changed?
+ attribute_changed?(:token)
+ end
+
# Blank token assignment triggers token reset
def prevent_token_assignment
if token.present? && token_changed?
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 71235ed1002..44d1b6cf907 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -77,4 +77,9 @@ class ApplicationRecord < ActiveRecord::Base
def self.where_exists(query)
where('EXISTS (?)', query.select(1))
end
+
+ def self.declarative_enum(enum_mod)
+ values = enum_mod.definition.transform_values { |v| v[:value] }
+ enum(enum_mod.key => values)
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 9b9db7f93fd..5655ea4d4bf 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -171,7 +171,7 @@ class ApplicationSetting < ApplicationRecord
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
- inclusion: { in: [true, false], message: 'must be a boolean value' }
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
validates :container_registry_token_expire_delay,
presence: true,
@@ -303,9 +303,15 @@ class ApplicationSetting < ApplicationRecord
validates :container_registry_delete_tags_service_timeout,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :container_registry_cleanup_tags_service_max_list_size,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :container_registry_expiration_policies_worker_capacity,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :invisible_captcha_enabled,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -400,6 +406,42 @@ class ApplicationSetting < ApplicationRecord
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
+ validates :rate_limiting_response_text,
+ length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
+ allow_blank: true
+
+ validates :throttle_unauthenticated_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_unauthenticated_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_authenticated_api_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_authenticated_api_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_authenticated_web_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_authenticated_web_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_protected_paths_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_protected_paths_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -430,7 +472,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm
validates :disable_feed_token,
- inclusion: { in: [true, false], message: 'must be a boolean value' }
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
before_validation :ensure_uuid!
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 105889a364a..b05355f14b4 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -91,12 +91,13 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
+ invisible_captcha_enabled: false,
issues_create_limit: 300,
local_markdown_version: 0,
login_recaptcha_protection_enabled: false,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
- max_import_size: 50,
+ max_import_size: 0,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
notify_on_unknown_sign_in: true,
@@ -172,7 +173,8 @@ module ApplicationSettingImplementation
container_registry_delete_tags_service_timeout: 250,
container_registry_expiration_policies_worker_capacity: 0,
kroki_enabled: false,
- kroki_url: nil
+ kroki_url: nil,
+ rate_limiting_response_text: nil
}
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index a4d991b040c..d1c0bb11dc8 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -4,6 +4,7 @@ class AuditEvent < ApplicationRecord
include CreatedAtFilterable
include BulkInsertSafe
include EachBatch
+ include PartitionedTable
PARALLEL_PERSISTENCE_COLUMNS = [
:author_name,
@@ -15,6 +16,8 @@ class AuditEvent < ApplicationRecord
self.primary_key = :id
+ partitioned_by :created_at, strategy: :monthly
+
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user, foreign_key: :author_id
diff --git a/app/models/audit_event_archived.rb b/app/models/audit_event_archived.rb
new file mode 100644
index 00000000000..3119f56fbcc
--- /dev/null
+++ b/app/models/audit_event_archived.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# This model is not intended to be used.
+# It is a temporary reference to the pre-partitioned
+# audit_events table.
+# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206
+# for details.
+class AuditEventArchived < ApplicationRecord
+ self.table_name = 'audit_events_archived'
+end
diff --git a/app/models/audit_event_partitioned.rb b/app/models/audit_event_partitioned.rb
deleted file mode 100644
index 672daebd14a..00000000000
--- a/app/models/audit_event_partitioned.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-# This model is not yet intended to be used.
-# It is in a transitioning phase while we are partitioning
-# the table on the database-side.
-# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206
-# for details.
-class AuditEventPartitioned < ApplicationRecord
- include PartitionedTable
-
- self.table_name = 'audit_events_part_5fc467ac26'
-
- partitioned_by :created_at, strategy: :monthly
-end
diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb
index 9d191e6ae4d..1e822629ba1 100644
--- a/app/models/authentication_event.rb
+++ b/app/models/authentication_event.rb
@@ -3,10 +3,10 @@
class AuthenticationEvent < ApplicationRecord
include UsageStatistics
- TWO_FACTOR = 'two-factor'.freeze
- TWO_FACTOR_U2F = 'two-factor-via-u2f-device'.freeze
- TWO_FACTOR_WEBAUTHN = 'two-factor-via-webauthn-device'.freeze
- STANDARD = 'standard'.freeze
+ TWO_FACTOR = 'two-factor'
+ TWO_FACTOR_U2F = 'two-factor-via-u2f-device'
+ TWO_FACTOR_WEBAUTHN = 'two-factor-via-webauthn-device'
+ STANDARD = 'standard'
STATIC_PROVIDERS = [TWO_FACTOR, TWO_FACTOR_U2F, TWO_FACTOR_WEBAUTHN, STANDARD].freeze
belongs_to :user, optional: true
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
index 11228e620c9..e255b6d15d2 100644
--- a/app/models/blob_viewer/gitlab_ci_yml.rb
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -15,7 +15,9 @@ module BlobViewer
prepare!
- @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, opts)
+ @validation_message = Gitlab::Ci::Lint
+ .new(project: opts[:project], current_user: opts[:user], sha: opts[:sha])
+ .validate(blob.data).errors.first
end
def valid?(opts)
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 19a0d424e33..ef3891908f7 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -7,7 +7,6 @@ module Ci
include Importable
include AfterCommitQueue
include Ci::HasRef
- extend ::Gitlab::Utils::Override
InvalidBridgeTypeError = Class.new(StandardError)
InvalidTransitionError = Class.new(StandardError)
@@ -200,13 +199,6 @@ module Ci
end
end
- override :dependency_variables
- def dependency_variables
- return [] unless ::Feature.enabled?(:ci_bridge_dependency_variables, project, default_enabled: true)
-
- super
- end
-
def target_revision_ref
downstream_pipeline_params.dig(:target_revision, :ref)
end
@@ -218,7 +210,8 @@ module Ci
project: downstream_project,
source: :pipeline,
target_revision: {
- ref: target_ref || downstream_project.default_branch
+ ref: target_ref || downstream_project.default_branch,
+ variables_attributes: downstream_variables
},
execute_params: {
ignore_skip_ci: true,
@@ -238,7 +231,8 @@ module Ci
checkout_sha: parent_pipeline.sha,
before: parent_pipeline.before_sha,
source_sha: parent_pipeline.source_sha,
- target_sha: parent_pipeline.target_sha
+ target_sha: parent_pipeline.target_sha,
+ variables_attributes: downstream_variables
},
execute_params: {
ignore_skip_ci: true,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 71939f070cb..5e3f42d7c2c 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -27,7 +27,8 @@ module Ci
upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? },
refspecs: -> (build) { build.merge_request_ref? },
artifacts_exclude: -> (build) { build.supports_artifacts_exclude? },
- multi_build_steps: -> (build) { build.multi_build_steps? }
+ multi_build_steps: -> (build) { build.multi_build_steps? },
+ return_exit_code: -> (build) { build.exit_codes_defined? }
}.freeze
DEFAULT_RETRIES = {
@@ -146,6 +147,12 @@ module Ci
.includes(:metadata, :job_artifacts_metadata)
end
+ scope :with_project_and_metadata, -> do
+ if Feature.enabled?(:non_public_artifacts, type: :development)
+ joins(:metadata).includes(:project, :metadata)
+ end
+ end
+
scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
scope :last_month, -> { where('created_at > ?', Date.today - 1.month) }
@@ -382,12 +389,8 @@ module Ci
end
after_transition any => [:skipped, :canceled] do |build, transition|
- if Feature.enabled?(:cd_skipped_deployment_status, build.project)
- if transition.to_name == :skipped
- build.deployment&.skip
- else
- build.deployment&.cancel
- end
+ if transition.to_name == :skipped
+ build.deployment&.skip
else
build.deployment&.cancel
end
@@ -741,6 +744,16 @@ module Ci
artifacts_metadata?
end
+ def artifacts_public?
+ return true unless Feature.enabled?(:non_public_artifacts, type: :development)
+
+ artifacts_public = options.dig(:artifacts, :public)
+
+ return true if artifacts_public.nil? # Default artifacts:public to true
+
+ options.dig(:artifacts, :public)
+ end
+
def artifacts_metadata_entry(path, **options)
artifacts_metadata.open do |metadata_stream|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
@@ -1007,14 +1020,23 @@ module Ci
end
def debug_mode?
- return false unless Feature.enabled?(:restrict_access_to_build_debug_mode, default_enabled: true)
-
# TODO: Have `debug_mode?` check against data on sent back from runner
# to capture all the ways that variables can be set.
# See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955)
variables.any? { |variable| variable[:key] == 'CI_DEBUG_TRACE' && variable[:value].casecmp('true') == 0 }
end
+ def drop_with_exit_code!(failure_reason, exit_code)
+ transaction do
+ conditionally_allow_failure!(exit_code)
+ drop!(failure_reason)
+ end
+ end
+
+ def exit_codes_defined?
+ options.dig(:allow_failure_criteria, :exit_codes).present?
+ end
+
protected
def run_status_commit_hooks!
@@ -1098,6 +1120,22 @@ module Ci
Gitlab::ErrorTracking.track_exception(e)
end
end
+
+ def conditionally_allow_failure!(exit_code)
+ return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled?
+ return unless exit_code
+
+ if allowed_to_fail_with_code?(exit_code)
+ update_columns(allow_failure: true)
+ end
+ end
+
+ def allowed_to_fail_with_code?(exit_code)
+ options
+ .dig(:allow_failure_criteria, :exit_codes)
+ .to_a
+ .include?(exit_code)
+ end
end
end
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index b977a5f4419..fac615f97b9 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -6,7 +6,7 @@ module Ci
include BulkInsertSafe
- belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :needs
+ belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
validates :build, presence: true
validates :name, presence: true, length: { maximum: 128 }
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index bc7f17f046c..b6196048ca1 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -7,8 +7,8 @@ module Ci
extend Gitlab::Ci::Model
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
- DEFAULT_SERVICE_NAME = 'build'.freeze
- DEFAULT_PORT_NAME = 'default_port'.freeze
+ DEFAULT_SERVICE_NAME = 'build'
+ DEFAULT_PORT_NAME = 'default_port'
self.table_name = 'ci_builds_runner_session'
diff --git a/app/models/ci/commit_with_pipeline.rb b/app/models/ci/commit_with_pipeline.rb
new file mode 100644
index 00000000000..7f952fb77a0
--- /dev/null
+++ b/app/models/ci/commit_with_pipeline.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class Ci::CommitWithPipeline < SimpleDelegator
+ include Presentable
+
+ def initialize(commit)
+ @latest_pipelines = {}
+ super(commit)
+ end
+
+ def pipelines
+ project.ci_pipelines.where(sha: sha)
+ end
+
+ def last_pipeline
+ strong_memoize(:last_pipeline) do
+ pipelines.last
+ end
+ end
+
+ def latest_pipeline(ref = nil)
+ @latest_pipelines.fetch(ref) do |ref|
+ @latest_pipelines[ref] = latest_pipeline_for_project(ref, project)
+ end
+ end
+
+ def latest_pipeline_for_project(ref, pipeline_project)
+ pipeline_project.ci_pipelines.latest_pipeline_per_commit(id, ref)[id]
+ end
+
+ def set_latest_pipeline_for_ref(ref, pipeline)
+ @latest_pipelines[ref] = pipeline
+ end
+
+ def status(ref = nil)
+ latest_pipeline(ref)&.status
+ end
+end
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index f0c035635b9..c7c0ec61e62 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -24,9 +24,22 @@ module Ci
def status
strong_memoize(:status) do
+ status_struct.status
+ end
+ end
+
+ def success?
+ status.to_s == 'success'
+ end
+
+ def has_warnings?
+ status_struct.warnings?
+ end
+
+ def status_struct
+ strong_memoize(:status_struct) do
Gitlab::Ci::Status::Composite
.new(@jobs)
- .status
end
end
@@ -39,8 +52,13 @@ module Ci
end
end
- def self.fabricate(project, stage)
- stage.latest_statuses
+ # Construct a grouping of statuses for this stage.
+ # We allow the caller to pass in statuses for efficiency (avoiding N+1
+ # queries).
+ def self.fabricate(project, stage, statuses = nil)
+ statuses ||= stage.latest_statuses
+
+ statuses
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
self.new(project, stage, name: group_name, jobs: grouped_statuses)
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index c80d50ea131..f13be3b3c86 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -9,6 +9,7 @@ module Ci
include Sortable
include Artifactable
include FileStoreMounter
+ include EachBatch
extend Gitlab::Ci::Model
TEST_REPORT_FILE_TYPES = %w[junit].freeze
@@ -133,6 +134,12 @@ module Ci
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
+ scope :with_job, -> do
+ if Feature.enabled?(:non_public_artifacts, type: :development)
+ joins(:job).includes(:job)
+ end
+ end
+
scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
@@ -170,7 +177,8 @@ module Ci
end
scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
- scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked).order(expire_at: :desc) }
+ scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) }
+ scope :order_expired_desc, -> { order(expire_at: :desc) }
scope :with_destroy_preloads, -> { includes(project: [:route, :statistics]) }
scope :scoped_project, -> { where('ci_job_artifacts.project_id = projects.id') }
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5e5f51d776f..4a579892e3f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -68,8 +68,8 @@ module Ci
has_many :variables, class_name: 'Ci::PipelineVariable'
has_many :deployments, through: :builds
has_many :environments, -> { distinct }, through: :deployments
- has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
- has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts
+ has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
+ has_many :downloadable_artifacts, -> { not_expired.downloadable.with_job }, through: :latest_builds, source: :job_artifacts
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
@@ -249,7 +249,7 @@ module Ci
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
pipeline.run_after_commit do
- ::Ci::Pipelines::CreateArtifactWorker.perform_async(pipeline.id)
+ ::Ci::PipelineArtifacts::CoverageReportWorker.perform_async(pipeline.id)
end
end
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 6e9b8416c10..713a0bf9c45 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -33,6 +33,9 @@ module Ci
state :still_failing, value: 5
after_transition any => [:fixed, :success] do |ci_ref|
+ # Do not try to unlock if no artifacts are locked
+ next unless ci_ref.artifacts_locked?
+
ci_ref.run_after_commit do
Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id)
end
@@ -54,6 +57,10 @@ module Ci
Ci::Pipeline.last_finished_for_ref_id(self.id)&.id
end
+ def artifacts_locked?
+ self.pipelines.where(locked: :artifacts_locked).exists?
+ end
+
def update_status_by!(pipeline)
retry_lock(self) do
next unless last_finished_pipeline_id == pipeline.id
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index d32fff14590..8560826928a 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -65,7 +65,7 @@ module Clusters
end
def retry_command(command)
- "for i in $(seq 1 90); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
+ Gitlab::Kubernetes::PodCmd.retry_command(command, times: 90)
end
def post_delete_script
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index b1c3116d77c..7c131e031c1 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Knative < ApplicationRecord
- VERSION = '0.9.0'
+ VERSION = '0.10.0'
REPOSITORY = 'https://charts.gitlab.io'
METRICS_CONFIG = 'https://gitlab.com/gitlab-org/charts/knative/-/raw/v0.9.0/vendor/istio-metrics.yml'
FETCH_IP_ADDRESS_DELAY = 30.seconds
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 1e41b6f4f31..56acac53e0b 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.23.0'
+ VERSION = '0.24.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 84de5828491..e3dcd5b0d07 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -247,7 +247,7 @@ module Clusters
def prevent_modification
return if provided_by_user?
- if api_url_changed? || token_changed? || ca_pem_changed?
+ if api_url_changed? || attribute_changed?(:token) || ca_pem_changed?
errors.add(:base, _('Cannot modify managed Kubernetes cluster'))
return false
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 80dd02981c1..edce9ad293e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -37,7 +37,7 @@ class Commit
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :full_title, pipeline: :single_line
- cache_markdown_field :description, pipeline: :commit_description
+ cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte
class << self
def decorate(commits, container)
@@ -80,7 +80,7 @@ class Commit
def diff_hard_limit_files(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
- 2000
+ 3000
else
1000
end
@@ -88,7 +88,7 @@ class Commit
def diff_hard_limit_lines(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
- 75000
+ 100000
else
50000
end
@@ -148,7 +148,7 @@ class Commit
to: :with_pipeline
def with_pipeline
- @with_pipeline ||= CommitWithPipeline.new(self)
+ @with_pipeline ||= Ci::CommitWithPipeline.new(self)
end
def id
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ee9c2501bfc..a399ffc32de 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -80,9 +80,9 @@ class CommitStatus < ApplicationRecord
merge(or_conditions)
end
- # We use `Enums::CommitStatus.failure_reasons` here so that EE can more easily
+ # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
- enum_with_nil failure_reason: Enums::CommitStatus.failure_reasons
+ enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons
##
# We still create some CommitStatuses outside of CreatePipelineService.
@@ -159,6 +159,12 @@ class CommitStatus < ApplicationRecord
commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason]
end
+ before_transition [:skipped, :manual] => :created do |commit_status, transition|
+ transition.args.first.try do |user|
+ commit_status.user = user
+ end
+ end
+
after_transition do |commit_status, transition|
next if transition.loopback?
next if commit_status.processed?
@@ -203,7 +209,7 @@ class CommitStatus < ApplicationRecord
def group_name
# 'rspec:linux: 1/10' => 'rspec:linux'
- common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
+ common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
diff --git a/app/models/commit_with_pipeline.rb b/app/models/commit_with_pipeline.rb
index f382ae8f55a..7f952fb77a0 100644
--- a/app/models/commit_with_pipeline.rb
+++ b/app/models/commit_with_pipeline.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class CommitWithPipeline < SimpleDelegator
+class Ci::CommitWithPipeline < SimpleDelegator
include Presentable
def initialize(commit)
diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb
new file mode 100644
index 00000000000..b7c0a8b3489
--- /dev/null
+++ b/app/models/concerns/boards/listable.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Boards
+ module Listable
+ extend ActiveSupport::Concern
+
+ included do
+ validates :label, :position, presence: true, if: :label?
+ validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable?
+
+ before_destroy :can_be_destroyed
+
+ scope :ordered, -> { order(:list_type, :position) }
+ scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
+ scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
+ end
+
+ class_methods do
+ def destroyable_types
+ [:label]
+ end
+
+ def movable_types
+ [:label]
+ end
+ end
+
+ def destroyable?
+ self.class.destroyable_types.include?(list_type&.to_sym)
+ end
+
+ def movable?
+ self.class.movable_types.include?(list_type&.to_sym)
+ end
+
+ def title
+ if label?
+ label.name
+ elsif backlog?
+ _('Open')
+ else
+ list_type.humanize
+ end
+ end
+
+ private
+
+ def can_be_destroyed
+ throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow
+ end
+ end
+end
diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb
index f9eb3fb875e..3748e77e933 100644
--- a/app/models/concerns/bulk_insert_safe.rb
+++ b/app/models/concerns/bulk_insert_safe.rb
@@ -53,9 +53,9 @@ module BulkInsertSafe
class_methods do
def set_callback(name, *args)
unless _bulk_insert_callback_allowed?(name, args)
- raise MethodNotAllowedError.new(
+ raise MethodNotAllowedError,
"Not allowed to call `set_callback(#{name}, #{args})` when model extends `BulkInsertSafe`." \
- "Callbacks that fire per each record being inserted do not work with bulk-inserts.")
+ "Callbacks that fire per each record being inserted do not work with bulk-inserts."
end
super
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 24df86dbc3c..cbe7d3b6abb 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -18,7 +18,8 @@ module Ci
gzip: 3
}, _suffix: true
- scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) }
+ scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) }
+ scope :expired, -> (limit) { expired_before(Time.current).limit(limit) }
end
def each_blob(&blk)
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index af5f4e30d06..a59f00d73ec 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -47,7 +47,7 @@ module EachBatch
# order_hint does not affect the search results. For example,
# `ORDER BY id ASC, updated_at ASC` means the same thing as `ORDER
# BY id ASC`.
- def each_batch(of: 1000, column: primary_key, order_hint: nil)
+ def each_batch(of: 1000, column: primary_key, order: :asc, order_hint: nil)
unless column
raise ArgumentError,
'the column: argument must be set to a column name to use for ordering rows'
@@ -55,7 +55,7 @@ module EachBatch
start = except(:select)
.select(column)
- .reorder(column => :asc)
+ .reorder(column => order)
start = start.order(order_hint) if order_hint
start = start.take
@@ -66,10 +66,12 @@ module EachBatch
arel_table = self.arel_table
1.step do |index|
+ start_cond = arel_table[column].gteq(start_id)
+ start_cond = arel_table[column].lteq(start_id) if order == :desc
stop = except(:select)
.select(column)
- .where(arel_table[column].gteq(start_id))
- .reorder(column => :asc)
+ .where(start_cond)
+ .reorder(column => order)
stop = stop.order(order_hint) if order_hint
stop = stop
@@ -77,12 +79,14 @@ module EachBatch
.limit(1)
.take
- relation = where(arel_table[column].gteq(start_id))
+ relation = where(start_cond)
if stop
stop_id = stop[column]
start_id = stop_id
- relation = relation.where(arel_table[column].lt(stop_id))
+ stop_cond = arel_table[column].lt(stop_id)
+ stop_cond = arel_table[column].gt(stop_id) if order == :desc
+ relation = relation.where(stop_cond)
end
# Any ORDER BYs are useless for this relation and can lead to less
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
new file mode 100644
index 00000000000..48b4a402974
--- /dev/null
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+module Enums
+ module Ci
+ module CommitStatus
+ # Returns the Hash to use for creating the `failure_reason` enum for
+ # `CommitStatus`.
+ def self.failure_reasons
+ {
+ unknown_failure: nil,
+ script_failure: 1,
+ api_failure: 2,
+ stuck_or_timeout_failure: 3,
+ runner_system_failure: 4,
+ missing_dependency_failure: 5,
+ runner_unsupported: 6,
+ stale_schedule: 7,
+ job_execution_timeout: 8,
+ archived_failure: 9,
+ unmet_prerequisites: 10,
+ scheduler_failure: 11,
+ data_integrity_failure: 12,
+ forward_deployment_failure: 13,
+ insufficient_bridge_permissions: 1_001,
+ downstream_bridge_project_not_found: 1_002,
+ invalid_bridge_trigger: 1_003,
+ bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data
+ downstream_pipeline_creation_failed: 1_007,
+ secrets_provider_not_found: 1_008,
+ reached_max_descendant_pipelines_depth: 1_009
+ }
+ end
+ end
+ end
+end
+
+Enums::Ci::CommitStatus.prepend_if_ee('EE::Enums::Ci::CommitStatus')
diff --git a/app/models/concerns/enums/commit_status.rb b/app/models/concerns/enums/commit_status.rb
deleted file mode 100644
index faeed7276ab..00000000000
--- a/app/models/concerns/enums/commit_status.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Enums
- module CommitStatus
- # Returns the Hash to use for creating the `failure_reason` enum for
- # `CommitStatus`.
- def self.failure_reasons
- {
- unknown_failure: nil,
- script_failure: 1,
- api_failure: 2,
- stuck_or_timeout_failure: 3,
- runner_system_failure: 4,
- missing_dependency_failure: 5,
- runner_unsupported: 6,
- stale_schedule: 7,
- job_execution_timeout: 8,
- archived_failure: 9,
- unmet_prerequisites: 10,
- scheduler_failure: 11,
- data_integrity_failure: 12,
- forward_deployment_failure: 13,
- insufficient_bridge_permissions: 1_001,
- downstream_bridge_project_not_found: 1_002,
- invalid_bridge_trigger: 1_003,
- bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data
- downstream_pipeline_creation_failed: 1_007,
- secrets_provider_not_found: 1_008,
- reached_max_descendant_pipelines_depth: 1_009
- }
- end
- end
-end
-
-Enums::CommitStatus.prepend_if_ee('EE::Enums::CommitStatus')
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
new file mode 100644
index 00000000000..4b2e9e9e0b2
--- /dev/null
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Enums
+ module Vulnerability
+ CONFIDENCE_LEVELS = {
+ # undefined: 0, no longer applicable
+ ignore: 1,
+ unknown: 2,
+ experimental: 3,
+ low: 4,
+ medium: 5,
+ high: 6,
+ confirmed: 7
+ }.with_indifferent_access.freeze
+
+ REPORT_TYPES = {
+ sast: 0,
+ secret_detection: 4
+ }.with_indifferent_access.freeze
+
+ SEVERITY_LEVELS = {
+ # undefined: 0, no longer applicable
+ info: 1,
+ unknown: 2,
+ # experimental: 3, formerly used by confidence, no longer applicable
+ low: 4,
+ medium: 5,
+ high: 6,
+ critical: 7
+ }.with_indifferent_access.freeze
+
+ def self.confidence_levels
+ CONFIDENCE_LEVELS
+ end
+
+ def self.report_types
+ REPORT_TYPES
+ end
+
+ def self.severity_levels
+ SEVERITY_LEVELS
+ end
+ end
+end
+
+Enums::Vulnerability.prepend_if_ee('EE::Enums::Vulnerability')
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index c3a394c1ca5..83ff5b16efe 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -196,6 +196,10 @@ module Issuable
is_a?(Issue)
end
+ def supports_assignee?
+ false
+ end
+
def severity
return IssuableSeverity::DEFAULT unless supports_severity?
@@ -216,6 +220,10 @@ module Issuable
end
class_methods do
+ def participant_includes
+ [:assignees, :author, { notes: [:author, :award_emoji] }]
+ end
+
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL.
@@ -344,12 +352,15 @@ module Issuable
#
# Returns an array of arel columns
def grouping_columns(sort)
+ sort = sort.to_s
grouping_columns = [arel_table[:id]]
if %w(milestone_due_desc milestone_due_asc milestone).include?(sort)
milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
+ elsif %w(merged_at_desc merged_at_asc).include?(sort)
+ grouping_columns << MergeRequest::Metrics.arel_table[:merged_at]
end
grouping_columns
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 886db133a94..a5ffa959174 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -9,7 +9,10 @@ module IssueAvailableFeatures
class_methods do
# EE only features are listed on EE::IssueAvailableFeatures
def available_features_for_issue_types
- {}.with_indifferent_access
+ {
+ assignee: %w(issue incident),
+ confidentiality: %(issue incident)
+ }.with_indifferent_access
end
end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index b1698bc2ee3..ccb334343ff 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -51,7 +51,7 @@ module Milestoneable
# Overridden on EE module
#
def supports_milestone?
- respond_to?(:milestone_id) && !incident?
+ respond_to?(:milestone_id)
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 2dbe9360d42..f3cc68e4b85 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -19,6 +19,11 @@ module Noteable
def resolvable_types
%w(MergeRequest DesignManagement::Design)
end
+
+ # `Noteable` class names that support creating/forwarding individual notes.
+ def email_creatable_types
+ %w(Issue)
+ end
end
# The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
@@ -55,6 +60,10 @@ module Noteable
supports_discussions? && self.class.replyable_types.include?(base_class_name)
end
+ def supports_creating_notes_by_email?
+ self.class.email_creatable_types.include?(base_class_name)
+ end
+
def supports_suggestion?
false
end
@@ -158,6 +167,18 @@ module Noteable
def after_note_destroyed(_note)
# no-op
end
+
+ # Email address that an authorized user can send/forward an email to be added directly
+ # to an issue or merge request.
+ # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue-34@localhost.com
+ def creatable_note_email_address(author)
+ return unless supports_creating_notes_by_email?
+
+ project_email = project.new_issuable_address(author, self.class.name.underscore)
+ return unless project_email
+
+ project_email.sub('@', "-#{iid}@")
+ end
end
Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb
new file mode 100644
index 00000000000..4aa633e0357
--- /dev/null
+++ b/app/models/concerns/packages/debian/architecture.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ module Architecture
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures
+
+ validates :distribution,
+ presence: true
+
+ validates :name,
+ presence: true,
+ length: { maximum: 255 },
+ uniqueness: { scope: %i[distribution_id] },
+ format: { with: Gitlab::Regex.debian_architecture_regex }
+
+ scope :with_distribution, ->(distribution) { where(distribution: distribution) }
+ scope :with_name, ->(name) { where(name: name) }
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
new file mode 100644
index 00000000000..285d293c9ee
--- /dev/null
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ module Distribution
+ extend ActiveSupport::Concern
+
+ included do
+ include FileStoreMounter
+
+ def self.container_foreign_key
+ "#{container_type}_id".to_sym
+ end
+
+ alias_attribute :container, container_type
+ alias_attribute :container_id, "#{container_type}_id"
+
+ belongs_to container_type
+ belongs_to :creator, class_name: 'User'
+
+ has_many :architectures,
+ class_name: "Packages::Debian::#{container_type.capitalize}Architecture",
+ foreign_key: :distribution_id,
+ inverse_of: :distribution
+
+ validates :codename,
+ presence: true,
+ uniqueness: { scope: [container_foreign_key] },
+ format: { with: Gitlab::Regex.debian_distribution_regex }
+
+ validates :suite,
+ allow_nil: true,
+ format: { with: Gitlab::Regex.debian_distribution_regex }
+ validates :suite,
+ uniqueness: { scope: [container_foreign_key] },
+ if: :suite
+
+ validate :unique_codename_and_suite
+
+ validates :origin,
+ allow_nil: true,
+ format: { with: Gitlab::Regex.debian_distribution_regex }
+
+ validates :label,
+ allow_nil: true,
+ format: { with: Gitlab::Regex.debian_distribution_regex }
+
+ validates :version,
+ allow_nil: true,
+ format: { with: Gitlab::Regex.debian_version_regex }
+
+ # The Valid-Until field is a security measure to prevent malicious attackers to
+ # serve an outdated repository, with vulnerable packages
+ # (keeping in mind that most Debian repository are not using TLS but use GPG
+ # signatures instead).
+ # A minimum of 24 hours is simply to avoid generating indices too often
+ # (which generates load).
+ # Official Debian repositories are generated 4 times a day, and valid for 7 days.
+ # Full ref: https://wiki.debian.org/DebianRepository/Format#Date.2C_Valid-Until
+ validates :valid_time_duration_seconds,
+ allow_nil: true,
+ numericality: { greater_than_or_equal_to: 24.hours.to_i }
+
+ validates container_type, presence: true
+ validates :file_store, presence: true
+
+ validates :file_signature, absence: true
+ validates :signing_keys, absence: true
+
+ scope :with_container, ->(subject) { where(container_type => subject) }
+ scope :with_codename, ->(codename) { where(codename: codename) }
+ scope :with_suite, ->(suite) { where(suite: suite) }
+ scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) }
+
+ attr_encrypted :signing_keys,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+
+ mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
+
+ def needs_update?
+ !file.exists? || time_duration_expired?
+ end
+
+ private
+
+ def time_duration_expired?
+ return false unless valid_time_duration_seconds.present?
+
+ updated_at + valid_time_duration_seconds.seconds + 6.hours < Time.current
+ end
+
+ def unique_codename_and_suite
+ errors.add(:codename, _('has already been taken as Suite')) if codename_exists_as_suite?
+ errors.add(:suite, _('has already been taken as Codename')) if suite_exists_as_codename?
+ end
+
+ def codename_exists_as_suite?
+ return false unless codename.present?
+
+ self.class.with_container(container).with_suite(codename).exists?
+ end
+
+ def suite_exists_as_codename?
+ return false unless suite.present?
+
+ self.class.with_container(container).with_codename(suite).exists?
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/repositories/can_housekeep_repository.rb b/app/models/concerns/repositories/can_housekeep_repository.rb
new file mode 100644
index 00000000000..2b79851a07c
--- /dev/null
+++ b/app/models/concerns/repositories/can_housekeep_repository.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Repositories
+ module CanHousekeepRepository
+ extend ActiveSupport::Concern
+
+ def pushes_since_gc
+ Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
+ end
+
+ def increment_pushes_since_gc
+ Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
+ end
+
+ def reset_pushes_since_gc
+ Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
+ end
+
+ private
+
+ def pushes_since_gc_redis_shared_state_key
+ "#{self.class.name.underscore.pluralize}/#{id}/pushes_since_gc"
+ end
+ end
+end
diff --git a/app/models/cycle_analytics/level_base.rb b/app/models/cycle_analytics/level_base.rb
deleted file mode 100644
index 901636a7263..00000000000
--- a/app/models/cycle_analytics/level_base.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module CycleAnalytics
- module LevelBase
- STAGES = %i[issue plan code test review staging].freeze
-
- # This is a temporary adapter class which makes the new value stream (cycle analytics)
- # backend compatible with the old implementation.
- class StageAdapter
- def initialize(stage, options)
- @stage = stage
- @options = options
- end
-
- # rubocop: disable CodeReuse/Presenter
- def as_json(serializer: AnalyticsStageSerializer)
- presenter = Analytics::CycleAnalytics::StagePresenter.new(stage)
-
- serializer.new.represent(OpenStruct.new(
- title: presenter.title,
- description: presenter.description,
- legend: presenter.legend,
- name: stage.name,
- project_median: median,
- group_median: median
- ))
- end
- # rubocop: enable CodeReuse/Presenter
-
- def events
- data_collector.records_fetcher.serialized_records
- end
-
- def median
- data_collector.median.seconds
- end
-
- alias_method :project_median, :median
- alias_method :group_median, :median
-
- private
-
- attr_reader :stage, :options
-
- def data_collector
- @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options)
- end
- end
-
- def all_medians_by_stage
- STAGES.each_with_object({}) do |stage_name, medians_per_stage|
- medians_per_stage[stage_name] = self[stage_name].median
- end
- end
-
- def stats
- @stats ||= STAGES.map do |stage_name|
- self[stage_name].as_json
- end
- end
-
- def [](stage_name)
- if Feature.enabled?(:new_project_level_vsa_backend, resource_parent, default_enabled: true)
- StageAdapter.new(build_stage(stage_name), options)
- else
- Gitlab::CycleAnalytics::Stage[stage_name].new(options: options)
- end
- end
-
- def stage_params_by_name(name)
- Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name)
- end
- end
-end
diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb
index 26cdcc0db4b..5bd07b3f6c3 100644
--- a/app/models/cycle_analytics/project_level.rb
+++ b/app/models/cycle_analytics/project_level.rb
@@ -2,7 +2,6 @@
module CycleAnalytics
class ProjectLevel
- include LevelBase
attr_reader :project, :options
def initialize(project, options:)
@@ -21,13 +20,29 @@ module CycleAnalytics
Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
end
+ def stats
+ @stats ||= default_stage_names.map do |stage_name|
+ self[stage_name].as_json
+ end
+ end
+
+ def [](stage_name)
+ CycleAnalytics::ProjectLevelStageAdapter.new(build_stage(stage_name), options)
+ end
+
+ private
+
def build_stage(stage_name)
stage_params = stage_params_by_name(stage_name).merge(project: project)
Analytics::CycleAnalytics::ProjectStage.new(stage_params)
end
- def resource_parent
- project
+ def stage_params_by_name(name)
+ Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name)
+ end
+
+ def default_stage_names
+ Gitlab::Analytics::CycleAnalytics::DefaultStages.symbolized_stage_names
end
end
end
diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb
new file mode 100644
index 00000000000..dd4afa9b809
--- /dev/null
+++ b/app/models/cycle_analytics/project_level_stage_adapter.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# This adapter class makes the new value stream (cycle analytics) backend
+# compatible with the old value stream controller actions.
+module CycleAnalytics
+ class ProjectLevelStageAdapter
+ def initialize(stage, options)
+ @stage = stage
+ @options = options
+ end
+
+ # rubocop: disable CodeReuse/Presenter
+ def as_json(serializer: AnalyticsStageSerializer)
+ presenter = Analytics::CycleAnalytics::StagePresenter.new(stage)
+
+ serializer.new.represent(OpenStruct.new(
+ title: presenter.title,
+ description: presenter.description,
+ legend: presenter.legend,
+ name: stage.name,
+ project_median: median
+ ))
+ end
+ # rubocop: enable CodeReuse/Presenter
+
+ def events
+ data_collector.records_fetcher.serialized_records
+ end
+
+ def median
+ data_collector.median.seconds
+ end
+
+ alias_method :project_median, :median
+
+ private
+
+ attr_reader :stage, :options
+
+ def data_collector
+ @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options)
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index b93b714ec8b..6f40466394a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -109,6 +109,23 @@ class Deployment < ApplicationRecord
Deployments::ExecuteHooksWorker.perform_async(id)
end
end
+
+ after_transition any => any - [:skipped] do |deployment, transition|
+ next if transition.loopback?
+ next unless Feature.enabled?(:jira_sync_deployments, deployment.project)
+
+ deployment.run_after_commit do
+ ::JiraConnect::SyncDeploymentsWorker.perform_async(id)
+ end
+ end
+ end
+
+ after_create unless: :importing? do |deployment|
+ next unless Feature.enabled?(:jira_sync_deployments, deployment.project)
+
+ run_after_commit do
+ ::JiraConnect::SyncDeploymentsWorker.perform_async(deployment.id)
+ end
end
enum status: {
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 944a64f5419..c8a0773cc5b 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -111,6 +111,10 @@ class DiffNote < Note
super.merge(suggestions_filter_enabled: true)
end
+ def multiline?
+ position&.multiline?
+ end
+
private
def enqueue_diff_file_creation_job
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
index a4cacab25ee..7dbc95f617a 100644
--- a/app/models/experiment.rb
+++ b/app/models/experiment.rb
@@ -16,7 +16,9 @@ class Experiment < ApplicationRecord
# Create or update the recorded experiment_user row for the user in this experiment.
def record_user_and_group(user, group_type, context = {})
- experiment_users.find_or_initialize_by(user: user).update!(group_type: group_type, context: context)
+ experiment_user = experiment_users.find_or_initialize_by(user: user)
+ merged_context = experiment_user.context.deep_merge(context.deep_stringify_keys)
+ experiment_user.update!(group_type: group_type, context: merged_context)
end
def record_conversion_event_for_user(user)
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 995baf8565c..ca6857a14b6 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -127,3 +127,5 @@ class GpgKey < ApplicationRecord
end
end
end
+
+GpgKey.prepend_if_ee('EE::GpgKey')
diff --git a/app/models/group.rb b/app/models/group.rb
index 739135e82dd..903d0154969 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -75,6 +75,9 @@ class Group < Namespace
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
+ # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 253f4465cd9..5da9f67f6ef 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -434,6 +434,10 @@ class Issue < ApplicationRecord
moved_to || duplicated_to
end
+ def supports_assignee?
+ issue_type_supports?(:assignee)
+ end
+
private
def ensure_metrics
diff --git a/app/models/list.rb b/app/models/list.rb
index 1df565c83e6..49834af3dfb 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class List < ApplicationRecord
+ include Boards::Listable
include Importable
belongs_to :board
@@ -10,30 +11,13 @@ class List < ApplicationRecord
enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4, iteration: 5 }
validates :board, :list_type, presence: true, unless: :importing?
- validates :label, :position, presence: true, if: :label?
validates :label_id, uniqueness: { scope: :board_id }, if: :label?
- validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable?
-
- before_destroy :can_be_destroyed
-
- scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
- scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
scope :preload_associated_models, -> { preload(:board, label: :priorities) }
- scope :ordered, -> { order(:list_type, :position) }
-
alias_method :preferences, :list_user_preferences
class << self
- def destroyable_types
- [:label]
- end
-
- def movable_types
- [:label]
- end
-
def preload_preferences_for_user(lists, user)
return unless user
@@ -60,18 +44,6 @@ class List < ApplicationRecord
preferences_for(user).update(preferences)
end
- def destroyable?
- self.class.destroyable_types.include?(list_type&.to_sym)
- end
-
- def movable?
- self.class.movable_types.include?(list_type&.to_sym)
- end
-
- def title
- label? ? label.name : list_type.humanize
- end
-
def collapsed?(user)
preferences = preferences_for(user)
@@ -95,12 +67,6 @@ class List < ApplicationRecord
end
end
end
-
- private
-
- def can_be_destroyed
- throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow
- end
end
List.prepend_if_ee('::EE::List')
diff --git a/app/models/member.rb b/app/models/member.rb
index 687830f5267..2e79b50d6b7 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -13,6 +13,8 @@ class Member < ApplicationRecord
include FromUnion
include UpdateHighestRole
+ AVATAR_SIZE = 40
+
attr_accessor :raw_invite_token
belongs_to :created_by, class_name: "User"
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 2bbcdbbe5ce..c30f6dc81ee 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -62,7 +62,9 @@ class GroupMember < Member
end
def post_create_hook
- run_after_commit_or_now { notification_service.new_group_member(self) }
+ if send_welcome_email?
+ run_after_commit_or_now { notification_service.new_group_member(self) }
+ end
super
end
@@ -72,6 +74,10 @@ class GroupMember < Member
run_after_commit { notification_service.update_group_member(self) }
end
+ if saved_change_to_expires_at?
+ run_after_commit { notification_service.updated_group_member_expiration(self) }
+ end
+
super
end
@@ -87,6 +93,10 @@ class GroupMember < Member
super
end
+
+ def send_welcome_email?
+ true
+ end
end
GroupMember.prepend_if_ee('EE::GroupMember')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 043f07cf9f3..64b8223a1f0 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -261,6 +261,19 @@ class MergeRequest < ApplicationRecord
scope :by_merge_commit_sha, -> (sha) do
where(merge_commit_sha: sha)
end
+ scope :by_squash_commit_sha, -> (sha) do
+ where(squash_commit_sha: sha)
+ end
+ scope :by_related_commit_sha, -> (sha) do
+ from_union(
+ [
+ by_commit_sha(sha),
+ by_squash_commit_sha(sha),
+ by_merge_commit_sha(sha)
+ ],
+ remove_duplicates: false
+ )
+ end
scope :by_cherry_pick_sha, -> (sha) do
joins(:notes).where(notes: { commit_id: sha })
end
@@ -493,6 +506,10 @@ class MergeRequest < ApplicationRecord
work_in_progress?(title) ? title : "Draft: #{title}"
end
+ def self.participant_includes
+ [:reviewers, :award_emoji] + super
+ end
+
def committers
@committers ||= commits.committers
end
@@ -1639,18 +1656,6 @@ class MergeRequest < ApplicationRecord
!has_commits?
end
- def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
- return false unless can_be_merged_by?(current_user)
-
- return true if autocomplete_precheck
-
- return false unless mergeable?(skip_ci_check: true)
- return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
- return false if last_diff_sha != diff_head_sha
-
- true
- end
-
def pipeline_coverage_delta
if base_pipeline&.coverage && head_pipeline&.coverage
'%.2f' % (head_pipeline.coverage.to_f - base_pipeline.coverage.to_f)
@@ -1762,6 +1767,10 @@ class MergeRequest < ApplicationRecord
false
end
+ def supports_assignee?
+ true
+ end
+
private
def with_rebase_lock
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c244150e7a3..aa4ddfede99 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -33,6 +33,7 @@ class Milestone < ApplicationRecord
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
+ scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 238e8f70778..6f7b377ee52 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -28,7 +28,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 :namespace_onboarding_actions
+ has_one :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`.
@@ -40,6 +40,7 @@ class Namespace < ApplicationRecord
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
+ has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting'
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
@@ -160,6 +161,10 @@ class Namespace < ApplicationRecord
end
end
+ def package_settings
+ package_setting_relation || build_package_setting_relation
+ end
+
def default_branch_protection
super || Gitlab::CurrentSettings.default_branch_protection
end
@@ -438,6 +443,10 @@ class Namespace < ApplicationRecord
end
end
+ def root?
+ !has_parent?
+ end
+
private
def all_projects_with_pages
diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb
new file mode 100644
index 00000000000..a2064e020b3
--- /dev/null
+++ b/app/models/namespace/package_setting.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class Namespace::PackageSetting < ApplicationRecord
+ self.primary_key = :namespace_id
+ self.table_name = 'namespace_package_settings'
+
+ PackageSettingNotImplemented = Class.new(StandardError)
+
+ PACKAGES_WITH_SETTINGS = %w[maven].freeze
+
+ belongs_to :namespace, inverse_of: :package_setting_relation
+
+ validates :namespace, presence: true
+ validates :maven_duplicates_allowed, inclusion: { in: [true, false] }
+ validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
+
+ class << self
+ def duplicates_allowed?(package)
+ return true unless package
+ raise PackageSettingNotImplemented unless PACKAGES_WITH_SETTINGS.include?(package.package_type)
+
+ duplicates_allowed = package.package_settings["#{package.package_type}_duplicates_allowed"]
+ regex = ::Gitlab::UntrustedRegexp.new("\\A#{package.package_settings["#{package.package_type}_duplicate_exception_regex"]}\\z")
+
+ duplicates_allowed || regex.match?(package.name)
+ end
+ end
+end
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index a3df82998c4..90aeee7a4f1 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Namespace::RootStorageStatistics < ApplicationRecord
- SNIPPETS_SIZE_STAT_NAME = 'snippets_size'.freeze
+ SNIPPETS_SIZE_STAT_NAME = 'snippets_size'
STATISTICS_ATTRIBUTES = %W(
storage_size
repository_size
diff --git a/app/models/namespace_onboarding_action.rb b/app/models/namespace_onboarding_action.rb
deleted file mode 100644
index 43dd872673c..00000000000
--- a/app/models/namespace_onboarding_action.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class NamespaceOnboardingAction < ApplicationRecord
- belongs_to :namespace, optional: false
-
- validates :action, presence: true
-
- ACTIONS = {
- subscription_created: 1,
- git_write: 2,
- merge_request_created: 3,
- git_read: 4,
- user_added: 6
- }.freeze
-
- enum action: ACTIONS
-
- class << self
- def completed?(namespace, action)
- where(namespace: namespace, action: action).exists?
- end
-
- def create_action(namespace, action)
- NamespaceOnboardingAction.safe_find_or_create_by(namespace: namespace, action: action)
- end
- end
-end
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
new file mode 100644
index 00000000000..419bbd595e9
--- /dev/null
+++ b/app/models/onboarding_progress.rb
@@ -0,0 +1,65 @@
+# 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_auto_closed,
+ :repository_imported,
+ :repository_mirrored
+ ].freeze
+
+ class << self
+ def onboard(namespace)
+ return unless root_namespace?(namespace)
+
+ safe_find_or_create_by(namespace: namespace)
+ end
+
+ def register(namespace, action)
+ return unless root_namespace?(namespace) && ACTIONS.include?(action)
+
+ action_column = column_name(action)
+ onboarding_progress = find_by(namespace: namespace, action_column => nil)
+ onboarding_progress&.update!(action_column => Time.current)
+ 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
+
+ private
+
+ def column_name(action)
+ :"#{action}_at"
+ end
+
+ def root_namespace?(namespace)
+ namespace && namespace.root?
+ end
+ 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/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb
index de54580e948..64ae5cd88a5 100644
--- a/app/models/packages/conan/file_metadatum.rb
+++ b/app/models/packages/conan/file_metadatum.rb
@@ -3,8 +3,8 @@
class Packages::Conan::FileMetadatum < ApplicationRecord
belongs_to :package_file, inverse_of: :conan_file_metadatum
- DEFAULT_PACKAGE_REVISION = '0'.freeze
- DEFAULT_RECIPE_REVISION = '0'.freeze
+ DEFAULT_PACKAGE_REVISION = '0'
+ DEFAULT_RECIPE_REVISION = '0'
validates :package_file, presence: true
diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb
new file mode 100644
index 00000000000..f7f7f9f95e9
--- /dev/null
+++ b/app/models/packages/debian.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ def self.table_name_prefix
+ 'packages_debian_'
+ end
+ end
+end
diff --git a/app/models/packages/debian/file_metadatum.rb b/app/models/packages/debian/file_metadatum.rb
new file mode 100644
index 00000000000..7c9f4f5f3f1
--- /dev/null
+++ b/app/models/packages/debian/file_metadatum.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class Packages::Debian::FileMetadatum < ApplicationRecord
+ belongs_to :package_file, inverse_of: :debian_file_metadatum
+
+ validates :package_file, presence: true
+ validate :valid_debian_package_type
+
+ enum file_type: {
+ unknown: 1, source: 2, dsc: 3, deb: 4, udeb: 5, buildinfo: 6, changes: 7
+ }
+
+ validates :file_type, presence: true
+ validates :file_type, inclusion: { in: %w[unknown] }, if: -> { package_file&.package&.debian_incoming? }
+ validates :file_type,
+ inclusion: { in: %w[source dsc deb udeb buildinfo changes] },
+ if: -> { package_file&.package&.debian_package? }
+
+ validates :component,
+ presence: true,
+ format: { with: Gitlab::Regex.debian_component_regex },
+ if: :requires_component?
+ validates :component, absence: true, unless: :requires_component?
+
+ validates :architecture,
+ presence: true,
+ format: { with: Gitlab::Regex.debian_architecture_regex },
+ if: :requires_architecture?
+ validates :architecture, absence: true, unless: :requires_architecture?
+
+ validates :fields,
+ presence: true,
+ json_schema: { filename: "debian_fields" },
+ if: :requires_fields?
+ validates :fields, absence: true, unless: :requires_fields?
+
+ private
+
+ def valid_debian_package_type
+ return if package_file&.package&.debian?
+
+ errors.add(:package_file, _('Package type must be Debian'))
+ end
+
+ def requires_architecture?
+ deb? || udeb?
+ end
+
+ def requires_component?
+ source? || dsc? || requires_architecture? || buildinfo?
+ end
+
+ def requires_fields?
+ dsc? || requires_architecture? || buildinfo? || changes?
+ end
+end
diff --git a/app/models/packages/debian/group_architecture.rb b/app/models/packages/debian/group_architecture.rb
new file mode 100644
index 00000000000..570f6accd3c
--- /dev/null
+++ b/app/models/packages/debian/group_architecture.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::GroupArchitecture < ApplicationRecord
+ def self.container_type
+ :group
+ end
+
+ include Packages::Debian::Architecture
+end
diff --git a/app/models/packages/debian/group_distribution.rb b/app/models/packages/debian/group_distribution.rb
new file mode 100644
index 00000000000..eea7acacc96
--- /dev/null
+++ b/app/models/packages/debian/group_distribution.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::GroupDistribution < ApplicationRecord
+ def self.container_type
+ :group
+ end
+
+ include Packages::Debian::Distribution
+end
diff --git a/app/models/packages/debian/project_architecture.rb b/app/models/packages/debian/project_architecture.rb
new file mode 100644
index 00000000000..44a38dfaf44
--- /dev/null
+++ b/app/models/packages/debian/project_architecture.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::ProjectArchitecture < ApplicationRecord
+ def self.container_type
+ :project
+ end
+
+ include Packages::Debian::Architecture
+end
diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb
new file mode 100644
index 00000000000..a73c12d172d
--- /dev/null
+++ b/app/models/packages/debian/project_distribution.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::ProjectDistribution < ApplicationRecord
+ def self.container_type
+ :project
+ end
+
+ include Packages::Debian::Distribution
+end
diff --git a/app/models/packages/dependency.rb b/app/models/packages/dependency.rb
index 51b80934827..a32c3c05bb3 100644
--- a/app/models/packages/dependency.rb
+++ b/app/models/packages/dependency.rb
@@ -6,7 +6,7 @@ class Packages::Dependency < ApplicationRecord
validates :name, uniqueness: { scope: :version_pattern }
- NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'.freeze
+ NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'
MAX_STRING_LENGTH = 255.freeze
MAX_CHUNKED_QUERIES_COUNT = 10.freeze
diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb
index 13da82d16d3..98c9d5246db 100644
--- a/app/models/packages/event.rb
+++ b/app/models/packages/event.rb
@@ -6,6 +6,8 @@ class Packages::Event < ApplicationRecord
UNIQUE_EVENTS_ALLOWED = %i[push_package delete_package pull_package].freeze
EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze
+ EVENT_PREFIX = "i_package"
+
enum event_scope: EVENT_SCOPES
enum event_type: {
@@ -24,13 +26,6 @@ class Packages::Event < ApplicationRecord
enum originator_type: { user: 0, deploy_token: 1, guest: 2 }
- def self.allowed_event_name(event_scope, event_type, originator)
- return unless event_allowed?(event_type)
-
- # remove `package` from the event name to avoid issues with HLLRedisCounter class parsing
- "i_package_#{event_scope}_#{originator}_#{event_type.gsub(/_packages?/, "")}"
- end
-
# Remove some of the events, for now, so we don't hammer Redis too hard.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/280770
def self.event_allowed?(event_type)
@@ -38,4 +33,23 @@ class Packages::Event < ApplicationRecord
false
end
+
+ # counter names for unique user tracking (for MAU)
+ def self.unique_counters_for(event_scope, event_type, originator_type)
+ return [] unless event_allowed?(event_type)
+ return [] if originator_type.to_s == 'guest'
+
+ ["#{EVENT_PREFIX}_#{event_scope}_#{originator_type}"]
+ end
+
+ # total counter names for tracking number of events
+ def self.counters_for(event_scope, event_type, originator_type)
+ return [] unless event_allowed?(event_type)
+
+ [
+ "#{EVENT_PREFIX}_#{event_type}",
+ "#{EVENT_PREFIX}_#{event_type}_by_#{originator_type}",
+ "#{EVENT_PREFIX}_#{event_scope}_#{event_type}"
+ ]
+ end
end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 10c98f03804..2067a800ad5 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -112,6 +112,7 @@ class Packages::Package < ApplicationRecord
scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') }
scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') }
scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') }
+ scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') }
def self.for_projects(projects)
return none unless projects.any?
@@ -199,6 +200,12 @@ class Packages::Package < ApplicationRecord
debian? && !version.nil?
end
+ def package_settings
+ strong_memoize(:package_settings) do
+ project.namespace.package_settings
+ end
+ end
+
private
def composer_tag_version?
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index e8d1dd1e8c4..389edaea392 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -5,14 +5,17 @@ class Packages::PackageFile < ApplicationRecord
delegate :project, :project_id, to: :package
delegate :conan_file_type, to: :conan_file_metadatum
+ delegate :file_type, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
belongs_to :package
has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum'
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
has_many :pipelines, through: :package_file_build_infos
+ has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum'
accepts_nested_attributes_for :conan_file_metadatum
+ accepts_nested_attributes_for :debian_file_metadatum
validates :package, presence: true
validates :file, presence: true
@@ -25,12 +28,18 @@ class Packages::PackageFile < ApplicationRecord
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
+ scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
scope :with_conan_file_type, ->(file_type) do
joins(:conan_file_metadatum)
.where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] })
end
+ scope :with_debian_file_type, ->(file_type) do
+ joins(:debian_file_metadatum)
+ .where(packages_debian_file_metadata: { debian_file_type: ::Packages::Debian::FileMetadatum.debian_file_types[file_type] })
+ end
+
scope :with_conan_package_reference, ->(conan_package_reference) do
joins(:conan_file_metadatum)
.where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference })
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 4004ea9a662..4d60489e599 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -188,7 +188,7 @@ class PagesDomain < ApplicationRecord
def user_provided_key=(key)
self.key = key
- self.certificate_source = 'user_provided' if key_changed?
+ self.certificate_source = 'user_provided' if attribute_changed?(:key)
end
def user_provided_certificate
@@ -207,7 +207,7 @@ class PagesDomain < ApplicationRecord
def gitlab_provided_key=(key)
self.key = key
- self.certificate_source = 'gitlab_provided' if key_changed?
+ self.certificate_source = 'gitlab_provided' if attribute_changed?(:key)
end
def pages_virtual_domain
diff --git a/app/models/plan.rb b/app/models/plan.rb
index b4091e0a755..6a7f32a5d5f 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Plan < ApplicationRecord
- DEFAULT = 'default'.freeze
+ DEFAULT = 'default'
has_one :limits, class_name: 'PlanLimits'
diff --git a/app/models/project.rb b/app/models/project.rb
index daa5605c2e0..ec790798806 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -34,6 +34,7 @@ class Project < ApplicationRecord
include FromUnion
include IgnorableColumns
include Integration
+ include Repositories::CanHousekeepRepository
include EachBatch
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
@@ -146,7 +147,6 @@ class Project < ApplicationRecord
has_many :boards
# Project services
- has_one :alerts_service
has_one :campfire_service
has_one :datadog_service
has_one :discord_service
@@ -200,6 +200,8 @@ class Project < ApplicationRecord
# Packages
has_many :packages, class_name: 'Packages::Package'
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
+ # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -408,6 +410,9 @@ class Project < ApplicationRecord
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci
+ delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, to: :ci_cd_settings, prefix: :ci
+ delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?,
+ to: :ci_cd_settings
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
:allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?,
@@ -831,6 +836,10 @@ class Project < ApplicationRecord
webide_pipelines.running_or_pending.for_user(user)
end
+ def latest_pipeline_locked
+ ci_keep_latest_artifact? ? :artifacts_locked : :unlocked
+ end
+
def autoclose_referenced_issues
return true if super.nil?
@@ -1331,19 +1340,11 @@ class Project < ApplicationRecord
end
def external_wiki
- if has_external_wiki.nil?
- cache_has_external_wiki
- end
+ cache_has_external_wiki if has_external_wiki.nil?
- if has_external_wiki
- @external_wiki ||= services.external_wikis.first
- else
- nil
- end
- end
+ return unless has_external_wiki?
- def cache_has_external_wiki
- update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
+ @external_wiki ||= services.external_wikis.first
end
def find_or_initialize_services
@@ -1355,9 +1356,9 @@ class Project < ApplicationRecord
end
def disabled_services
- return ['datadog'] unless Feature.enabled?(:datadog_ci_integration, self)
+ return %w(datadog alerts) unless Feature.enabled?(:datadog_ci_integration, self)
- []
+ %w(alerts)
end
def find_or_initialize_service(name)
@@ -1829,6 +1830,15 @@ class Project < ApplicationRecord
ensure_pages_metadatum.update!(pages_deployment: deployment)
end
+ def set_first_pages_deployment!(deployment)
+ ensure_pages_metadatum
+
+ # where().update_all to perform update in the single transaction with check for null
+ ProjectPagesMetadatum
+ .where(project_id: id, pages_deployment_id: nil)
+ .update_all(pages_deployment_id: deployment.id)
+ end
+
def write_repository_config(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
@@ -1980,6 +1990,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
.append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase)
.append(key: 'CI_DEFAULT_BRANCH', value: default_branch)
+ .append(key: 'CI_PROJECT_CONFIG_PATH', value: ci_config_path_or_default)
end
def predefined_ci_server_variables
@@ -2113,18 +2124,6 @@ class Project < ApplicationRecord
(auto_devops || build_auto_devops)&.predefined_variables
end
- def pushes_since_gc
- Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
- end
-
- def increment_pushes_since_gc
- Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
- end
-
- def reset_pushes_since_gc
- Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
- end
-
def route_map_for(commit_sha)
@route_maps_by_commit ||= Hash.new do |h, sha|
h[sha] = begin
@@ -2430,10 +2429,6 @@ class Project < ApplicationRecord
protected_branches.limit(limit)
end
- def alerts_service_activated?
- alerts_service&.active?
- end
-
def self_monitoring?
Gitlab::CurrentSettings.self_monitoring_project_id == id
end
@@ -2486,16 +2481,12 @@ class Project < ApplicationRecord
end
def service_desk_custom_address
- return unless service_desk_custom_address_enabled?
+ return unless Gitlab::ServiceDeskEmail.enabled?
key = service_desk_setting&.project_key
return unless key.present?
- ::Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
- end
-
- def service_desk_custom_address_enabled?
- ::Gitlab::ServiceDeskEmail.enabled? && ::Feature.enabled?(:service_desk_custom_address, self, default_enabled: true)
+ Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
def root_namespace
@@ -2633,10 +2624,6 @@ class Project < ApplicationRecord
from && self != from
end
- def pushes_since_gc_redis_shared_state_key
- "projects/#{id}/pushes_since_gc"
- end
-
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
@@ -2699,6 +2686,10 @@ class Project < ApplicationRecord
objects.each_batch { |relation| out.concat(relation.pluck(:oid)) }
end
end
+
+ def cache_has_external_wiki
+ update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
+ end
end
Project.prepend_if_ee('EE::Project')
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index b167c2e371b..4f445758653 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -3,8 +3,8 @@
class ProjectFeatureUsage < ApplicationRecord
self.primary_key = :project_id
- JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'.freeze
- JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'.freeze
+ JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'
+ JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'
belongs_to :project
validates :project, presence: true
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index bd1919fe7ed..2bef0056732 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectPagesMetadatum < ApplicationRecord
+ include EachBatch
+
self.primary_key = :project_id
belongs_to :project, inverse_of: :pages_metadatum
@@ -8,4 +10,5 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :pages_deployment
scope :deployed, -> { where(deployed: true) }
+ scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) }
end
diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb
index 5b7d149ace1..4afce0dfe95 100644
--- a/app/models/project_services/alerts_service.rb
+++ b/app/models/project_services/alerts_service.rb
@@ -1,54 +1,10 @@
# frozen_string_literal: true
-require 'securerandom'
-
+# This service is scheduled for removal. All records must
+# be deleted before the class can be removed.
+# https://gitlab.com/groups/gitlab-org/-/epics/5056
class AlertsService < Service
- has_one :data, class_name: 'AlertsServiceData', autosave: true,
- inverse_of: :service, foreign_key: :service_id
-
- attribute :token, :string
- delegate :token, :token=, :token_changed?, :token_was, to: :data
-
- validates :token, presence: true, if: :activated?
-
- before_validation :prevent_token_assignment
- before_validation :ensure_token, if: :activated?
-
- after_save :update_http_integration
-
- def url
- return if instance? || template?
-
- url_helpers.project_alerts_notify_url(project, format: :json)
- end
-
- def json_fields
- super + %w(token)
- end
-
- def editable?
- false
- end
-
- def show_active_box?
- false
- end
-
- def can_test?
- false
- end
-
- def title
- _('Alerts endpoint')
- end
-
- def description
- _('Authorize external services to send alerts to GitLab')
- end
-
- def detailed_description
- description
- end
+ before_save :prevent_save
def self.to_param
'alerts'
@@ -58,35 +14,15 @@ class AlertsService < Service
%w()
end
- def data
- super || build_data
- end
-
private
- def prevent_token_assignment
- self.token = token_was if token.present? && token_changed?
- end
-
- def ensure_token
- self.token = generate_token if token.blank?
- end
-
- def generate_token
- SecureRandom.hex
- end
-
- def url_helpers
- Gitlab::Routing.url_helpers
- end
-
- def update_http_integration
- return unless project_id && type == 'AlertsService'
+ def prevent_save
+ errors.add(:base, _('Alerts endpoint is deprecated and should not be created or modified. Use HTTP Integrations instead.'))
+ log_error('Prevented attempt to save or update deprecated AlertsService')
- AlertManagement::SyncAlertServiceDataService # rubocop: disable CodeReuse/ServiceClass
- .new(self)
- .execute
+ # Stops execution of callbacks and database operation while
+ # preserving expectations of #save (will not raise) & #save! (raises)
+ # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution
+ throw :abort # rubocop:disable Cop/BanCatchThrow
end
end
-
-AlertsService.prepend_if_ee('EE::AlertsService')
diff --git a/app/models/project_services/alerts_service_data.rb b/app/models/project_services/alerts_service_data.rb
deleted file mode 100644
index 5a52ed83455..00000000000
--- a/app/models/project_services/alerts_service_data.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'securerandom'
-
-class AlertsServiceData < ApplicationRecord
- belongs_to :service, class_name: 'AlertsService'
-
- validates :service, presence: true
-
- attr_encrypted :token,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
- algorithm: 'aes-256-gcm'
-end
diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb
index 543843ab1b0..3a742bfdcda 100644
--- a/app/models/project_services/datadog_service.rb
+++ b/app/models/project_services/datadog_service.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
class DatadogService < Service
- DEFAULT_SITE = 'datadoghq.com'.freeze
- URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'.freeze
- URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'.freeze
- URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/".freeze
+ DEFAULT_SITE = 'datadoghq.com'
+ URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'
+ URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'
+ URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
pipeline job
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 1f4abfc1aca..dafd3d095ec 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -157,8 +157,12 @@ class JiraService < IssueTrackerService
# support any events.
end
+ def find_issue(issue_key)
+ jira_request { client.Issue.find(issue_key) }
+ end
+
def close_issue(entity, external_issue)
- issue = jira_request { client.Issue.find(external_issue.iid) }
+ issue = find_issue(external_issue.iid)
return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
@@ -172,7 +176,7 @@ class JiraService < IssueTrackerService
# Depending on the Jira project's workflow, a comment during transition
# may or may not be allowed. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
- issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
+ issue = find_issue(issue.key) if transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
end
@@ -181,7 +185,7 @@ class JiraService < IssueTrackerService
return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) }
end
- jira_issue = jira_request { client.Issue.find(mentioned.id) }
+ jira_issue = find_issue(mentioned.id)
return unless jira_issue.present?
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 0fd85e3a5a9..f39d3947e5b 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MattermostSlashCommandsService < SlashCommandsService
- include TriggersHelper
+ include Ci::TriggersHelper
prop_accessor :token
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index 01ded0495a7..548f3623504 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SlackSlashCommandsService < SlashCommandsService
- include TriggersHelper
+ include Ci::TriggersHelper
def title
'Slack slash commands'
diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb
index 25e70ab406c..e1336be9528 100644
--- a/app/models/protectable_dropdown.rb
+++ b/app/models/protectable_dropdown.rb
@@ -12,6 +12,8 @@ class ProtectableDropdown
# Tags/branches which are yet to be individually protected
def protectable_ref_names
+ return [] if @project.empty_repo?
+
@protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names
end
diff --git a/app/models/release.rb b/app/models/release.rb
index bebf91fb247..2b82fdc37f6 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -82,7 +82,7 @@ class Release < ApplicationRecord
end
def milestone_titles
- self.milestones.map {|m| m.title }.sort.join(", ")
+ self.milestones.order_by_dates_and_title.map {|m| m.title }.join(', ')
end
def to_hook_data(action)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 6b8b34ce4d2..880970b72a8 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -308,7 +308,7 @@ class RemoteMirror < ApplicationRecord
end
def mirror_url_changed?
- url_changed? || credentials_changed?
+ url_changed? || attribute_changed?(:credentials)
end
def saved_change_to_mirror_url?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 93f22dbe122..c19448332f8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -24,10 +24,10 @@ class Repository
attr_accessor :full_path, :shard, :disk_path, :container, :repo_type
- delegate :ref_name_for_sha, to: :raw_repository
- delegate :bundle_to_disk, to: :raw_repository
delegate :lfs_enabled?, to: :container
+ delegate_missing_to :raw_repository
+
CreateTreeError = Class.new(StandardError)
AmbiguousRefError = Class.new(StandardError)
@@ -386,10 +386,6 @@ class Repository
raw_repository.expire_has_local_branches_cache
end
- def lookup_cache
- @lookup_cache ||= {}
- end
-
def expire_exists_cache
expire_method_caches(%i(exists?))
end
@@ -494,19 +490,12 @@ class Repository
expire_branches_cache if expire_cache
end
- def method_missing(msg, *args, &block)
- if msg == :lookup && !block_given?
- lookup_cache[msg] ||= {}
- lookup_cache[msg][args.join(":")] ||= raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
- else
- raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def lookup(sha)
+ strong_memoize("lookup_#{sha}") do
+ raw_repository.lookup(sha)
end
end
- def respond_to_missing?(method, include_private = false)
- raw_repository.respond_to?(method, include_private) || super
- end
-
def blob_at(sha, path)
blob = Blob.decorate(raw_repository.blob_at(sha, path), container)
diff --git a/app/models/service.rb b/app/models/service.rb
index 57c099d6f04..e5626462dd3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -19,7 +19,6 @@ class Service < ApplicationRecord
PROJECT_SPECIFIC_SERVICE_NAMES = %w[
jenkins
- alerts
].freeze
# Fake services to help with local development.
@@ -48,7 +47,6 @@ class Service < ApplicationRecord
after_commit :reset_updated_properties
after_commit :cache_project_has_external_issue_tracker
- after_commit :cache_project_has_external_wiki
belongs_to :project, inverse_of: :services
belongs_to :group, inverse_of: :services
@@ -469,12 +467,6 @@ class Service < ApplicationRecord
end
end
- def cache_project_has_external_wiki
- if project && !project.destroyed?
- project.cache_has_external_wiki
- end
- end
-
def valid_recipients?
activated? && !importing?
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 817f9d014eb..c4a7c5e25dc 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -20,6 +20,7 @@ class Snippet < ApplicationRecord
extend ::Gitlab::Utils::Override
MAX_FILE_COUNT = 10
+ MASTER_BRANCH = 'master'
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -82,6 +83,7 @@ class Snippet < ApplicationRecord
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> { includes(author: :status) }
scope :with_statistics, -> { joins(:statistics) }
+ scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
attr_mentionable :description
@@ -311,13 +313,27 @@ class Snippet < ApplicationRecord
override :default_branch
def default_branch
- super || 'master'
+ super || MASTER_BRANCH
end
def repository_storage
snippet_repository&.shard_name || self.class.pick_repository_storage
end
+ # Repositories are created by default with the `master` branch.
+ # This method changes the `HEAD` file to point to the existing
+ # default branch in case it's not master.
+ def change_head_to_default_branch
+ return unless repository.exists?
+ return if default_branch == MASTER_BRANCH
+ # All snippets must have at least 1 file. Therefore, if
+ # `HEAD` is empty is because it's pointing to the wrong
+ # default branch
+ return unless repository.empty? || list_files('HEAD').empty?
+
+ repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}")
+ end
+
def create_repository
return if repository_exists? && snippet_repository
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index fa25a6f8441..54dbc579d54 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class SnippetRepository < ApplicationRecord
+ include EachBatch
include Shardable
DEFAULT_EMPTY_FILE_NAME = 'snippetfile'
diff --git a/app/models/snippet_repository_storage_move.rb b/app/models/snippet_repository_storage_move.rb
index a365569bfa8..bb157c08995 100644
--- a/app/models/snippet_repository_storage_move.rb
+++ b/app/models/snippet_repository_storage_move.rb
@@ -12,7 +12,11 @@ class SnippetRepositoryStorageMove < ApplicationRecord
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
- # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/218991
+ SnippetUpdateRepositoryStorageWorker.perform_async(
+ snippet_id,
+ destination_storage_name,
+ id
+ )
end
private
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 1b99f310e1a..efbbd86ae4a 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -27,6 +27,8 @@ module Terraform
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
+ before_destroy :ensure_state_is_unlocked
+
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
def latest_file
@@ -87,6 +89,13 @@ module Terraform
new_version.save!
end
+ def ensure_state_is_unlocked
+ return unless locked?
+
+ errors.add(:base, s_("Terraform|You cannot remove the State file because it's locked. Unlock the State file first before removing it."))
+ throw :abort # rubocop:disable Cop/BanCatchThrow
+ end
+
def parse_serial(file)
Gitlab::Json.parse(file)["serial"]
rescue JSON::ParserError
diff --git a/app/models/user.rb b/app/models/user.rb
index c735f20b92c..b4ec6064ff8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -31,7 +31,7 @@ class User < ApplicationRecord
INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
- BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'.freeze
+ BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
@@ -1358,6 +1358,7 @@ class User < ApplicationRecord
def hook_attrs
{
+ id: id,
name: name,
username: username,
avatar_url: avatar_url(only_path: false),
@@ -1377,7 +1378,14 @@ class User < ApplicationRecord
def set_username_errors
namespace_path_errors = self.errors.delete(:"namespace.path")
- self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
+
+ return unless namespace_path_errors&.any?
+
+ if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username)
+ self.errors.add(:base, :username_exists_as_a_different_namespace)
+ else
+ self.errors[:username].concat(namespace_path_errors)
+ end
end
def username_changed_hook
@@ -1564,6 +1572,12 @@ class User < ApplicationRecord
end
end
+ def review_requested_open_merge_requests_count(force: false)
+ Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
+ MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count
+ end
+ end
+
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
@@ -1607,6 +1621,7 @@ class User < ApplicationRecord
def invalidate_merge_request_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
+ Rails.cache.delete(['users', id, 'review_requested_open_merge_requests_count'])
end
def invalidate_todos_done_count
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index b49a7eb72dc..49b93ffaf66 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -8,8 +8,6 @@ class UserPreference < ApplicationRecord
# extra methods that aren't really needed here.
NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
- ignore_column :feature_filter_type, remove_with: '13.8', remove_after: '2021-01-22'
-
belongs_to :user
scope :with_user, -> { joins(:user) }
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index e329a094319..11c10a61d18 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -3,6 +3,7 @@
class Wiki
extend ::Gitlab::Utils::Override
include HasRepository
+ include Repositories::CanHousekeepRepository
include Gitlab::Utils::StrongMemoize
include GlobalID::Identification