summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/models
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
downloadgitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/active_session.rb25
-rw-r--r--app/models/application_setting.rb50
-rw-r--r--app/models/application_setting_implementation.rb17
-rw-r--r--app/models/audit_event.rb9
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/bulk_imports/entity.rb15
-rw-r--r--app/models/ci/bridge.rb6
-rw-r--r--app/models/ci/build.rb65
-rw-r--r--app/models/ci/build_dependencies.rb2
-rw-r--r--app/models/ci/build_trace_chunk.rb20
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb10
-rw-r--r--app/models/ci/build_trace_section.rb1
-rw-r--r--app/models/ci/daily_build_group_report_result.rb5
-rw-r--r--app/models/ci/job_artifact.rb10
-rw-r--r--app/models/ci/pipeline.rb36
-rw-r--r--app/models/ci/pipeline_artifact.rb25
-rw-r--r--app/models/ci/pipeline_schedule.rb2
-rw-r--r--app/models/ci/processable.rb56
-rw-r--r--app/models/ci/resource.rb6
-rw-r--r--app/models/ci/resource_group.rb10
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/clusters/agent.rb1
-rw-r--r--app/models/clusters/agent_token.rb1
-rw-r--r--app/models/clusters/applications/cert_manager.rb2
-rw-r--r--app/models/clusters/applications/crossplane.rb2
-rw-r--r--app/models/clusters/applications/ingress.rb2
-rw-r--r--app/models/clusters/applications/jupyter.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.rb8
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb34
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb19
-rw-r--r--app/models/concerns/atomic_internal_id.rb44
-rw-r--r--app/models/concerns/cacheable_attributes.rb2
-rw-r--r--app/models/concerns/can_move_repository_storage.rb21
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb8
-rw-r--r--app/models/concerns/featurable.rb3
-rw-r--r--app/models/concerns/has_repository.rb9
-rw-r--r--app/models/concerns/nullify_if_blank.rb48
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb4
-rw-r--r--app/models/concerns/packages/debian/architecture.rb6
-rw-r--r--app/models/concerns/packages/debian/component.rb31
-rw-r--r--app/models/concerns/packages/debian/component_file.rb101
-rw-r--r--app/models/concerns/packages/debian/distribution.rb10
-rw-r--r--app/models/concerns/protected_ref.rb16
-rw-r--r--app/models/concerns/repositories/can_housekeep_repository.rb4
-rw-r--r--app/models/concerns/repository_storage_movable.rb24
-rw-r--r--app/models/concerns/spammable.rb11
-rw-r--r--app/models/concerns/suppress_composite_primary_key_warning.rb19
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb10
-rw-r--r--app/models/concerns/triggerable_hooks.rb3
-rw-r--r--app/models/container_repository.rb1
-rw-r--r--app/models/custom_emoji.rb9
-rw-r--r--app/models/deployment.rb18
-rw-r--r--app/models/deployment_merge_request.rb2
-rw-r--r--app/models/design_management/design.rb11
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/experiment.rb8
-rw-r--r--app/models/group.rb14
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/issue_assignee.rb2
-rw-r--r--app/models/issue_link.rb4
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/license_template.rb9
-rw-r--r--app/models/merge_request.rb87
-rw-r--r--app/models/merge_request/metrics.rb8
-rw-r--r--app/models/merge_request_context_commit.rb3
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb25
-rw-r--r--app/models/merge_request_diff_commit.rb27
-rw-r--r--app/models/merge_request_diff_file.rb2
-rw-r--r--app/models/merge_request_reviewer.rb9
-rw-r--r--app/models/milestone_release.rb2
-rw-r--r--app/models/namespace.rb57
-rw-r--r--app/models/namespaces/traversal/recursive.rb61
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/onboarding_progress.rb26
-rw-r--r--app/models/operations/feature_flag.rb26
-rw-r--r--app/models/packages/composer/cache_file.rb23
-rw-r--r--app/models/packages/composer/metadatum.rb3
-rw-r--r--app/models/packages/debian/group_component.rb9
-rw-r--r--app/models/packages/debian/group_component_file.rb9
-rw-r--r--app/models/packages/debian/project_component.rb9
-rw-r--r--app/models/packages/debian/project_component_file.rb9
-rw-r--r--app/models/packages/debian/project_distribution.rb3
-rw-r--r--app/models/packages/debian/publication.rb24
-rw-r--r--app/models/packages/package.rb28
-rw-r--r--app/models/packages/package_file.rb4
-rw-r--r--app/models/packages/rubygems/metadatum.rb24
-rw-r--r--app/models/pages/lookup_path.rb14
-rw-r--r--app/models/pages/virtual_domain.rb11
-rw-r--r--app/models/pages_deployment.rb8
-rw-r--r--app/models/project.rb56
-rw-r--r--app/models/project_authorization.rb1
-rw-r--r--app/models/project_ci_cd_setting.rb5
-rw-r--r--app/models/project_pages_metadatum.rb3
-rw-r--r--app/models/project_services/alerts_service.rb28
-rw-r--r--app/models/project_services/chat_notification_service.rb26
-rw-r--r--app/models/project_services/confluence_service.rb4
-rw-r--r--app/models/project_services/datadog_service.rb40
-rw-r--r--app/models/project_services/jira_service.rb22
-rw-r--r--app/models/project_services/jira_tracker_data.rb9
-rw-r--r--app/models/project_services/mock_deployment_service.rb38
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_statistics.rb1
-rw-r--r--app/models/protected_branch/push_access_level.rb2
-rw-r--r--app/models/push_event_payload.rb2
-rw-r--r--app/models/readme_blob.rb17
-rw-r--r--app/models/release.rb1
-rw-r--r--app/models/repository.rb64
-rw-r--r--app/models/repository_language.rb2
-rw-r--r--app/models/service.rb19
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/terraform/state.rb2
-rw-r--r--app/models/terraform/state_version.rb6
-rw-r--r--app/models/token_with_iv.rb23
-rw-r--r--app/models/u2f_registration.rb16
-rw-r--r--app/models/user.rb34
-rw-r--r--app/models/user_callout.rb20
-rw-r--r--app/models/user_interacted_project.rb2
-rw-r--r--app/models/user_status.rb16
-rw-r--r--app/models/users/user_follow_user.rb7
-rw-r--r--app/models/vulnerability.rb10
-rw-r--r--app/models/wiki.rb11
125 files changed, 1443 insertions, 486 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index dded0eb1dc3..823685f78f4 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -1,5 +1,24 @@
# frozen_string_literal: true
+# Backing store for GitLab session data.
+#
+# The raw session information is stored by the Rails session store
+# (config/initializers/session_store.rb). These entries are accessible by the
+# rack_key_name class method and consistute the base of the session data
+# entries. All other entries in the session store can be traced back to these
+# entries.
+#
+# After a user logs in (config/initializers/warden.rb) a further entry is made
+# in Redis. This entry holds a record of the user's logged in session. These
+# are accessible with the key_name(user_id, session_id) class method. These
+# entries will expire. Lookups to these entries are lazilly cleaned on future
+# user access.
+#
+# There is a reference to all sessions that belong to a specific user. A
+# user may login through multiple browsers/devices and thus record multiple
+# login sessions. These are accessible through the lookup_key_name(user_id)
+# class method.
+#
class ActiveSession
include ActiveModel::Model
@@ -143,6 +162,10 @@ class ActiveSession
list(user).reject(&:is_impersonated)
end
+ def self.rack_key_name(session_id)
+ "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}"
+ end
+
def self.key_name(user_id, session_id = '*')
"#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end
@@ -197,7 +220,7 @@ class ActiveSession
end
def self.rack_session_keys(rack_session_ids)
- rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
+ rack_session_ids.map { |session_id| rack_key_name(session_id)}
end
def self.raw_active_session_entries(redis, session_ids, user_id)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5655ea4d4bf..33c058dab96 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -29,6 +29,21 @@ class ApplicationSetting < ApplicationRecord
@repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze
end
+ def self.kroki_formats_attributes
+ {
+ blockdiag: {
+ label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)'
+ },
+ bpmn: {
+ label: 'BPMN'
+ },
+ excalidraw: {
+ label: 'Excalidraw'
+ }
+ }
+ end
+
+ store_accessor :kroki_formats, *ApplicationSetting.kroki_formats_attributes.keys, prefix: true
store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true
# Include here so it can override methods from
@@ -43,6 +58,8 @@ class ApplicationSetting < ApplicationRecord
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
cache_markdown_field :sign_in_text
@@ -52,6 +69,7 @@ class ApplicationSetting < ApplicationRecord
default_value_for :id, 1
default_value_for :repository_storages_weighted, {}
+ default_value_for :kroki_formats, {}
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
@@ -133,6 +151,8 @@ class ApplicationSetting < ApplicationRecord
validate :validate_kroki_url, if: :kroki_enabled
+ validates :kroki_formats, json_schema: { filename: 'application_setting_kroki_formats' }
+
validates :plantuml_url,
presence: true,
if: :plantuml_enabled
@@ -442,6 +462,13 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :notes_create_limit,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
+ validates :notes_create_limit_allowlist,
+ length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
+ allow_nil: false
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -523,6 +550,10 @@ class ApplicationSetting < ApplicationRecord
current_without_cache
end
+ def self.find_or_create_without_cache
+ current_without_cache || create_from_defaults
+ end
+
# Due to the frequency with which settings are accessed, it is
# likely that during a backup restore a running GitLab process
# will insert a new `application_settings` row before the
@@ -557,6 +588,25 @@ class ApplicationSetting < ApplicationRecord
end
end
+ kroki_formats_attributes.keys.each do |key|
+ define_method :"kroki_formats_#{key}=" do |value|
+ super(::Gitlab::Utils.to_boolean(value))
+ end
+ end
+
+ def kroki_format_supported?(diagram_type)
+ case diagram_type
+ when 'excalidraw'
+ return kroki_formats_excalidraw
+ when 'bpmn'
+ return kroki_formats_bpmn
+ end
+
+ return kroki_formats_blockdiag if ::Gitlab::Kroki::BLOCKDIAG_FORMATS.include?(diagram_type)
+
+ ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.include?(diagram_type)
+ end
+
private
def parsed_grafana_url
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index b05355f14b4..2911ae6b1c8 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -100,6 +100,8 @@ module ApplicationSettingImplementation
max_import_size: 0,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
+ notes_create_limit: 300,
+ notes_create_limit_allowlist: [],
notify_on_unknown_sign_in: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,
@@ -174,6 +176,7 @@ module ApplicationSettingImplementation
container_registry_expiration_policies_worker_capacity: 0,
kroki_enabled: false,
kroki_url: nil,
+ kroki_formats: { blockdiag: false, bpmn: false, excalidraw: false },
rate_limiting_response_text: nil
}
end
@@ -269,13 +272,21 @@ module ApplicationSettingImplementation
self.protected_paths = strings_to_array(values)
end
- def asset_proxy_whitelist=(values)
+ def notes_create_limit_allowlist_raw
+ array_to_string(self.notes_create_limit_allowlist)
+ end
+
+ def notes_create_limit_allowlist_raw=(values)
+ self.notes_create_limit_allowlist = strings_to_array(values).map(&:downcase)
+ end
+
+ def asset_proxy_allowlist=(values)
values = strings_to_array(values) if values.is_a?(String)
- # make sure we always whitelist the running host
+ # make sure we always allow the running host
values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host)
- self[:asset_proxy_whitelist] = values
+ self[:asset_proxy_allowlist] = values
end
def repository_storages
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index d1c0bb11dc8..32c9d44f836 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -55,15 +55,20 @@ class AuditEvent < ApplicationRecord
end
def author_name
- lazy_author.name
+ author&.name
end
def formatted_details
details.merge(details.slice(:from, :to).transform_values(&:to_s))
end
+ def author
+ lazy_author&.itself.presence ||
+ ::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name]))
+ end
+
def lazy_author
- BatchLoader.for(author_id).batch(default_value: default_author_value, replace_methods: false) do |author_ids, loader|
+ BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader|
User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
loader.call(user.id, user)
end
diff --git a/app/models/board.rb b/app/models/board.rb
index a57d101b30a..85fad762ebe 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -44,6 +44,14 @@ class Board < ApplicationRecord
def scoped?
false
end
+
+ def self.to_type
+ name.demodulize
+ end
+
+ def to_type
+ self.class.to_type
+ end
end
Board.prepend_if_ee('EE::Board')
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index a4d0b7485ba..16224fde502 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -43,6 +43,8 @@ class BulkImports::Entity < ApplicationRecord
validate :validate_parent_is_a_group, if: :parent
validate :validate_imported_entity_type
+ validate :validate_destination_namespace_ascendency, if: :group_entity?
+
enum source_type: { group_entity: 0, project_entity: 1 }
state_machine :status, initial: :created do
@@ -107,4 +109,17 @@ class BulkImports::Entity < ApplicationRecord
)
end
end
+
+ def validate_destination_namespace_ascendency
+ source = Group.find_by_full_path(source_full_path)
+
+ return unless source
+
+ if source.self_and_descendants.any? { |namespace| namespace.full_path == destination_namespace }
+ errors.add(
+ :destination_namespace,
+ s_('BulkImport|destination group cannot be part of the source group tree')
+ )
+ end
+ end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index ef3891908f7..ca400cebe4e 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -27,7 +27,7 @@ module Ci
# rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do
- after_transition [:created, :manual] => :pending do |bridge|
+ after_transition [:created, :manual, :waiting_for_resource] => :pending do |bridge|
next unless bridge.downstream_project
bridge.run_after_commit do
@@ -156,6 +156,10 @@ module Ci
false
end
+ def any_unmet_prerequisites?
+ false
+ end
+
def expanded_environment_name
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5e3f42d7c2c..db151126caf 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -20,7 +20,6 @@ module Ci
belongs_to :runner
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
- belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :builds
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
RUNNER_FEATURES = {
@@ -38,7 +37,6 @@ module Ci
DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
has_one :deployment, as: :deployable, class_name: 'Deployment'
- has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
@@ -230,27 +228,20 @@ module Ci
end
def with_preloads
- preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
+ preload(:job_artifacts_archive, :job_artifacts, :tags, project: [:namespace])
end
end
state_machine :status do
event :enqueue do
- transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :requires_resource?
transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites?
end
event :enqueue_scheduled do
- transition scheduled: :waiting_for_resource, if: :requires_resource?
transition scheduled: :preparing, if: :any_unmet_prerequisites?
transition scheduled: :pending
end
- event :enqueue_waiting_for_resource do
- transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
- transition waiting_for_resource: :pending
- end
-
event :enqueue_preparing do
transition preparing: :pending
end
@@ -279,23 +270,6 @@ module Ci
build.scheduled_at = build.options_scheduled_at
end
- before_transition any => :waiting_for_resource do |build|
- build.waiting_for_resource_at = Time.current
- end
-
- before_transition on: :enqueue_waiting_for_resource do |build|
- next unless build.requires_resource?
-
- build.resource_group.assign_resource_to(build) # If false is returned, it stops the transition
- end
-
- after_transition any => :waiting_for_resource do |build|
- build.run_after_commit do
- Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
- .perform_async(build.resource_group_id)
- end
- end
-
before_transition on: :enqueue_preparing do |build|
!build.any_unmet_prerequisites? # If false is returned, it stops the transition
end
@@ -328,16 +302,6 @@ module Ci
end
end
- after_transition any => ::Ci::Build.completed_statuses do |build|
- next unless build.resource_group_id.present?
- next unless build.resource_group.release_resource_from(build)
-
- build.run_after_commit do
- Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
- .perform_async(build.resource_group_id)
- end
- end
-
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
build.run_status_commit_hooks!
@@ -403,7 +367,7 @@ module Ci
def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory
- .new(self, current_user)
+ .new(self.present, current_user)
.fabricate!
end
@@ -467,6 +431,11 @@ module Ci
pipeline.builds.retried.where(name: self.name).count
end
+ override :all_met_to_become_pending?
+ def all_met_to_become_pending?
+ super && !any_unmet_prerequisites?
+ end
+
def any_unmet_prerequisites?
prerequisites.present?
end
@@ -501,10 +470,6 @@ module Ci
end
end
- def requires_resource?
- self.resource_group_id.present?
- end
-
def has_environment?
environment.present?
end
@@ -821,7 +786,9 @@ module Ci
end
def artifacts_file_for_type(type)
- job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
+ file_types = Ci::JobArtifact.associated_file_types_for(type)
+ file_types_ids = file_types&.map { |file_type| Ci::JobArtifact.file_types[file_type] }
+ job_artifacts.find_by(file_type: file_types_ids)&.file
end
def coverage_regex
@@ -941,19 +908,12 @@ module Ci
end
def collect_coverage_reports!(coverage_report)
- project_path, worktree_paths = if Feature.enabled?(:smart_cobertura_parser, project)
- # If the flag is disabled, we intentionally pass nil
- # for both project_path and worktree_paths to fallback
- # to the non-smart behavior of the parser
- [project.full_path, pipeline.all_worktree_paths]
- end
-
each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
blob,
coverage_report,
- project_path: project_path,
- worktree_paths: worktree_paths
+ project_path: project.full_path,
+ worktree_paths: pipeline.all_worktree_paths
)
end
@@ -1122,7 +1082,6 @@ module Ci
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)
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index a6abeb517c1..b50ecf99439 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -103,7 +103,7 @@ module Ci
end
def valid_local?
- return true if Feature.enabled?(:ci_disable_validates_dependencies)
+ return true unless Gitlab::Ci::Features.validate_build_dependencies?(project)
local.all?(&:valid_dependency?)
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index ceefb6a8b8a..d4f9f78a1ac 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -77,6 +77,22 @@ module Ci
end
##
+ # Sometime we need to ensure that the first read goes to a primary
+ # database, what is especially important in EE. This method does not
+ # change the behavior in CE.
+ #
+ def with_read_consistency(build, &block)
+ return yield unless consistent_reads_enabled?(build)
+
+ ::Gitlab::Database::Consistency
+ .with_read_consistency(&block)
+ end
+
+ def consistent_reads_enabled?(build)
+ Feature.enabled?(:gitlab_ci_trace_read_consistency, build.project, type: :development, default_enabled: true)
+ end
+
+ ##
# Sometimes we do not want to read raw data. This method makes it easier
# to find attributes that are just metadata excluding raw data.
#
@@ -154,8 +170,8 @@ module Ci
in_lock(lock_key, **lock_params) do # exclusive Redis lock is acquired first
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
- self.reset.then do |chunk| # we ensure having latest lock_version
- chunk.unsafe_persist_data! # we migrate the data and update data store
+ self.class.with_read_consistency(build) do
+ self.reset.then { |chunk| chunk.unsafe_persist_data! }
end
end
rescue FailedToObtainLockError
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
index 27b579bf428..cbf0c0a1696 100644
--- a/app/models/ci/build_trace_chunks/fog.rb
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -14,15 +14,7 @@ module Ci
end
def set_data(model, new_data)
- if Feature.enabled?(:ci_live_trace_use_fog_attributes, default_enabled: true)
- files.create(create_attributes(model, new_data))
- else
- # TODO: Support AWS S3 server side encryption
- files.create({
- key: key(model),
- body: new_data
- })
- end
+ files.create(create_attributes(model, new_data))
end
def append_data(model, new_data, offset)
diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb
index 8be42eb48d6..5091e3ff04a 100644
--- a/app/models/ci/build_trace_section.rb
+++ b/app/models/ci/build_trace_section.rb
@@ -2,6 +2,7 @@
module Ci
class BuildTraceSection < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
extend Gitlab::Ci::Model
belongs_to :build, class_name: 'Ci::Build'
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index e9f3366b939..23c96e63724 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -9,14 +9,19 @@ module Ci
belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
belongs_to :project
+ belongs_to :group
validates :data, json_schema: { filename: "daily_build_group_report_result_data" }
scope :with_included_projects, -> { includes(:project) }
+ scope :by_ref_path, -> (ref_path) { where(ref_path: ref_path) }
scope :by_projects, -> (ids) { where(project_id: ids) }
+ scope :by_group, -> (group_id) { where(group_id: group_id) }
scope :with_coverage, -> { where("(data->'coverage') IS NOT NULL") }
scope :with_default_branch, -> { where(default_branch: true) }
scope :by_date, -> (start_date) { where(date: report_window(start_date)..Date.current) }
+ scope :by_dates, -> (start_date, end_date) { where(date: start_date..end_date) }
+ scope :ordered_by_date_and_group_name, -> { order(date: :desc, group_name: :asc) }
store_accessor :data, :coverage
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index f13be3b3c86..f927111758a 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -19,6 +19,8 @@ module Ci
NON_ERASABLE_FILE_TYPES = %w[trace].freeze
TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze
UNSUPPORTED_FILE_TYPES = %i[license_management].freeze
+ SAST_REPORT_TYPES = %w[sast].freeze
+ SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze
DEFAULT_FILE_NAMES = {
archive: nil,
metadata: nil,
@@ -150,6 +152,14 @@ module Ci
with_file_types(REPORT_TYPES.keys.map(&:to_s))
end
+ scope :sast_reports, -> do
+ with_file_types(SAST_REPORT_TYPES)
+ end
+
+ scope :secret_detection_reports, -> do
+ with_file_types(SECRET_DETECTION_REPORT_TYPES)
+ end
+
scope :test_reports, -> do
with_file_types(TEST_REPORT_FILE_TYPES)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 88c7002b1b6..3be107ea2e1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -251,6 +251,7 @@ module Ci
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
pipeline.run_after_commit do
::Ci::PipelineArtifacts::CoverageReportWorker.perform_async(pipeline.id)
+ ::Ci::PipelineArtifacts::CreateQualityReportWorker.perform_async(pipeline.id)
end
end
@@ -263,8 +264,6 @@ module Ci
end
after_transition any => any do |pipeline|
- next unless Feature.enabled?(:jira_sync_builds, pipeline.project)
-
pipeline.run_after_commit do
# Passing the seq-id ensures this is idempotent
seq_id = ::Atlassian::JiraConnect::Client.generate_update_sequence_id
@@ -678,7 +677,7 @@ module Ci
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
- ::Ci::Build.where(commit_id: pipeline_ids)
+ ::CommitStatus.where(commit_id: pipeline_ids)
.latest
.failed_but_allowed
.group(:commit_id)
@@ -805,7 +804,7 @@ module Ci
variables.concat(merge_request.predefined_variables)
end
- if Gitlab::Ci::Features.pipeline_open_merge_requests?(project) && open_merge_requests_refs.any?
+ if open_merge_requests_refs.any?
variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(','))
end
@@ -962,7 +961,7 @@ module Ci
def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory
- .new(self, current_user)
+ .new(self.present, current_user)
.fabricate!
end
@@ -998,13 +997,23 @@ module Ci
end
def has_coverage_reports?
- pipeline_artifacts&.has_code_coverage?
+ pipeline_artifacts&.report_exists?(:code_coverage)
end
def can_generate_coverage_reports?
has_reports?(Ci::JobArtifact.coverage_reports)
end
+ def has_codequality_mr_diff_report?
+ pipeline_artifacts&.report_exists?(:code_quality_mr_diff)
+ end
+
+ def can_generate_codequality_reports?
+ return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project)
+
+ has_reports?(Ci::JobArtifact.codequality_reports)
+ end
+
def test_report_summary
strong_memoize(:test_report_summary) do
Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
@@ -1206,6 +1215,21 @@ module Ci
end
# rubocop:enable Rails/FindEach
+ # EE-only
+ def merge_train_pipeline?
+ false
+ end
+
+ def security_reports(report_types: [])
+ reports_scope = report_types.empty? ? ::Ci::JobArtifact.security_reports : ::Ci::JobArtifact.security_reports(file_types: report_types)
+
+ ::Gitlab::Ci::Reports::Security::Reports.new(self).tap do |security_reports|
+ latest_report_builds(reports_scope).each do |build|
+ build.collect_security_reports!(security_reports)
+ end
+ end
+ end
+
private
def add_message(severity, content)
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index b6db8cad667..f538a4cd808 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -14,7 +14,13 @@ module Ci
EXPIRATION_DATE = 1.week.freeze
DEFAULT_FILE_NAMES = {
- code_coverage: 'code_coverage.json'
+ code_coverage: 'code_coverage.json',
+ code_quality_mr_diff: 'code_quality_mr_diff.json'
+ }.freeze
+
+ REPORT_TYPES = {
+ code_coverage: :raw,
+ code_quality_mr_diff: :raw
}.freeze
belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts
@@ -30,15 +36,20 @@ module Ci
update_project_statistics project_statistics_name: :pipeline_artifacts_size
enum file_type: {
- code_coverage: 1
+ code_coverage: 1,
+ code_quality_mr_diff: 2
}
- def self.has_code_coverage?
- where(file_type: :code_coverage).exists?
- end
+ class << self
+ def report_exists?(file_type)
+ return false unless REPORT_TYPES.key?(file_type)
+
+ where(file_type: file_type).exists?
+ end
- def self.find_with_code_coverage
- find_by(file_type: :code_coverage)
+ def find_by_file_type(file_type)
+ find_by(file_type: file_type)
+ end
end
def present
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 8c9ad343f32..2fae077dd87 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -21,7 +21,7 @@ module Ci
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :description, presence: true
- validates :variables, variable_duplicates: true
+ validates :variables, nested_attributes_duplicates: true
strip_attributes :cron
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 6aaf6ac530b..fae65ed0632 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -3,6 +3,11 @@
module Ci
class Processable < ::CommitStatus
include Gitlab::Utils::StrongMemoize
+ extend ::Gitlab::Utils::Override
+
+ has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
+
+ belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
accepts_nested_attributes_for :needs
@@ -20,6 +25,48 @@ module Ci
where('NOT EXISTS (?)', needs)
end
+ state_machine :status do
+ event :enqueue do
+ transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :with_resource_group?
+ end
+
+ event :enqueue_scheduled do
+ transition scheduled: :waiting_for_resource, if: :with_resource_group?
+ end
+
+ event :enqueue_waiting_for_resource do
+ transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
+ transition waiting_for_resource: :pending
+ end
+
+ before_transition any => :waiting_for_resource do |processable|
+ processable.waiting_for_resource_at = Time.current
+ end
+
+ before_transition on: :enqueue_waiting_for_resource do |processable|
+ next unless processable.with_resource_group?
+
+ processable.resource_group.assign_resource_to(processable)
+ end
+
+ after_transition any => :waiting_for_resource do |processable|
+ processable.run_after_commit do
+ Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
+ .perform_async(processable.resource_group_id)
+ end
+ end
+
+ after_transition any => ::Ci::Processable.completed_statuses do |processable|
+ next unless processable.with_resource_group?
+ next unless processable.resource_group.release_resource_from(processable)
+
+ processable.run_after_commit do
+ Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
+ .perform_async(processable.resource_group_id)
+ end
+ end
+ end
+
def self.select_with_aggregated_needs(project)
aggregated_needs_names = Ci::BuildNeed
.scoped_build
@@ -77,6 +124,15 @@ module Ci
raise NotImplementedError
end
+ override :all_met_to_become_pending?
+ def all_met_to_become_pending?
+ super && !with_resource_group?
+ end
+
+ def with_resource_group?
+ self.resource_group_id.present?
+ end
+
# Overriding scheduling_type enum's method for nil `scheduling_type`s
def scheduling_type_dag?
scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super
diff --git a/app/models/ci/resource.rb b/app/models/ci/resource.rb
index ee5b6546165..e0e1fab642d 100644
--- a/app/models/ci/resource.rb
+++ b/app/models/ci/resource.rb
@@ -5,9 +5,9 @@ module Ci
extend Gitlab::Ci::Model
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources
- belongs_to :build, class_name: 'Ci::Build', inverse_of: :resource
+ belongs_to :processable, class_name: 'Ci::Processable', foreign_key: 'build_id', inverse_of: :resource
- scope :free, -> { where(build: nil) }
- scope :retained_by, -> (build) { where(build: build) }
+ scope :free, -> { where(processable: nil) }
+ scope :retained_by, -> (processable) { where(processable: processable) }
end
end
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index eb18f3da0bf..85fbe03e1c9 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -7,7 +7,7 @@ module Ci
belongs_to :project, inverse_of: :resource_groups
has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group
- has_many :builds, class_name: 'Ci::Build', inverse_of: :resource_group
+ has_many :processables, class_name: 'Ci::Processable', inverse_of: :resource_group
validates :key,
length: { maximum: 255 },
@@ -19,12 +19,12 @@ module Ci
##
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking.
- def assign_resource_to(build)
- resources.free.limit(1).update_all(build_id: build.id) > 0
+ def assign_resource_to(processable)
+ resources.free.limit(1).update_all(build_id: processable.id) > 0
end
- def release_resource_from(build)
- resources.retained_by(build).update_all(build_id: nil) > 0
+ def release_resource_from(processable)
+ resources.retained_by(processable).update_all(build_id: nil) > 0
end
private
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index cc6bd1870b9..ae80692d598 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -118,7 +118,7 @@ module Ci
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader|
- ::Ci::Build.where(stage_id: stage_ids)
+ ::CommitStatus.where(stage_id: stage_ids)
.latest
.failed_but_allowed
.group(:stage_id)
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index c58a3bab1a9..c5b9dddb1da 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -4,6 +4,7 @@ module Clusters
class Agent < ApplicationRecord
self.table_name = 'cluster_agents'
+ belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index 5c9561ffa98..b260822f784 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -8,6 +8,7 @@ module Clusters
self.table_name = 'cluster_agent_tokens'
belongs_to :agent, class_name: 'Clusters::Agent'
+ belongs_to :created_by_user, class_name: 'User', optional: true
before_save :ensure_token
end
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 8560826928a..2a051233de2 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -2,6 +2,8 @@
module Clusters
module Applications
+ # DEPRECATED for removal in %14.0
+ # See https://gitlab.com/groups/gitlab-org/-/epics/4280
class CertManager < ApplicationRecord
VERSION = 'v0.10.1'
CRD_VERSION = '0.10'
diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb
index 2b1a86706a4..07378b4e8dc 100644
--- a/app/models/clusters/applications/crossplane.rb
+++ b/app/models/clusters/applications/crossplane.rb
@@ -2,6 +2,8 @@
module Clusters
module Applications
+ # DEPRECATED for removal in %14.0
+ # See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Crossplane < ApplicationRecord
VERSION = '0.4.1'
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 36324e7f3e0..e7d4d737b8e 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -2,6 +2,8 @@
module Clusters
module Applications
+ # DEPRECATED for removal in %14.0
+ # See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Ingress < ApplicationRecord
VERSION = '1.40.2'
INGRESS_CONTAINER_NAME = 'nginx-ingress-controller'
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index ff907c6847f..8d7d9c20bfa 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -4,6 +4,8 @@ require 'securerandom'
module Clusters
module Applications
+ # DEPRECATED for removal in %14.0
+ # See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Jupyter < ApplicationRecord
VERSION = '0.9.0'
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 7c131e031c1..6867d7b6934 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -2,6 +2,8 @@
module Clusters
module Applications
+ # DEPRECATED for removal in %14.0
+ # See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Knative < ApplicationRecord
VERSION = '0.10.0'
REPOSITORY = 'https://charts.gitlab.io'
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 56acac53e0b..f87eccecf9f 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.24.0'
+ VERSION = '0.25.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index e3dcd5b0d07..da5f4cc1862 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -7,6 +7,7 @@ module Clusters
include EnumWithNil
include AfterCommitQueue
include ReactiveCaching
+ include NullifyIfBlank
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
@@ -25,7 +26,6 @@ module Clusters
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
- before_validation :nullify_blank_namespace
before_validation :enforce_namespace_to_lower_case
before_validation :enforce_ca_whitespace_trimming
@@ -64,6 +64,8 @@ module Clusters
default_value_for :authorization_type, :rbac
+ nullify_if_blank :namespace
+
def predefined_variables(project:, environment_name:, kubernetes_namespace: nil)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'KUBE_URL', value: api_url)
@@ -255,10 +257,6 @@ module Clusters
true
end
- def nullify_blank_namespace
- self.namespace = nil if namespace.blank?
- end
-
def extract_relevant_pod_data(pods)
pods.map do |pod|
{
diff --git a/app/models/commit.rb b/app/models/commit.rb
index edce9ad293e..bf168aaacc5 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -36,7 +36,7 @@ class Commit
LINK_EXTENSION_PATTERN = /(patch)/.freeze
cache_markdown_field :title, pipeline: :single_line
- cache_markdown_field :full_title, pipeline: :single_line
+ cache_markdown_field :full_title, pipeline: :single_line, limit: 1.kilobyte
cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte
class << self
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c2aecc524d4..ea2f425c5f6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -209,14 +209,26 @@ class CommitStatus < ApplicationRecord
end
def group_name
- # 'rspec:linux: 1/10' => 'rspec:linux'
- common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
+ simplified_commit_status_group_name_feature_flag = Gitlab::SafeRequestStore.fetch("project:#{project_id}:simplified_commit_status_group_name") do
+ Feature.enabled?(:simplified_commit_status_group_name, project, default_enabled: false)
+ end
+
+ if simplified_commit_status_group_name_feature_flag
+ # Only remove one or more [...] "X/Y" "X Y" from the end of build names.
+ # More about the regular expression logic: https://docs.gitlab.com/ee/ci/jobs/#group-jobs-in-a-pipeline
+
+ name.to_s.sub(%r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z}, '').strip
+ else
+ # Prior implementation, remove [...] "X/Y" "X Y" from the beginning and middle of build names
+ # 'rspec:linux: 1/10' => 'rspec:linux'
+ 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}, '')
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
+ common_name.gsub!(%r{: \[.*\]\s*\z}, '')
- common_name.strip!
- common_name
+ common_name.strip!
+ common_name
+ end
end
def failed_but_allowed?
@@ -256,15 +268,7 @@ class CommitStatus < ApplicationRecord
end
def all_met_to_become_pending?
- !any_unmet_prerequisites? && !requires_resource?
- end
-
- def any_unmet_prerequisites?
- false
- end
-
- def requires_resource?
- false
+ true
end
def auto_canceled?
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index f1c39dda49d..080ff07ec0c 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -49,6 +49,14 @@ module Analytics
end
end
+ def start_event_identifier
+ backward_compatible_identifier(:start_event_identifier) || super
+ end
+
+ def end_event_identifier
+ backward_compatible_identifier(:end_event_identifier) || super
+ end
+
def start_event_label_based?
start_event_identifier && start_event.label_based?
end
@@ -128,6 +136,17 @@ module Analytics
.id_in(label_id)
.exists?
end
+
+ # Temporary, will be removed in 13.10
+ def backward_compatible_identifier(attribute_name)
+ removed_identifier = 6 # References IssueFirstMentionedInCommit removed on https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51975
+ replacement_identifier = :issue_first_mentioned_in_commit
+
+ # ActiveRecord returns nil if the column value is not part of the Enum definition
+ if self[attribute_name].nil? && read_attribute_before_type_cast(attribute_name) == removed_identifier
+ replacement_identifier
+ end
+ end
end
end
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index baa99fa5a7f..bbf9ecbcfe9 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -26,20 +26,31 @@
module AtomicInternalId
extend ActiveSupport::Concern
+ MissingValueError = Class.new(StandardError)
+
class_methods do
def has_internal_id( # rubocop:disable Naming/PredicateName
- column, scope:, init: :not_given, ensure_if: nil, track_if: nil,
- presence: true, backfill: false, hook_names: :create)
+ column, scope:, init: :not_given, ensure_if: nil, track_if: nil, presence: true, hook_names: :create)
raise "has_internal_id init must not be nil if given." if init.nil?
raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope)
init = infer_init(scope) if init == :not_given
- before_validation :"track_#{scope}_#{column}!", on: hook_names, if: track_if
- before_validation :"ensure_#{scope}_#{column}!", on: hook_names, if: ensure_if
- validates column, presence: presence
+ callback_names = Array.wrap(hook_names).map { |hook_name| :"before_#{hook_name}" }
+ callback_names.each do |callback_name|
+ # rubocop:disable GitlabSecurity/PublicSend
+ public_send(callback_name, :"track_#{scope}_#{column}!", if: track_if)
+ public_send(callback_name, :"ensure_#{scope}_#{column}!", if: ensure_if)
+ # rubocop:enable GitlabSecurity/PublicSend
+ end
+ after_rollback :"clear_#{scope}_#{column}!", on: hook_names, if: ensure_if
+
+ if presence
+ before_create :"validate_#{column}_exists!"
+ before_update :"validate_#{column}_exists!"
+ end
define_singleton_internal_id_methods(scope, column, init)
- define_instance_internal_id_methods(scope, column, init, backfill)
+ define_instance_internal_id_methods(scope, column, init)
end
private
@@ -62,10 +73,8 @@ module AtomicInternalId
# - track_{scope}_{column}!
# - reset_{scope}_{column}
# - {column}=
- def define_instance_internal_id_methods(scope, column, init, backfill)
+ def define_instance_internal_id_methods(scope, column, init)
define_method("ensure_#{scope}_#{column}!") do
- return if backfill && self.class.where(column => nil).exists?
-
scope_value = internal_id_read_scope(scope)
value = read_attribute(column)
return value unless scope_value
@@ -79,6 +88,8 @@ module AtomicInternalId
internal_id_scope_usage,
init)
write_attribute(column, value)
+
+ @internal_id_set_manually = false
end
value
@@ -110,6 +121,7 @@ module AtomicInternalId
super(value).tap do |v|
# Indicate the iid was set from externally
@internal_id_needs_tracking = true
+ @internal_id_set_manually = true
end
end
@@ -128,6 +140,20 @@ module AtomicInternalId
read_attribute(column)
end
+
+ define_method("clear_#{scope}_#{column}!") do
+ return if @internal_id_set_manually
+
+ return unless public_send(:"#{column}_previously_changed?") # rubocop:disable GitlabSecurity/PublicSend
+
+ write_attribute(column, nil)
+ end
+
+ define_method("validate_#{column}_exists!") do
+ value = read_attribute(column)
+
+ raise MissingValueError, "#{column} was unexpectedly blank!" if value.blank?
+ end
end
# Defines class methods:
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index de176ffde5c..ee56322cce7 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -83,6 +83,6 @@ module CacheableAttributes
end
def cache!
- self.class.cache_backend.write(self.class.cache_key, self, expires_in: 1.minute)
+ self.class.cache_backend.write(self.class.cache_key, self, expires_in: Gitlab.config.gitlab['application_settings_cache_seconds'] || 60)
end
end
diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb
index 52c3a4106e3..1132e4e79ac 100644
--- a/app/models/concerns/can_move_repository_storage.rb
+++ b/app/models/concerns/can_move_repository_storage.rb
@@ -16,10 +16,10 @@ module CanMoveRepositoryStorage
!skip_git_transfer_check && git_transfer_in_progress?
raise RepositoryReadOnlyError, _('Repository already read-only') if
- self.class.where(id: id).pick(:repository_read_only)
+ _safe_read_repository_read_only_column
raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- update_column(:repository_read_only, true)
+ _update_repository_read_only_column(true)
nil
end
@@ -30,7 +30,7 @@ module CanMoveRepositoryStorage
def set_repository_writable!
with_lock do
raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- update_column(:repository_read_only, false)
+ _update_repository_read_only_column(false)
nil
end
@@ -43,4 +43,19 @@ module CanMoveRepositoryStorage
def reference_counter(type:)
Gitlab::ReferenceCounter.new(type.identifier_for_container(self))
end
+
+ private
+
+ # Not all resources that can move repositories have the `repository_read_only`
+ # in their table, for example groups. We need these methods to override the
+ # behavior in those classes in order to access the column.
+ def _safe_read_repository_read_only_column
+ # This was added originally this way because of
+ # https://gitlab.com/gitlab-org/gitlab/-/commit/43f9b98302d3985312c9f8b66018e2835d8293d2
+ self.class.where(id: id).pick(:repository_read_only)
+ end
+
+ def _update_repository_read_only_column(value)
+ update_column(:repository_read_only, value)
+ end
end
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index e1f07fa162c..f8314d8b429 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -10,6 +10,9 @@ module Enums
unknown_failure: 0,
config_error: 1,
external_validation_failure: 2,
+ activity_limit_exceeded: 20,
+ size_limit_exceeded: 21,
+ job_activity_limit_exceeded: 22,
deployments_limit_exceeded: 23
}
end
@@ -71,11 +74,10 @@ module Enums
remote_source: 4,
external_project_source: 5,
bridge_source: 6,
- parameter_source: 7
+ parameter_source: 7,
+ compliance_source: 8
}
end
end
end
end
-
-Enums::Ci::Pipeline.prepend_if_ee('EE::Enums::Ci::Pipeline')
diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb
index 20b72957ec2..ed9bce87da1 100644
--- a/app/models/concerns/featurable.rb
+++ b/app/models/concerns/featurable.rb
@@ -88,9 +88,6 @@ module Featurable
end
def feature_available?(feature, user)
- # This feature might not be behind a feature flag at all, so default to true
- return false unless ::Feature.enabled?(feature, user, default_enabled: true)
-
get_permission(user, feature)
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 9692941d8b2..b9ad78c14fd 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -15,15 +15,6 @@ module HasRepository
delegate :base_dir, :disk_path, to: :storage
- class_methods do
- def pick_repository_storage
- # We need to ensure application settings are fresh when we pick
- # a repository storage to use.
- Gitlab::CurrentSettings.expire_current_application_settings
- Gitlab::CurrentSettings.pick_repository_storage
- end
- end
-
def valid_repo?
repository.exists?
rescue
diff --git a/app/models/concerns/nullify_if_blank.rb b/app/models/concerns/nullify_if_blank.rb
new file mode 100644
index 00000000000..5a5cc51509b
--- /dev/null
+++ b/app/models/concerns/nullify_if_blank.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+# Helper that sets attributes to nil prior to validation if they
+# are blank (are false, empty or contain only whitespace), to avoid
+# unnecessarily persisting empty strings.
+#
+# Model usage:
+#
+# class User < ApplicationRecord
+# include NullifyIfBlank
+#
+# nullify_if_blank :name, :email
+# end
+#
+#
+# Test usage:
+#
+# RSpec.describe User do
+# it { is_expected.to nullify_if_blank(:name) }
+# it { is_expected.to nullify_if_blank(:email) }
+# end
+#
+module NullifyIfBlank
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def nullify_if_blank(*attributes)
+ self.attributes_to_nullify += attributes
+ end
+ end
+
+ included do
+ class_attribute :attributes_to_nullify,
+ instance_accessor: false,
+ instance_predicate: false,
+ default: Set.new
+
+ before_validation :nullify_blank_attributes
+ end
+
+ private
+
+ def nullify_blank_attributes
+ self.class.attributes_to_nullify.each do |attribute|
+ assign_attributes(attribute => nil) if read_attribute(attribute).blank?
+ end
+ end
+end
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
index 82055822cfb..c7af841e450 100644
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ b/app/models/concerns/optimized_issuable_label_filter.rb
@@ -13,7 +13,7 @@ module OptimizedIssuableLabelFilter
def by_label(items)
return items unless params.labels?
- return super if Feature.disabled?(:optimized_issuable_label_filter)
+ return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
target_model = items.model
@@ -29,7 +29,7 @@ module OptimizedIssuableLabelFilter
# Taken from IssuableFinder
def count_by_state
return super if root_namespace.nil?
- return super if Feature.disabled?(:optimized_issuable_label_filter)
+ return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
count_params = params.merge(state: nil, sort: nil, force_cte: true)
finder = self.class.new(current_user, count_params)
diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb
index 4aa633e0357..760ebb49980 100644
--- a/app/models/concerns/packages/debian/architecture.rb
+++ b/app/models/concerns/packages/debian/architecture.rb
@@ -7,6 +7,12 @@ module Packages
included do
belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures
+ # files must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :files,
+ class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile",
+ foreign_key: :architecture_id,
+ inverse_of: :architecture,
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :distribution,
presence: true
diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb
new file mode 100644
index 00000000000..7b342c7b684
--- /dev/null
+++ b/app/models/concerns/packages/debian/component.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ module Component
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :components
+ # files must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :files,
+ class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile",
+ foreign_key: :component_id,
+ inverse_of: :component,
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ validates :distribution,
+ presence: true
+
+ validates :name,
+ presence: true,
+ length: { maximum: 255 },
+ uniqueness: { scope: %i[distribution_id] },
+ format: { with: Gitlab::Regex.debian_component_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/component_file.rb b/app/models/concerns/packages/debian/component_file.rb
new file mode 100644
index 00000000000..3cc2c291e96
--- /dev/null
+++ b/app/models/concerns/packages/debian/component_file.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ module ComponentFile
+ extend ActiveSupport::Concern
+
+ included do
+ include Sortable
+ include FileStoreMounter
+
+ def self.container_foreign_key
+ "#{container_type}_id".to_sym
+ end
+
+ def self.distribution_class
+ "::Packages::Debian::#{container_type.capitalize}Distribution".constantize
+ end
+
+ belongs_to :component, class_name: "Packages::Debian::#{container_type.capitalize}Component", inverse_of: :files
+ belongs_to :architecture, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", inverse_of: :files, optional: true
+
+ enum file_type: { packages: 1, source: 2, di_packages: 3 }
+ enum compression_type: { gz: 1, bz2: 2, xz: 3 }
+
+ validates :component, presence: true
+ validates :file_type, presence: true
+ validates :architecture, presence: true, unless: :source?
+ validates :architecture, absence: true, if: :source?
+ validates :file, length: { minimum: 0, allow_nil: false }
+ validates :size, presence: true
+ validates :file_store, presence: true
+ validates :file_md5, presence: true
+ validates :file_sha256, presence: true
+
+ scope :with_container, ->(container) do
+ joins(component: :distribution)
+ .where("packages_debian_#{container_type}_distributions" => { container_foreign_key => container.id })
+ end
+
+ scope :with_codename_or_suite, ->(codename_or_suite) do
+ joins(component: :distribution)
+ .merge(distribution_class.with_codename_or_suite(codename_or_suite))
+ end
+
+ scope :with_component_name, ->(component_name) do
+ joins(:component)
+ .where("packages_debian_#{container_type}_components" => { name: component_name })
+ end
+
+ scope :with_file_type, ->(file_type) { where(file_type: file_type) }
+
+ scope :with_architecture_name, ->(architecture_name) do
+ left_outer_joins(:architecture)
+ .where("packages_debian_#{container_type}_architectures" => { name: architecture_name })
+ end
+
+ scope :with_compression_type, ->(compression_type) { where(compression_type: compression_type) }
+ scope :with_file_sha256, ->(file_sha256) { where(file_sha256: file_sha256) }
+
+ scope :preload_distribution, -> { includes(component: :distribution) }
+
+ mount_file_store_uploader Packages::Debian::ComponentFileUploader
+
+ before_validation :update_size_from_file
+
+ def file_name
+ case file_type
+ when 'di_packages'
+ 'Packages'
+ else
+ file_type.capitalize
+ end
+ end
+
+ def relative_path
+ case file_type
+ when 'packages'
+ "#{component.name}/binary-#{architecture.name}/#{file_name}#{extension}"
+ when 'source'
+ "#{component.name}/source/#{file_name}#{extension}"
+ when 'di_packages'
+ "#{component.name}/debian-installer/binary-#{architecture.name}/#{file_name}#{extension}"
+ end
+ end
+
+ private
+
+ def extension
+ return '' unless compression_type
+
+ ".#{compression_type}"
+ end
+
+ def update_size_from_file
+ self.size ||= file.size
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 285d293c9ee..08fb9ccf3ea 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -18,6 +18,16 @@ module Packages
belongs_to container_type
belongs_to :creator, class_name: 'User'
+ # component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :components,
+ class_name: "Packages::Debian::#{container_type.capitalize}Component",
+ foreign_key: :distribution_id,
+ inverse_of: :distribution,
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :component_files,
+ through: :components,
+ source: :files,
+ class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile"
has_many :architectures,
class_name: "Packages::Debian::#{container_type.capitalize}Architecture",
foreign_key: :distribution_id,
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 65195a8d5aa..cf23a27244c 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -40,20 +40,26 @@ module ProtectedRef
end
def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
- access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
+ all_matching_rules_allow?(ref, action: action, protected_refs: protected_refs) do |access_level|
access_level.check_access(user)
end
end
def developers_can?(action, ref, protected_refs: nil)
- access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
+ all_matching_rules_allow?(ref, action: action, protected_refs: protected_refs) do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER
end
end
- def access_levels_for_ref(ref, action:, protected_refs: nil)
- self.matching(ref, protected_refs: protected_refs)
- .flat_map(&:"#{action}_access_levels")
+ def all_matching_rules_allow?(ref, action:, protected_refs: nil, &block)
+ access_levels_groups =
+ self.matching(ref, protected_refs: protected_refs).map(&:"#{action}_access_levels")
+
+ return false if access_levels_groups.blank?
+
+ access_levels_groups.all? do |access_levels|
+ access_levels.any?(&block)
+ end
end
# Returns all protected refs that match the given ref name.
diff --git a/app/models/concerns/repositories/can_housekeep_repository.rb b/app/models/concerns/repositories/can_housekeep_repository.rb
index 2b79851a07c..946f82c5f36 100644
--- a/app/models/concerns/repositories/can_housekeep_repository.rb
+++ b/app/models/concerns/repositories/can_housekeep_repository.rb
@@ -16,6 +16,10 @@ module Repositories
Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
end
+ def git_garbage_collect_worker_klass
+ raise NotImplementedError
+ end
+
private
def pushes_since_gc_redis_shared_state_key
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index a45b4626628..8607f0d94f4 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -20,7 +20,7 @@ module RepositoryStorageMovable
validate :container_repository_writable, on: :create
default_value_for(:destination_storage_name, allows_nil: false) do
- pick_repository_storage
+ Repository.pick_storage_shard
end
state_machine initial: :initial do
@@ -68,6 +68,18 @@ module RepositoryStorageMovable
storage_move.update_repository_storage(storage_move.destination_storage_name)
end
+ after_transition started: :replicated do |storage_move|
+ # We have several scripts in place that replicate some statistics information
+ # to other databases. Some of them depend on the updated_at column
+ # to identify the models they need to extract.
+ #
+ # If we don't update the `updated_at` of the container after a repository storage move,
+ # the scripts won't know that they need to sync them.
+ #
+ # See https://gitlab.com/gitlab-data/analytics/-/issues/7868
+ storage_move.container.touch
+ end
+
before_transition started: :failed do |storage_move|
storage_move.container.set_repository_writable!
end
@@ -82,16 +94,6 @@ module RepositoryStorageMovable
end
end
- class_methods do
- private
-
- def pick_repository_storage
- container_klass = reflect_on_association(:container).class_name.constantize
-
- container_klass.pick_repository_storage
- end
- end
-
# Projects, snippets, and group wikis has different db structure. In projects,
# we need to update some columns in this step, but we don't with the other resources.
#
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 9cd1a22b203..2daea388939 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -45,6 +45,17 @@ module Spammable
self.needs_recaptcha = true
end
+ ##
+ # Indicates if a recaptcha should be rendered before allowing this model to be saved.
+ #
+ def render_recaptcha?
+ return false unless Gitlab::Recaptcha.enabled?
+
+ return false if self.errors.count > 1 # captcha should not be rendered if are still other errors
+
+ self.needs_recaptcha?
+ end
+
def spam!
self.spam = true
end
diff --git a/app/models/concerns/suppress_composite_primary_key_warning.rb b/app/models/concerns/suppress_composite_primary_key_warning.rb
new file mode 100644
index 00000000000..32634e7bc72
--- /dev/null
+++ b/app/models/concerns/suppress_composite_primary_key_warning.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# When extended, silences this warning below:
+# WARNING: Active Record does not support composite primary key.
+#
+# project_authorizations has composite primary key. Composite primary key is ignored.
+#
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/292909
+module SuppressCompositePrimaryKeyWarning
+ extend ActiveSupport::Concern
+
+ private
+
+ def suppress_composite_primary_key(pk)
+ silence_warnings do
+ super
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
index 4728cb658dc..672402ee4d6 100644
--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
@@ -85,10 +85,18 @@ module TokenAuthenticatableStrategies
end
def find_by_encrypted_token(token, unscoped)
- encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
+ encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce)
+
relation(unscoped).find_by(encrypted_field => encrypted_value)
end
+ def find_hashed_iv(token)
+ token_record = TokenWithIv.find_by_plaintext_token(token)
+
+ token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
+ end
+
def insecure_strategy
@insecure_strategy ||= TokenAuthenticatableStrategies::Insecure
.new(klass, token_field, options)
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index 473b430bb04..db5df6c2c9f 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -16,7 +16,8 @@ module TriggerableHooks
deployment_hooks: :deployment_events,
feature_flag_hooks: :feature_flag_events,
release_hooks: :releases_events,
- member_hooks: :member_events
+ member_hooks: :member_events,
+ subgroup_hooks: :subgroup_events
}.freeze
extend ActiveSupport::Concern
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 0d7ce966537..e2bdf8ffce2 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -4,6 +4,7 @@ class ContainerRepository < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Gitlab::SQL::Pattern
include EachBatch
+ include Sortable
WAITING_CLEANUP_STATUSES = %i[cleanup_scheduled cleanup_unfinished].freeze
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 4f8f86965d7..f4c914c6a3a 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class CustomEmoji < ApplicationRecord
+ NAME_REGEXP = /[a-z0-9_-]+/.freeze
+
belongs_to :namespace, inverse_of: :custom_emoji
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
@@ -17,7 +19,12 @@ class CustomEmoji < ApplicationRecord
uniqueness: { scope: [:namespace_id, :name] },
presence: true,
length: { maximum: 36 },
- format: { with: /\A[a-z0-9][a-z0-9\-_]*[a-z0-9]\z/ }
+
+ format: { with: /\A#{NAME_REGEXP}\z/ }
+
+ scope :by_name, -> (names) { where(name: names) }
+
+ alias_attribute :url, :file # this might need a change in https://gitlab.com/gitlab-org/gitlab/-/issues/230467
private
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 6f40466394a..f000e474605 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -38,6 +38,7 @@ class Deployment < ApplicationRecord
scope :for_status, -> (status) { where(status: status) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
+ scope :for_projects, -> (projects) { where(project: projects) }
scope :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
@@ -45,11 +46,8 @@ class Deployment < ApplicationRecord
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) }
- scope :finished_between, -> (start_date, end_date = nil) do
- selected = where('deployments.finished_at >= ?', start_date)
- selected = selected.where('deployments.finished_at < ?', end_date) if end_date
- selected
- end
+ scope :finished_after, ->(date) { where('finished_at >= ?', date) }
+ scope :finished_before, ->(date) { where('finished_at < ?', date) }
FINISHED_STATUSES = %i[success failed canceled].freeze
@@ -112,7 +110,6 @@ class Deployment < ApplicationRecord
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)
@@ -121,8 +118,6 @@ class Deployment < ApplicationRecord
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
@@ -353,6 +348,13 @@ class Deployment < ApplicationRecord
File.join(environment.ref_path, 'deployments', iid.to_s)
end
+ def equal_to?(params)
+ ref == params[:ref] &&
+ tag == params[:tag] &&
+ sha == params[:sha] &&
+ status == params[:status]
+ end
+
private
def legacy_finished_at
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
index 64a578e16bf..7949bd81605 100644
--- a/app/models/deployment_merge_request.rb
+++ b/app/models/deployment_merge_request.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class DeploymentMergeRequest < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
belongs_to :deployment, optional: false
belongs_to :merge_request, optional: false
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index f5e52c04944..e2d10cc7e78 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -228,17 +228,6 @@ module DesignManagement
project
end
- def immediately_before?(next_design)
- return false if next_design.relative_position <= relative_position
-
- interloper = self.class.on_issue(issue).where(
- "relative_position <@ int4range(?, ?, '()')",
- *[self, next_design].map(&:relative_position)
- )
-
- !interloper.exists?
- end
-
def notes_with_associations
notes.includes(:author)
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 671def16151..401dfc4cb02 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -294,10 +294,14 @@ class Event < ApplicationRecord
note? && target && target.for_merge_request?
end
- def project_snippet_note?
+ def snippet_note?
note? && target && target.for_snippet?
end
+ def project_snippet_note?
+ note? && target && target.for_project_snippet?
+ end
+
def personal_snippet_note?
note? && target && target.for_personal_snippet?
end
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
index 7dbc95f617a..354b1e0b6b9 100644
--- a/app/models/experiment.rb
+++ b/app/models/experiment.rb
@@ -10,6 +10,10 @@ class Experiment < ApplicationRecord
find_or_create_by!(name: name).record_user_and_group(user, group_type, context)
end
+ def self.add_group(name, variant:, group:)
+ find_or_create_by!(name: name).record_group_and_variant!(group, variant)
+ end
+
def self.record_conversion_event(name, user)
find_or_create_by!(name: name).record_conversion_event_for_user(user)
end
@@ -24,4 +28,8 @@ class Experiment < ApplicationRecord
def record_conversion_event_for_user(user)
experiment_users.find_by(user: user, converted_at: nil)&.touch(:converted_at)
end
+
+ def record_group_and_variant!(group, variant)
+ experiment_subjects.find_or_initialize_by(group: group).update!(variant: variant)
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 903d0154969..1eaa4499eb5 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -48,6 +48,7 @@ class Group < Namespace
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
+ has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
has_many :boards
@@ -75,7 +76,7 @@ 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
+ # debian_distributions and associated component_files 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
@@ -84,7 +85,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed
- validates :variables, variable_duplicates: true
+ validates :variables, nested_attributes_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
@@ -169,6 +170,15 @@ class Group < Namespace
where('NOT EXISTS (?)', services)
end
+ # This method can be used only if all groups have the same top-level
+ # group
+ def preset_root_ancestor_for(groups)
+ return groups if groups.size < 2
+
+ root = groups.first.root_ancestor
+ groups.drop(1).each { |group| group.root_ancestor = root }
+ end
+
private
def public_to_user_arel(user)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5da9f67f6ef..79d0229a281 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -132,7 +132,7 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
- scope :inc_relations_for_view, -> { includes(author: :status) }
+ scope :inc_relations_for_view, -> { includes(author: :status, assignees: :status) }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
index 7f3d552b3d9..d62f0eb170c 100644
--- a/app/models/issue_assignee.rb
+++ b/app/models/issue_assignee.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class IssueAssignee < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :issue_assignees
diff --git a/app/models/issue_link.rb b/app/models/issue_link.rb
index 5448ebdf50b..ba97874ed39 100644
--- a/app/models/issue_link.rb
+++ b/app/models/issue_link.rb
@@ -17,9 +17,11 @@ class IssueLink < ApplicationRecord
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
+ # we don't store is_blocked_by in the db but need it for displaying the relation
+ # from the target (used in IssueLink.inverse_link_type)
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
- enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
+ enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
def self.inverse_link_type(type)
type
diff --git a/app/models/label.rb b/app/models/label.rb
index 54129c7c7f3..7a31b095cfc 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -12,7 +12,7 @@ class Label < ApplicationRecord
cache_markdown_field :description, pipeline: :single_line
- DEFAULT_COLOR = '#428BCA'
+ DEFAULT_COLOR = '#6699cc'
default_value_for :color, DEFAULT_COLOR
diff --git a/app/models/license_template.rb b/app/models/license_template.rb
index bd24259984b..548066107c1 100644
--- a/app/models/license_template.rb
+++ b/app/models/license_template.rb
@@ -12,11 +12,12 @@ class LicenseTemplate
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]}xi.freeze
- attr_reader :key, :name, :category, :nickname, :url, :meta
+ attr_reader :key, :name, :project, :category, :nickname, :url, :meta
- def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {})
+ def initialize(key:, name:, project:, category:, content:, nickname: nil, url: nil, meta: {})
@key = key
@name = name
+ @project = project
@category = category
@content = content
@nickname = nickname
@@ -24,6 +25,10 @@ class LicenseTemplate
@meta = meta
end
+ def project_id
+ project&.id
+ end
+
def popular?
category == :Popular
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 64b8223a1f0..1374e8a814a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -50,12 +50,15 @@ class MergeRequest < ApplicationRecord
end
end
- has_many :merge_request_diffs
+ has_many :merge_request_diffs,
+ -> { regular }, inverse_of: :merge_request
has_many :merge_request_context_commits, inverse_of: :merge_request
has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files
has_one :merge_request_diff,
- -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
+ -> { regular.order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
+ has_one :merge_head_diff,
+ -> { merge_head }, inverse_of: :merge_request, class_name: 'MergeRequestDiff'
has_one :cleanup_schedule, inverse_of: :merge_request
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
@@ -270,8 +273,7 @@ class MergeRequest < ApplicationRecord
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
@@ -477,13 +479,17 @@ class MergeRequest < ApplicationRecord
# This is used after project import, to reset the IDs to the correct
# values. It is not intended to be called without having already scoped the
# relation.
+ #
+ # Only set `regular` merge request diffs as latest so `merge_head` diff
+ # won't be considered as `MergeRequest#merge_request_diff`.
def self.set_latest_merge_request_diff_ids!
- update = '
+ update = "
latest_merge_request_diff_id = (
SELECT MAX(id)
FROM merge_request_diffs
WHERE merge_requests.id = merge_request_diffs.merge_request_id
- )'.squish
+ AND merge_request_diffs.diff_type = #{MergeRequestDiff.diff_types[:regular]}
+ )".squish
self.each_batch do |batch|
batch.update_all(update)
@@ -915,6 +921,10 @@ class MergeRequest < ApplicationRecord
closed? && !source_project_missing? && source_branch_exists?
end
+ def can_be_closed?
+ opened?
+ end
+
def ensure_merge_request_diff
merge_request_diff.persisted? || create_merge_request_diff
end
@@ -922,7 +932,7 @@ class MergeRequest < ApplicationRecord
def create_merge_request_diff
fetch_ref!
- # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37435
+ # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377
Gitlab::GitalyClient.allow_n_plus_1_calls do
merge_request_diffs.create!
reload_merge_request_diff
@@ -996,7 +1006,7 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def diffable_merge_ref?
- open? && merge_ref_head.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?)
+ open? && merge_head_diff.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?)
end
# Returns boolean indicating the merge_status should be rechecked in order to
@@ -1478,8 +1488,26 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::GenerateCoverageReportsService)
end
+ def has_codequality_mr_diff_report?
+ return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project)
+
+ actual_head_pipeline&.has_codequality_mr_diff_report?
+ end
+
+ # TODO: this method and compare_test_reports use the same
+ # result type, which is handled by the controller's #reports_response.
+ # we should minimize mistakes by isolating the common parts.
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+ def find_codequality_mr_diff_reports
+ unless has_codequality_mr_diff_report?
+ return { status: :error, status_reason: 'This merge request does not have codequality mr diff reports' }
+ end
+
+ compare_reports(Ci::GenerateCodequalityMrDiffReportService)
+ end
+
def has_codequality_reports?
- return false unless Feature.enabled?(:codequality_mr_diff, project)
+ return false unless ::Gitlab::Ci::Features.display_codequality_backend_comparison?(project)
actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports)
end
@@ -1530,6 +1558,26 @@ class MergeRequest < ApplicationRecord
end || { status: :parsing }
end
+ def has_sast_reports?
+ !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports)
+ end
+
+ def has_secret_detection_reports?
+ !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports)
+ end
+
+ def compare_sast_reports(current_user)
+ return missing_report_error("SAST") unless has_sast_reports?
+
+ compare_reports(::Ci::CompareSecurityReportsService, current_user, 'sast')
+ end
+
+ def compare_secret_detection_reports(current_user)
+ return missing_report_error("secret detection") unless has_secret_detection_reports?
+
+ compare_reports(::Ci::CompareSecurityReportsService, current_user, 'secret_detection')
+ end
+
def calculate_reactive_cache(identifier, current_user_id = nil, report_type = nil, *args)
service_class = identifier.constantize
@@ -1760,7 +1808,7 @@ class MergeRequest < ApplicationRecord
end
def allows_reviewers?
- Feature.enabled?(:merge_request_reviewers, project, default_enabled: true)
+ true
end
def allows_multiple_reviewers?
@@ -1771,8 +1819,23 @@ class MergeRequest < ApplicationRecord
true
end
+ def find_reviewer(user)
+ merge_request_reviewers.find_by(user_id: user.id)
+ end
+
+ def enabled_reports
+ {
+ sast: report_type_enabled?(:sast),
+ secret_detection: report_type_enabled?(:secret_detection)
+ }
+ end
+
private
+ def missing_report_error(report_type)
+ { status: :error, status_reason: "This merge request does not have #{report_type} reports" }
+ end
+
def with_rebase_lock
if Feature.enabled?(:merge_request_rebase_nowait_lock, default_enabled: true)
with_retried_nowait_lock { yield }
@@ -1814,6 +1877,10 @@ class MergeRequest < ApplicationRecord
key = Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(project, self, format: :json)
Gitlab::EtagCaching::Store.new.touch(key)
end
+
+ def report_type_enabled?(report_type)
+ !!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type)
+ end
end
MergeRequest.prepend_if_ee('::EE::MergeRequest')
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index d3fe256fb1b..5c611da0684 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -5,12 +5,14 @@ class MergeRequest::Metrics < ApplicationRecord
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
belongs_to :latest_closed_by, class_name: 'User'
belongs_to :merged_by, class_name: 'User'
+ belongs_to :target_project, class_name: 'Project', inverse_of: :merge_requests
before_save :ensure_target_project_id
scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) }
scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) }
scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) }
+ scope :by_target_project, ->(project) { where(target_project_id: project) }
def self.time_to_merge_expression
Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))')
@@ -21,6 +23,12 @@ class MergeRequest::Metrics < ApplicationRecord
def ensure_target_project_id
self.target_project_id ||= merge_request.target_project_id
end
+
+ def self.total_time_to_merge
+ with_valid_time_to_merge
+ .pluck(time_to_merge_expression)
+ .first
+ end
end
MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index 59cc82cfaf5..e081a96dc10 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -12,6 +12,9 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
+ serialize :trailers, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
+ validates :trailers, json_schema: { filename: 'git_trailers' }
+
# Sort by committed date in descending order to ensure latest commits comes on the top
scope :order_by_committed_date_desc, -> { order('committed_date DESC') }
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index b89d1983ce3..6f15df1b70f 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MergeRequestContextCommitDiffFile < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
include Gitlab::EncodingHelper
include ShaAttribute
include DiffFile
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d23e66b9697..fb873ddbbab 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -32,6 +32,7 @@ class MergeRequestDiff < ApplicationRecord
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
validates :base_commit_sha, :head_commit_sha, :start_commit_sha, sha: true
+ validates :merge_request_id, uniqueness: { scope: :diff_type }, if: :merge_head?
state_machine :state, initial: :empty do
event :clean do
@@ -50,6 +51,11 @@ class MergeRequestDiff < ApplicationRecord
state :overflow_diff_lines_limit
end
+ enum diff_type: {
+ regular: 1,
+ merge_head: 2
+ }
+
scope :with_files, -> { without_states(:without_files, :empty) }
scope :viewable, -> { without_state(:empty) }
scope :by_commit_sha, ->(sha) do
@@ -72,6 +78,7 @@ class MergeRequestDiff < ApplicationRecord
join_condition = merge_requests[:id].eq(mr_diffs[:merge_request_id])
.and(mr_diffs[:id].not_eq(merge_requests[:latest_merge_request_diff_id]))
+ .and(mr_diffs[:diff_type].eq(diff_types[:regular]))
arel_join = mr_diffs.join(merge_requests).on(join_condition)
joins(arel_join.join_sources)
@@ -196,6 +203,10 @@ class MergeRequestDiff < ApplicationRecord
end
def set_as_latest_diff
+ # Don't set merge_head diff as latest so it won't get considered as the
+ # MergeRequest#merge_request_diff.
+ return if merge_head?
+
MergeRequest
.where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
.update_all(latest_merge_request_diff_id: self.id)
@@ -203,8 +214,16 @@ class MergeRequestDiff < ApplicationRecord
def ensure_commit_shas
self.start_commit_sha ||= merge_request.target_branch_sha
- self.head_commit_sha ||= merge_request.source_branch_sha
- self.base_commit_sha ||= find_base_sha
+
+ if merge_head? && merge_request.merge_ref_head.present?
+ diff_refs = merge_request.merge_ref_head.diff_refs
+
+ self.head_commit_sha ||= diff_refs.head_sha
+ self.base_commit_sha ||= diff_refs.base_sha
+ else
+ self.head_commit_sha ||= merge_request.source_branch_sha
+ self.base_commit_sha ||= find_base_sha
+ end
end
# Override head_commit_sha to keep compatibility with merge request diff
@@ -749,7 +768,7 @@ class MergeRequestDiff < ApplicationRecord
end
def sort_diffs?
- Feature.enabled?(:sort_diffs, project, default_enabled: false)
+ Feature.enabled?(:sort_diffs, project, default_enabled: :yaml)
end
end
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 9f6933d0879..259690ef308 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MergeRequestDiffCommit < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
include BulkInsertSafe
include ShaAttribute
include CachedCommit
@@ -10,6 +12,9 @@ class MergeRequestDiffCommit < ApplicationRecord
sha_attribute :sha
alias_attribute :id, :sha
+ serialize :trailers, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
+ validates :trailers, json_schema: { filename: 'git_trailers' }
+
# Deprecated; use `bulk_insert!` from `BulkInsertSafe` mixin instead.
# cf. https://gitlab.com/gitlab-org/gitlab/issues/207989 for progress
def self.create_bulk(merge_request_diff_id, commits)
@@ -23,10 +28,30 @@ class MergeRequestDiffCommit < ApplicationRecord
relative_order: index,
sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
- committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
+ committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]),
+ trailers: commit_hash.fetch(:trailers, {}).to_json
)
end
Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
+
+ def self.oldest_merge_request_id_per_commit(project_id, shas)
+ # This method is defined here and not on MergeRequest, otherwise the SHA
+ # values used in the WHERE below won't be encoded correctly.
+ select(['merge_request_diff_commits.sha AS sha', 'min(merge_requests.id) AS merge_request_id'])
+ .joins(:merge_request_diff)
+ .joins(
+ 'INNER JOIN merge_requests ' \
+ 'ON merge_requests.latest_merge_request_diff_id = merge_request_diffs.id'
+ )
+ .where(sha: shas)
+ .where(
+ merge_requests: {
+ target_project_id: project_id,
+ state_id: MergeRequest.available_states[:merged]
+ }
+ )
+ .group(:sha)
+ end
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 817e77bf12f..f3f64971426 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MergeRequestDiffFile < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
include BulkInsertSafe
include Gitlab::EncodingHelper
include DiffFile
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index c4e5274f832..4a1f31a7f39 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -1,6 +1,15 @@
# frozen_string_literal: true
class MergeRequestReviewer < ApplicationRecord
+ enum state: {
+ unreviewed: 0,
+ reviewed: 1
+ }
+
+ validates :state,
+ presence: true,
+ inclusion: { in: MergeRequestReviewer.states.keys }
+
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
end
diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb
index 2f2bf91e436..c6b5a967af9 100644
--- a/app/models/milestone_release.rb
+++ b/app/models/milestone_release.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MilestoneRelease < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
belongs_to :milestone
belongs_to :release
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 6f7b377ee52..3342fb1fce9 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -12,6 +12,7 @@ class Namespace < ApplicationRecord
include FromUnion
include Gitlab::Utils::StrongMemoize
include IgnorableColumns
+ include Namespaces::Traversal::Recursive
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
@@ -103,6 +104,10 @@ class Namespace < ApplicationRecord
)
end
+ # Make sure that the name is same as strong_memoize name in root_ancestor
+ # method
+ attr_writer :root_ancestor
+
class << self
def by_path(path)
find_by('lower(path) = :value', value: path.downcase)
@@ -243,50 +248,6 @@ class Namespace < ApplicationRecord
projects.with_shared_runners.any?
end
- # Returns all ancestors, self, and descendants of the current namespace.
- def self_and_hierarchy
- Gitlab::ObjectHierarchy
- .new(self.class.where(id: id))
- .all_objects
- end
-
- # Returns all the ancestors of the current namespaces.
- def ancestors
- return self.class.none unless parent_id
-
- Gitlab::ObjectHierarchy
- .new(self.class.where(id: parent_id))
- .base_and_ancestors
- end
-
- # returns all ancestors upto but excluding the given namespace
- # when no namespace is given, all ancestors upto the top are returned
- def ancestors_upto(top = nil, hierarchy_order: nil)
- Gitlab::ObjectHierarchy.new(self.class.where(id: id))
- .ancestors(upto: top, hierarchy_order: hierarchy_order)
- end
-
- def self_and_ancestors(hierarchy_order: nil)
- return self.class.where(id: id) unless parent_id
-
- Gitlab::ObjectHierarchy
- .new(self.class.where(id: id))
- .base_and_ancestors(hierarchy_order: hierarchy_order)
- end
-
- # Returns all the descendants of the current namespace.
- def descendants
- Gitlab::ObjectHierarchy
- .new(self.class.where(parent_id: id))
- .base_and_descendants
- end
-
- def self_and_descendants
- Gitlab::ObjectHierarchy
- .new(self.class.where(id: id))
- .base_and_descendants
- end
-
def user_ids_for_project_authorizations
[owner_id]
end
@@ -312,14 +273,6 @@ class Namespace < ApplicationRecord
parent_id.present? || parent.present?
end
- def root_ancestor
- return self if persisted? && parent_id.nil?
-
- strong_memoize(:root_ancestor) do
- self_and_ancestors.reorder(nil).find_by(parent_id: nil)
- end
- end
-
def subgroup?
has_parent?
end
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
new file mode 100644
index 00000000000..c46cc521735
--- /dev/null
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module Recursive
+ extend ActiveSupport::Concern
+
+ def root_ancestor
+ return self if persisted? && parent_id.nil?
+
+ strong_memoize(:root_ancestor) do
+ self_and_ancestors.reorder(nil).find_by(parent_id: nil)
+ end
+ end
+
+ # Returns all ancestors, self, and descendants of the current namespace.
+ def self_and_hierarchy
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .all_objects
+ end
+
+ # Returns all the ancestors of the current namespaces.
+ def ancestors
+ return self.class.none unless parent_id
+
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: parent_id))
+ .base_and_ancestors
+ end
+
+ # returns all ancestors upto but excluding the given namespace
+ # when no namespace is given, all ancestors upto the top are returned
+ def ancestors_upto(top = nil, hierarchy_order: nil)
+ Gitlab::ObjectHierarchy.new(self.class.where(id: id))
+ .ancestors(upto: top, hierarchy_order: hierarchy_order)
+ end
+
+ def self_and_ancestors(hierarchy_order: nil)
+ return self.class.where(id: id) unless parent_id
+
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .base_and_ancestors(hierarchy_order: hierarchy_order)
+ end
+
+ # Returns all the descendants of the current namespace.
+ def descendants
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(parent_id: id))
+ .base_and_descendants
+ end
+
+ def self_and_descendants
+ Gitlab::ObjectHierarchy
+ .new(self.class.where(id: id))
+ .base_and_descendants
+ end
+ end
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index 77f7726079c..fdc972d9726 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -259,6 +259,10 @@ class Note < ApplicationRecord
noteable_type == 'AlertManagement::Alert'
end
+ def for_project_snippet?
+ noteable.is_a?(ProjectSnippet)
+ end
+
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
@@ -542,7 +546,7 @@ class Note < ApplicationRecord
end
def skip_notification?
- review.present?
+ review.present? || author.blocked? || author.ghost?
end
private
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
index 419bbd595e9..8a444f8934e 100644
--- a/app/models/onboarding_progress.rb
+++ b/app/models/onboarding_progress.rb
@@ -22,6 +22,24 @@ class OnboardingProgress < ApplicationRecord
:repository_mirrored
].freeze
+ scope :incomplete_actions, -> (actions) do
+ Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) }
+ end
+
+ scope :completed_actions, -> (actions) do
+ Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) }
+ end
+
+ scope :completed_actions_with_latest_in_range, -> (actions, range) do
+ actions = Array(actions)
+ if actions.size == 1
+ where(column_name(actions[0]) => range)
+ else
+ action_columns = actions.map { |action| arel_table[column_name(action)] }
+ completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range))
+ end
+ end
+
class << self
def onboard(namespace)
return unless root_namespace?(namespace)
@@ -29,6 +47,10 @@ class OnboardingProgress < ApplicationRecord
safe_find_or_create_by(namespace: namespace)
end
+ def onboarding?(namespace)
+ where(namespace: namespace).any?
+ end
+
def register(namespace, action)
return unless root_namespace?(namespace) && ACTIONS.include?(action)
@@ -44,12 +66,12 @@ class OnboardingProgress < ApplicationRecord
where(namespace: namespace).where.not(action_column => nil).exists?
end
- private
-
def column_name(action)
:"#{action}_at"
end
+ private
+
def root_namespace?(namespace)
namespace && namespace.root?
end
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 442f9d36c43..be3f719ddb3 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -6,6 +6,7 @@ module Operations
include AtomicInternalId
include IidRoutes
include Limitable
+ include Referable
self.table_name = 'operations_feature_flags'
self.limit_scope = :project
@@ -65,6 +66,31 @@ module Operations
.reorder(:id)
.references(:operations_scopes)
end
+
+ def reference_prefix
+ '[feature_flag:'
+ end
+
+ def reference_pattern
+ @reference_pattern ||= %r{
+ #{Regexp.escape(reference_prefix)}(#{::Project.reference_pattern}\/)?(?<feature_flag>\d+)#{Regexp.escape(reference_postfix)}
+ }x
+ end
+
+ def link_reference_pattern
+ @link_reference_pattern ||= super("feature_flags", /(?<feature_flag>\d+)\/edit/)
+ end
+
+ def reference_postfix
+ ']'
+ end
+ end
+
+ def to_reference(from = nil, full: false)
+ project
+ .to_reference_base(from, full: full)
+ .then { |reference_base| reference_base.present? ? "#{reference_base}/" : nil }
+ .then { |reference_base| "#{self.class.reference_prefix}#{reference_base}#{iid}#{self.class.reference_postfix}" }
end
def related_issues(current_user, preload:)
diff --git a/app/models/packages/composer/cache_file.rb b/app/models/packages/composer/cache_file.rb
new file mode 100644
index 00000000000..ecd7596b989
--- /dev/null
+++ b/app/models/packages/composer/cache_file.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Packages
+ module Composer
+ class CacheFile < ApplicationRecord
+ include FileStoreMounter
+
+ self.table_name = 'packages_composer_cache_files'
+
+ mount_file_store_uploader Packages::Composer::CacheUploader
+
+ belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
+ belongs_to :namespace
+
+ validates :namespace, presence: true
+
+ scope :with_namespace, ->(namespace) { where(namespace: namespace) }
+ scope :with_sha, ->(sha) { where(file_sha256: sha) }
+ scope :expired, -> { where("delete_at <= ?", Time.current) }
+ scope :without_namespace, -> { where(namespace_id: nil) }
+ end
+ end
+end
diff --git a/app/models/packages/composer/metadatum.rb b/app/models/packages/composer/metadatum.rb
index 3026f5ea878..363858a3ed1 100644
--- a/app/models/packages/composer/metadatum.rb
+++ b/app/models/packages/composer/metadatum.rb
@@ -9,6 +9,9 @@ module Packages
belongs_to :package, -> { where(package_type: :composer) }, inverse_of: :composer_metadatum
validates :package, :target_sha, :composer_json, presence: true
+
+ scope :for_package, ->(name, project_id) { joins(:package).where(packages_packages: { name: name, project_id: project_id, package_type: Packages::Package.package_types[:composer] }) }
+ scope :locked_for_update, -> { lock('FOR UPDATE') }
end
end
end
diff --git a/app/models/packages/debian/group_component.rb b/app/models/packages/debian/group_component.rb
new file mode 100644
index 00000000000..81e02c363b0
--- /dev/null
+++ b/app/models/packages/debian/group_component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::GroupComponent < ApplicationRecord
+ def self.container_type
+ :group
+ end
+
+ include Packages::Debian::Component
+end
diff --git a/app/models/packages/debian/group_component_file.rb b/app/models/packages/debian/group_component_file.rb
new file mode 100644
index 00000000000..333aab044a4
--- /dev/null
+++ b/app/models/packages/debian/group_component_file.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::GroupComponentFile < ApplicationRecord
+ def self.container_type
+ :group
+ end
+
+ include Packages::Debian::ComponentFile
+end
diff --git a/app/models/packages/debian/project_component.rb b/app/models/packages/debian/project_component.rb
new file mode 100644
index 00000000000..98cd7fd589b
--- /dev/null
+++ b/app/models/packages/debian/project_component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::ProjectComponent < ApplicationRecord
+ def self.container_type
+ :project
+ end
+
+ include Packages::Debian::Component
+end
diff --git a/app/models/packages/debian/project_component_file.rb b/app/models/packages/debian/project_component_file.rb
new file mode 100644
index 00000000000..60ac29f91c2
--- /dev/null
+++ b/app/models/packages/debian/project_component_file.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::ProjectComponentFile < ApplicationRecord
+ def self.container_type
+ :project
+ end
+
+ include Packages::Debian::ComponentFile
+end
diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb
index a73c12d172d..22f1008b3b5 100644
--- a/app/models/packages/debian/project_distribution.rb
+++ b/app/models/packages/debian/project_distribution.rb
@@ -5,5 +5,8 @@ class Packages::Debian::ProjectDistribution < ApplicationRecord
:project
end
+ has_many :publications, class_name: 'Packages::Debian::Publication', inverse_of: :distribution, foreign_key: :distribution_id
+ has_many :packages, class_name: 'Packages::Package', through: :publications
+
include Packages::Debian::Distribution
end
diff --git a/app/models/packages/debian/publication.rb b/app/models/packages/debian/publication.rb
new file mode 100644
index 00000000000..93f5aa11d81
--- /dev/null
+++ b/app/models/packages/debian/publication.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class Packages::Debian::Publication < ApplicationRecord
+ belongs_to :package,
+ -> { where(package_type: :debian).where.not(version: nil) },
+ inverse_of: :debian_publication,
+ class_name: 'Packages::Package'
+ belongs_to :distribution,
+ inverse_of: :publications,
+ class_name: 'Packages::Debian::ProjectDistribution',
+ foreign_key: :distribution_id
+
+ validates :package, presence: true
+ validate :valid_debian_package_type
+
+ validates :distribution, presence: true
+
+ private
+
+ def valid_debian_package_type
+ return errors.add(:package, _('type must be Debian')) unless package&.debian?
+ return errors.add(:package, _('must be a Debian package')) unless package.debian_package?
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 2067a800ad5..391540634be 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -17,13 +17,18 @@ class Packages::Package < ApplicationRecord
has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum'
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
+ has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
has_many :build_infos, inverse_of: :package
has_many :pipelines, through: :build_infos
+ has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
+ has_one :debian_distribution, through: :debian_publication, source: :distribution, inverse_of: :packages, class_name: 'Packages::Debian::ProjectDistribution'
accepts_nested_attributes_for :conan_metadatum
+ accepts_nested_attributes_for :debian_publication
accepts_nested_attributes_for :maven_metadatum
delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan
+ delegate :codename, :suite, to: :debian_distribution, prefix: :debian_distribution
validates :project, presence: true
validates :name, presence: true
@@ -31,7 +36,8 @@ class Packages::Package < ApplicationRecord
validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: -> { conan? || generic? || debian? }
validates :name,
- uniqueness: { scope: %i[project_id version package_type] }, unless: :conan?
+ uniqueness: { scope: %i[project_id version package_type] }, unless: -> { conan? || debian_package? }
+ validate :unique_debian_package_name, if: :debian_package?
validate :valid_conan_package_recipe, if: :conan?
validate :valid_npm_package_name, if: :npm?
@@ -59,7 +65,11 @@ class Packages::Package < ApplicationRecord
if: :debian_package?
validate :forbidden_debian_changes, if: :debian?
- enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7, golang: 8, debian: 9 }
+ enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5,
+ composer: 6, generic: 7, golang: 8, debian: 9,
+ rubygems: 10 }
+
+ enum status: { default: 0, hidden: 1, processing: 2 }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
@@ -68,6 +78,8 @@ class Packages::Package < ApplicationRecord
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
+ scope :with_status, ->(status) { where(status: status) }
+ scope :displayable, -> { with_status(:default) }
scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
@@ -251,6 +263,18 @@ class Packages::Package < ApplicationRecord
end
end
+ def unique_debian_package_name
+ return unless debian_publication&.distribution
+
+ package_exists = debian_publication.distribution.packages
+ .with_name(name)
+ .with_version(version)
+ .id_not_in(id)
+ .exists?
+
+ errors.add(:base, _('Debian package already exists in Distribution')) if package_exists
+ end
+
def forbidden_debian_changes
return unless persisted?
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 389edaea392..9059f61b5de 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -55,10 +55,6 @@ class Packages::PackageFile < ApplicationRecord
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
end
- def local?
- file_store == ::Packages::PackageFileUploader::Store::LOCAL
- end
-
private
def update_size_from_file
diff --git a/app/models/packages/rubygems/metadatum.rb b/app/models/packages/rubygems/metadatum.rb
new file mode 100644
index 00000000000..42db1f3defc
--- /dev/null
+++ b/app/models/packages/rubygems/metadatum.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Packages
+ module Rubygems
+ class Metadatum < ApplicationRecord
+ self.table_name = 'packages_rubygems_metadata'
+ self.primary_key = :package_id
+
+ belongs_to :package, -> { where(package_type: :rubygems) }, inverse_of: :rubygems_metadatum
+
+ validates :package, presence: true
+
+ validate :rubygems_package_type
+
+ private
+
+ def rubygems_package_type
+ unless package&.rubygems?
+ errors.add(:base, _('Package type must be RubyGems'))
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 84928468ad1..c6781f8f6e3 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -4,6 +4,8 @@ module Pages
class LookupPath
include Gitlab::Utils::StrongMemoize
+ LegacyStorageDisabledError = Class.new(::StandardError)
+
def initialize(project, trim_prefix: nil, domain: nil)
@project = project
@domain = domain
@@ -24,7 +26,7 @@ module Pages
end
def source
- zip_source || file_source
+ zip_source || legacy_source
end
def prefix
@@ -52,6 +54,8 @@ module Pages
return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project)
+ return if deployment.migrated? && !Feature.enabled?(:pages_serve_from_migrated_zip, project)
+
global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
{
@@ -64,11 +68,17 @@ module Pages
}
end
- def file_source
+ def legacy_source
+ raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true)
+
{
type: 'file',
path: File.join(project.full_path, 'public/')
}
+ rescue LegacyStorageDisabledError => e
+ Gitlab::ErrorTracking.track_exception(e)
+
+ nil
end
end
end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 7e42b8e6ae2..90cb8253b52 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -17,9 +17,16 @@ module Pages
end
def lookup_paths
- projects.map do |project|
+ paths = projects.map do |project|
project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
- end.sort_by(&:prefix).reverse
+ end
+
+ # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524
+ # source can only be nil if pages_serve_from_legacy_storage FF is disabled
+ # we can remove this filtering once we remove legacy storage
+ paths = paths.select(&:source)
+
+ paths.sort_by(&:prefix).reverse
end
private
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index 61818a63764..d67a92af6af 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -2,14 +2,18 @@
# PagesDeployment stores a zip archive containing GitLab Pages web-site
class PagesDeployment < ApplicationRecord
+ include EachBatch
include FileStoreMounter
+ MIGRATED_FILE_NAME = "_migrated.zip"
+
attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store }
belongs_to :project, optional: false
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
scope :older_than, -> (id) { where('id < ?', id) }
+ scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
@@ -25,6 +29,10 @@ class PagesDeployment < ApplicationRecord
# this is to be adressed in https://gitlab.com/groups/gitlab-org/-/epics/589
end
+ def migrated?
+ file.filename == MIGRATED_FILE_NAME
+ end
+
private
def set_size
diff --git a/app/models/project.rb b/app/models/project.rb
index ec790798806..2b9b7dcf733 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -75,7 +75,7 @@ class Project < ApplicationRecord
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) do
- pick_repository_storage
+ Repository.pick_storage_shard
end
default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
@@ -117,7 +117,7 @@ class Project < ApplicationRecord
use_fast_destroy :build_trace_chunks
- after_destroy -> { run_after_commit { remove_pages } }
+ after_destroy -> { run_after_commit { legacy_remove_pages } }
after_destroy :remove_exports
after_validation :check_pending_delete
@@ -200,7 +200,7 @@ 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
+ # debian_distributions and associated component_files 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
@@ -218,6 +218,7 @@ class Project < ApplicationRecord
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
+ has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
@@ -410,7 +411,7 @@ 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 :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, :keep_latest_artifacts_available?, to: :ci_cd_settings
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
@@ -456,7 +457,7 @@ class Project < ApplicationRecord
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
- validates :variables, variable_duplicates: { scope: :environment_scope }
+ validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
@@ -836,8 +837,12 @@ class Project < ApplicationRecord
webide_pipelines.running_or_pending.for_user(user)
end
- def latest_pipeline_locked
- ci_keep_latest_artifact? ? :artifacts_locked : :unlocked
+ def default_pipeline_lock
+ if keep_latest_artifacts_available?
+ return :artifacts_locked
+ end
+
+ :unlocked
end
def autoclose_referenced_issues
@@ -1314,21 +1319,11 @@ class Project < ApplicationRecord
end
def external_issue_tracker
- if has_external_issue_tracker.nil?
- cache_has_external_issue_tracker
- end
+ cache_has_external_issue_tracker if has_external_issue_tracker.nil?
- if has_external_issue_tracker?
- strong_memoize(:external_issue_tracker) do
- services.external_issue_trackers.first
- end
- else
- nil
- end
- end
+ return unless has_external_issue_tracker?
- def cache_has_external_issue_tracker
- update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
+ @external_issue_tracker ||= services.external_issue_trackers.first
end
def external_references_supported?
@@ -1356,9 +1351,9 @@ class Project < ApplicationRecord
end
def disabled_services
- return %w(datadog alerts) unless Feature.enabled?(:datadog_ci_integration, self)
+ return %w(datadog) unless Feature.enabled?(:datadog_ci_integration, self)
- %w(alerts)
+ []
end
def find_or_initialize_service(name)
@@ -1797,16 +1792,16 @@ class Project < ApplicationRecord
.delete_all
end
- # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
+ # TODO: remove this method https://gitlab.com/gitlab-org/gitlab/-/issues/320775
# rubocop: disable CodeReuse/ServiceClass
- def remove_pages
+ def legacy_remove_pages
+ return unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
+
# Projects with a missing namespace cannot have their pages removed
return unless namespace
mark_pages_as_not_deployed unless destroyed?
- DestroyPagesDeploymentsWorker.perform_async(id)
-
# 1. We rename pages to temporary directory
# 2. We wait 5 minutes, due to NFS caching
# 3. We asynchronously remove pages with force
@@ -2532,6 +2527,11 @@ class Project < ApplicationRecord
tracing_setting&.external_url
end
+ override :git_garbage_collect_worker_klass
+ def git_garbage_collect_worker_klass
+ Projects::GitGarbageCollectWorker
+ end
+
private
def find_service(services, name)
@@ -2690,6 +2690,10 @@ class Project < ApplicationRecord
def cache_has_external_wiki
update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end
+
+ def cache_has_external_issue_tracker
+ update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
+ end
end
Project.prepend_if_ee('EE::Project')
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 366852d93bf..2c3f70654f8 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ProjectAuthorization < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
include FromUnion
belongs_to :user
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index e5fc481b035..31be0759cd0 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -21,6 +21,11 @@ class ProjectCiCdSetting < ApplicationRecord
super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true)
end
+ def keep_latest_artifacts_available?
+ # The project level feature can only be enabled when the feature is enabled instance wide
+ Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact?
+ end
+
private
def set_default_git_depth
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index 2bef0056732..58dbac9057f 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
+ extend SuppressCompositePrimaryKeyWarning
+
include EachBatch
self.primary_key = :project_id
@@ -11,4 +13,5 @@ class ProjectPagesMetadatum < ApplicationRecord
scope :deployed, -> { where(deployed: true) }
scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) }
+ scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end
diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb
deleted file mode 100644
index 4afce0dfe95..00000000000
--- a/app/models/project_services/alerts_service.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-# 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
- before_save :prevent_save
-
- def self.to_param
- 'alerts'
- end
-
- def self.supported_events
- %w()
- end
-
- private
-
- 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')
-
- # 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
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index c9e97efb4ac..1d50d5cf19e 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -11,11 +11,13 @@ class ChatNotificationService < Service
tag_push pipeline wiki_page deployment
].freeze
+ SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
+
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
default_value_for :category, 'chat'
- prop_accessor :webhook, :username, :channel, :branches_to_be_notified
+ prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
@@ -62,12 +64,16 @@ class ChatNotificationService < Service
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true }.freeze,
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }.freeze,
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
+ { type: 'text', name: 'labels_to_be_notified', placeholder: 'e.g. ~backend', help: 'Only supported for issue, merge request and note events.' }.freeze
].freeze
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
+
+ return unless notify_label?(data)
+
return unless webhook.present?
object_kind = data[:object_kind]
@@ -114,6 +120,22 @@ class ChatNotificationService < Service
private
+ def labels_to_be_notified_list
+ return [] if labels_to_be_notified.nil?
+
+ labels_to_be_notified.delete('~').split(',').map(&:strip)
+ end
+
+ def notify_label?(data)
+ return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
+
+ issue_labels = data.dig(:issue, :labels) || []
+ merge_request_labels = data.dig(:merge_request, :labels) || []
+ label_titles = (issue_labels + merge_request_labels).pluck(:title)
+
+ (labels_to_be_notified_list & label_titles).any?
+ end
+
# every notifier must implement this independently
def notify(message, opts)
raise NotImplementedError
diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb
index 6db446fc04c..8a6f4de540c 100644
--- a/app/models/project_services/confluence_service.rb
+++ b/app/models/project_services/confluence_service.rb
@@ -30,8 +30,8 @@ class ConfluenceService < Service
s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
end
- def detailed_description
- return unless project.wiki_enabled?
+ def help
+ return unless project&.wiki_enabled?
if activated?
wiki_url = project.wiki.web_url
diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb
index 3a742bfdcda..a48dea71645 100644
--- a/app/models/project_services/datadog_service.rb
+++ b/app/models/project_services/datadog_service.rb
@@ -12,14 +12,22 @@ class DatadogService < Service
prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env
- with_options presence: true, if: :activated? do
- validates :api_key, format: { with: /\A\w+\z/ }
- validates :datadog_site, format: { with: /\A[\w\.]+\z/ }, unless: :api_url
- validates :api_url, public_url: true, unless: :datadog_site
+ with_options if: :activated? do
+ validates :api_key, presence: true, format: { with: /\A\w+\z/ }
+ validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
+ validates :api_url, public_url: { allow_blank: true }
+ validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
+ validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
end
after_save :compose_service_hook, if: :activated?
+ def initialize_properties
+ super
+
+ self.datadog_site ||= DEFAULT_SITE
+ end
+
def self.supported_events
SUPPORTED_EVENTS
end
@@ -54,27 +62,37 @@ class DatadogService < Service
def fields
[
{
- type: 'text', name: 'datadog_site',
- placeholder: DEFAULT_SITE, default: DEFAULT_SITE,
+ type: 'text',
+ name: 'datadog_site',
+ placeholder: DEFAULT_SITE,
help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
required: false
},
{
- type: 'text', name: 'api_url', title: 'Custom URL',
+ type: 'text',
+ name: 'api_url',
+ title: 'API URL',
help: '(Advanced) Define the full URL for your Datadog site directly',
required: false
},
{
- type: 'password', name: 'api_key', title: 'API key',
+ type: 'password',
+ name: 'api_key',
+ title: 'API key',
help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
required: true
},
{
- type: 'text', name: 'datadog_service', title: 'Service', placeholder: 'gitlab-ci',
+ type: 'text',
+ name: 'datadog_service',
+ title: 'Service',
+ placeholder: 'gitlab-ci',
help: 'Name of this GitLab instance that all data will be tagged with'
},
{
- type: 'text', name: 'datadog_env', title: 'Env',
+ type: 'text',
+ name: 'datadog_env',
+ title: 'Env',
help: 'The environment tag that traces will be tagged with'
}
]
@@ -90,7 +108,7 @@ class DatadogService < Service
url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site)
url = URI.parse(url)
url.path = File.join(url.path || '/', api_key)
- query = { service: datadog_service, env: datadog_env }.compact
+ query = { service: datadog_service.presence, env: datadog_env.presence }.compact
url.query = query.to_query unless query.empty?
url.to_s
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index dafd3d095ec..5857d86f921 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Accessible as Project#external_issue_tracker
class JiraService < IssueTrackerService
extend ::Gitlab::Utils::Override
include Gitlab::Routing
@@ -30,7 +31,8 @@ class JiraService < IssueTrackerService
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab/issues/29404
- data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled, :vulnerabilities_enabled, :vulnerabilities_issuetype
+ data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled,
+ :vulnerabilities_enabled, :vulnerabilities_issuetype, :proxy_address, :proxy_port, :proxy_username, :proxy_password
before_update :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
@@ -157,11 +159,14 @@ class JiraService < IssueTrackerService
# support any events.
end
- def find_issue(issue_key)
- jira_request { client.Issue.find(issue_key) }
+ def find_issue(issue_key, rendered_fields: false)
+ options = {}
+ options = options.merge(expand: 'renderedFields') if rendered_fields
+
+ jira_request { client.Issue.find(issue_key, options) }
end
- def close_issue(entity, external_issue)
+ def close_issue(entity, external_issue, current_user)
issue = find_issue(external_issue.iid)
return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
@@ -178,6 +183,7 @@ class JiraService < IssueTrackerService
# if it is closed, so we don't have one comment for every commit.
issue = find_issue(issue.key) if transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
+ log_usage(:close_issue, current_user)
end
def create_cross_reference_note(mentioned, noteable, author)
@@ -213,7 +219,7 @@ class JiraService < IssueTrackerService
}
}
- add_comment(data, jira_issue)
+ add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
end
def valid_connection?
@@ -274,6 +280,12 @@ class JiraService < IssueTrackerService
end
end
+ def log_usage(action, user)
+ key = "i_ecosystem_jira_service_#{action}"
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id)
+ end
+
def add_issue_solved_comment(issue, commit_id, commit_url)
link_title = "Solved by commit #{commit_id}."
comment = "Issue solved with [#{commit_id}|#{commit_url}]."
diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb
index 00b6ab6a70f..6cbcb1550c1 100644
--- a/app/models/project_services/jira_tracker_data.rb
+++ b/app/models/project_services/jira_tracker_data.rb
@@ -7,6 +7,15 @@ class JiraTrackerData < ApplicationRecord
attr_encrypted :api_url, encryption_options
attr_encrypted :username, encryption_options
attr_encrypted :password, encryption_options
+ attr_encrypted :proxy_address, encryption_options
+ attr_encrypted :proxy_port, encryption_options
+ attr_encrypted :proxy_username, encryption_options
+ attr_encrypted :proxy_password, encryption_options
+
+ validates :proxy_address, length: { maximum: 2048 }
+ validates :proxy_port, length: { maximum: 5 }
+ validates :proxy_username, length: { maximum: 255 }
+ validates :proxy_password, length: { maximum: 255 }
enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment
end
diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb
deleted file mode 100644
index e55335d9aae..00000000000
--- a/app/models/project_services/mock_deployment_service.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-# Deprecated, to be deleted in 13.8 (https://gitlab.com/gitlab-org/gitlab/-/issues/293914)
-#
-# This was a class used only in development environment but became unusable
-# since DeploymentService was deleted
-class MockDeploymentService < Service
- default_value_for :category, 'deployment'
-
- def title
- 'Mock deployment'
- end
-
- def description
- 'Mock deployment service'
- end
-
- def self.to_param
- 'mock_deployment'
- end
-
- # No terminals support
- def terminals(environment)
- []
- end
-
- def self.supported_events
- %w()
- end
-
- def predefined_variables(project:, environment_name:)
- []
- end
-
- def can_test?
- false
- end
-end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index aca7eec3382..83ff0702b88 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -21,4 +21,4 @@ class ProjectSetting < ApplicationRecord
end
end
-ProjectSetting.prepend_if_ee('EE::ProjectSetting')
+ProjectSetting.prepend_ee_mod
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 7605ef54d5b..8c3dcaa7c0f 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -31,6 +31,7 @@ class ProjectStatistics < ApplicationRecord
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
+ scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
def total_repository_size
repository_size + lfs_objects_size
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index f28440f2444..ea51dca8a42 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -19,7 +19,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord
end
def check_access(user)
- if Feature.enabled?(:deploy_keys_on_protected_branches, project) && user && deploy_key.present?
+ if user && deploy_key.present?
return true if user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
end
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
index 6a32c480b04..2786ecb641a 100644
--- a/app/models/push_event_payload.rb
+++ b/app/models/push_event_payload.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class PushEventPayload < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
include ShaAttribute
belongs_to :event, inverse_of: :push_event_payload
diff --git a/app/models/readme_blob.rb b/app/models/readme_blob.rb
deleted file mode 100644
index 695b4e3ffe3..00000000000
--- a/app/models/readme_blob.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-class ReadmeBlob < SimpleDelegator
- include BlobActiveModel
-
- attr_reader :repository
-
- def initialize(blob, repository)
- @repository = repository
-
- super(blob)
- end
-
- def rendered_markup
- repository.rendered_readme
- end
-end
diff --git a/app/models/release.rb b/app/models/release.rb
index 2b82fdc37f6..60c2abcacb3 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -24,6 +24,7 @@ class Release < ApplicationRecord
validates :project, :tag, presence: true
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
+ validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
scope :sorted, -> { order(released_at: :desc) }
scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index c19448332f8..06a13194e1a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -39,11 +39,11 @@ class Repository
#
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
- CACHED_METHODS = %i(size commit_count rendered_readme readme_path contribution_guide
+ CACHED_METHODS = %i(size commit_count readme_path contribution_guide
changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
- has_visible_content? issue_template_names merge_request_template_names
+ has_visible_content? issue_template_names_by_category merge_request_template_names_by_category
user_defined_metrics_dashboard_paths xcode_project? has_ambiguous_refs?).freeze
# Methods that use cache_method but only memoize the value
@@ -53,15 +53,15 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
- readme: %i(rendered_readme readme_path),
+ readme: %i(readme_path),
changelog: :changelog,
license: %i(license_blob license_key license),
contributing: :contribution_guide,
gitignore: :gitignore,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar,
- issue_template: :issue_template_names,
- merge_request_template: :merge_request_template_names,
+ issue_template: :issue_template_names_by_category,
+ merge_request_template: :merge_request_template_names_by_category,
metrics_dashboard: :user_defined_metrics_dashboard_paths,
xcode_config: :xcode_project?
}.freeze
@@ -151,7 +151,8 @@ class Repository
all: !!opts[:all],
first_parent: !!opts[:first_parent],
order: opts[:order],
- literal_pathspec: opts.fetch(:literal_pathspec, true)
+ literal_pathspec: opts.fetch(:literal_pathspec, true),
+ trailers: opts[:trailers]
}
commits = Gitlab::Git::Commit.where(options)
@@ -497,23 +498,7 @@ class Repository
end
def blob_at(sha, path)
- blob = Blob.decorate(raw_repository.blob_at(sha, path), container)
-
- # Don't attempt to return a special result if there is no blob at all
- return unless blob
-
- # Don't attempt to return a special result if this can't be a README
- return blob unless Gitlab::FileDetector.type_of(blob.name) == :readme
-
- # Don't attempt to return a special result unless we're looking at HEAD
- return blob unless head_commit&.sha == sha
-
- case path
- when head_tree&.readme_path
- ReadmeBlob.new(blob, self)
- else
- blob
- end
+ Blob.decorate(raw_repository.blob_at(sha, path), container)
rescue Gitlab::Git::Repository::NoRepository
nil
end
@@ -587,15 +572,16 @@ class Repository
end
cache_method :avatar
- def issue_template_names
- Gitlab::Template::IssueTemplate.dropdown_names(project)
+ # store issue_template_names as hash
+ def issue_template_names_by_category
+ Gitlab::Template::IssueTemplate.repository_template_names(project)
end
- cache_method :issue_template_names, fallback: []
+ cache_method :issue_template_names_by_category, fallback: {}
- def merge_request_template_names
- Gitlab::Template::MergeRequestTemplate.dropdown_names(project)
+ def merge_request_template_names_by_category
+ Gitlab::Template::MergeRequestTemplate.repository_template_names(project)
end
- cache_method :merge_request_template_names, fallback: []
+ cache_method :merge_request_template_names_by_category, fallback: {}
def user_defined_metrics_dashboard_paths
Gitlab::Metrics::Dashboard::RepoDashboardFinder.list_dashboards(project)
@@ -611,15 +597,6 @@ class Repository
end
cache_method :readme_path
- def rendered_readme
- return unless readme
-
- context = { project: project }
-
- MarkupHelper.markup_unsafe(readme.name, readme.data, context)
- end
- cache_method :rendered_readme
-
def contribution_guide
file_on_head(:contributing)
end
@@ -1058,6 +1035,10 @@ class Repository
blob_data_at(sha, '.lfsconfig')
end
+ def changelog_config(ref = 'HEAD')
+ blob_data_at(ref, Gitlab::Changelog::Config::FILE_PATH)
+ end
+
def fetch_ref(source_repository, source_ref:, target_ref:)
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
@@ -1142,6 +1123,13 @@ class Repository
end
end
+ # Choose one of the available repository storage options based on a normalized weighted probability.
+ # We should always use the latest settings, to avoid picking a deleted shard.
+ def self.pick_storage_shard(expire: true)
+ Gitlab::CurrentSettings.expire_current_application_settings if expire
+ Gitlab::CurrentSettings.pick_repository_storage
+ end
+
private
# TODO Genericize finder, later split this on finders by Ref or Oid
diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb
index 6b1793a551f..b7a96211fb1 100644
--- a/app/models/repository_language.rb
+++ b/app/models/repository_language.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class RepositoryLanguage < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
belongs_to :project
belongs_to :programming_language
diff --git a/app/models/service.rb b/app/models/service.rb
index e5626462dd3..c49e0869b21 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -46,7 +46,6 @@ class Service < ApplicationRecord
after_initialize :initialize_properties
after_commit :reset_updated_properties
- after_commit :cache_project_has_external_issue_tracker
belongs_to :project, inverse_of: :services
belongs_to :group, inverse_of: :services
@@ -55,11 +54,11 @@ class Service < ApplicationRecord
validates :project_id, presence: true, unless: -> { template? || instance? || group_id }
validates :group_id, presence: true, unless: -> { template? || instance? || project_id }
validates :project_id, :group_id, absence: true, if: -> { template? || instance? }
- validates :type, uniqueness: { scope: :project_id }, unless: -> { template? || instance? || group_id }, on: :create
- validates :type, uniqueness: { scope: :group_id }, unless: -> { template? || instance? || project_id }
validates :type, presence: true
- validates :template, uniqueness: { scope: :type }, if: -> { template? }
- validates :instance, uniqueness: { scope: :type }, if: -> { instance? }
+ validates :type, uniqueness: { scope: :template }, if: :template?
+ validates :type, uniqueness: { scope: :instance }, if: :instance?
+ validates :type, uniqueness: { scope: :project_id }, if: :project_id?
+ validates :type, uniqueness: { scope: :group_id }, if: :group_id?
validate :validate_is_instance_or_template
validate :validate_belongs_to_project_or_group
@@ -438,10 +437,6 @@ class Service < ApplicationRecord
ProjectServiceWorker.perform_async(id, data)
end
- def external_issue_tracker?
- category == :issue_tracker && active?
- end
-
def external_wiki?
type == 'ExternalWikiService' && active?
end
@@ -461,12 +456,6 @@ class Service < ApplicationRecord
errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id
end
- def cache_project_has_external_issue_tracker
- if project && !project.destroyed?
- project.cache_has_external_issue_tracker
- end
- end
-
def valid_recipients?
activated? && !importing?
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index c4a7c5e25dc..ab8782ed87f 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -317,7 +317,7 @@ class Snippet < ApplicationRecord
end
def repository_storage
- snippet_repository&.shard_name || self.class.pick_repository_storage
+ snippet_repository&.shard_name || Repository.pick_storage_shard
end
# Repositories are created by default with the `master` branch.
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index efbbd86ae4a..eb7d465d585 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -22,7 +22,9 @@ module Terraform
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
scope :ordered_by_name, -> { order(:name) }
+ scope :with_name, -> (name) { where(name: name) }
+ validates :name, presence: true, uniqueness: { scope: :project_id }
validates :project_id, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index 19d708616fc..432ac5b6422 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -9,16 +9,14 @@ module Terraform
belongs_to :build, class_name: 'Ci::Build', optional: true, foreign_key: :ci_build_id
scope :ordered_by_version_desc, -> { order(version: :desc) }
+ scope :with_files_stored_locally, -> { where(file_store: Terraform::StateUploader::Store::LOCAL) }
+ scope :preload_state, -> { includes(:terraform_state) }
default_value_for(:file_store) { StateUploader.default_store }
mount_file_store_uploader StateUploader
delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
-
- def local?
- file_store == ObjectStorage::Store::LOCAL
- end
end
end
diff --git a/app/models/token_with_iv.rb b/app/models/token_with_iv.rb
new file mode 100644
index 00000000000..115f40b4a82
--- /dev/null
+++ b/app/models/token_with_iv.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# rubocop: todo Gitlab/NamespacedClass
+class TokenWithIv < ApplicationRecord
+ validates :hashed_token, presence: true
+ validates :iv, presence: true
+ validates :hashed_plaintext_token, presence: true
+
+ def self.find_by_hashed_token(value)
+ find_by(hashed_token: ::Digest::SHA256.digest(value))
+ end
+
+ def self.find_by_plaintext_token(value)
+ find_by(hashed_plaintext_token: ::Digest::SHA256.digest(value))
+ end
+
+ def self.find_nonce_by_hashed_token(value)
+ return unless table_exists?
+
+ token_record = find_by_hashed_token(value)
+ token_record&.iv
+ end
+end
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
index 1a389081913..65dc7a47533 100644
--- a/app/models/u2f_registration.rb
+++ b/app/models/u2f_registration.rb
@@ -4,11 +4,19 @@
class U2fRegistration < ApplicationRecord
belongs_to :user
- after_commit :schedule_webauthn_migration, on: :create
- after_commit :update_webauthn_registration, on: :update, if: :counter_changed?
- def schedule_webauthn_migration
- BackgroundMigrationWorker.perform_async('MigrateU2fWebauthn', [id, id])
+ after_create :create_webauthn_registration
+ after_update :update_webauthn_registration, if: :counter_changed?
+
+ def create_webauthn_registration
+ converter = Gitlab::Auth::U2fWebauthnConverter.new(self)
+ WebauthnRegistration.create!(converter.convert)
+ rescue StandardError => ex
+ Gitlab::AppJsonLogger.error(
+ event: 'u2f_migration',
+ error: ex.class.name,
+ backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(ex.backtrace),
+ message: "U2F to WebAuthn conversion failed")
end
def update_webauthn_registration
diff --git a/app/models/user.rb b/app/models/user.rb
index b4ec6064ff8..1f8b680c7e5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -116,6 +116,13 @@ class User < ApplicationRecord
has_one :user_synced_attributes_metadata, autosave: true
has_one :aws_role, class_name: 'Aws::Role'
+ # Followers
+ has_many :followed_users, foreign_key: :follower_id, class_name: 'Users::UserFollowUser'
+ has_many :followees, through: :followed_users
+
+ has_many :following_users, foreign_key: :followee_id, class_name: 'Users::UserFollowUser'
+ has_many :followers, through: :following_users
+
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember'
@@ -960,8 +967,8 @@ class User < ApplicationRecord
end
# rubocop: disable CodeReuse/ServiceClass
- def refresh_authorized_projects
- Users::RefreshAuthorizedProjectsService.new(self).execute
+ def refresh_authorized_projects(source: nil)
+ Users::RefreshAuthorizedProjectsService.new(self, source: source).execute
end
# rubocop: enable CodeReuse/ServiceClass
@@ -1442,6 +1449,29 @@ class User < ApplicationRecord
end
end
+ def following?(user)
+ self.followees.exists?(user.id)
+ end
+
+ def follow(user)
+ return false if self.id == user.id
+
+ begin
+ followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id)
+ self.followees.reset if followee.persisted?
+ rescue ActiveRecord::RecordNotUnique
+ false
+ end
+ end
+
+ def unfollow(user)
+ if Users::UserFollowUser.where(follower_id: self.id, followee_id: user.id).delete_all > 0
+ self.followees.reset
+ else
+ false
+ end
+ end
+
def manageable_namespaces
@manageable_namespaces ||= [namespace] + manageable_groups
end
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index ad5651f9439..d93fe611538 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -7,19 +7,19 @@ class UserCallout < ApplicationRecord
gke_cluster_integration: 1,
gcp_signup_offer: 2,
cluster_security_warning: 3,
- gold_trial: 4, # EE-only
- geo_enable_hashed_storage: 5, # EE-only
- geo_migrate_hashed_storage: 6, # EE-only
- canary_deployment: 7, # EE-only
- gold_trial_billings: 8, # EE-only
+ gold_trial: 4, # EE-only
+ geo_enable_hashed_storage: 5, # EE-only
+ geo_migrate_hashed_storage: 6, # EE-only
+ canary_deployment: 7, # EE-only
+ gold_trial_billings: 8, # EE-only
suggest_popover_dismissed: 9,
tabs_position_highlight: 10,
- threat_monitoring_info: 11, # EE-only
- account_recovery_regular_check: 12, # EE-only
+ threat_monitoring_info: 11, # EE-only
+ account_recovery_regular_check: 12, # EE-only
webhooks_moved: 13,
service_templates_deprecated: 14,
admin_integrations_moved: 15,
- web_ide_alert_dismissed: 16, # no longer in use
+ web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
personal_access_token_expiry: 21, # EE-only
@@ -27,7 +27,9 @@ class UserCallout < ApplicationRecord
customize_homepage: 23,
feature_flags_new_version: 24,
registration_enabled_callout: 25,
- new_user_signups_cap_reached: 26 # EE-only
+ new_user_signups_cap_reached: 26, # EE-only
+ unfinished_tag_cleanup_callout: 27,
+ eoa_bronze_plan_banner: 28 # EE-only
}
validates :user, presence: true
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 7e7a387d3d4..4c8cc5fc83a 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class UserInteractedProject < ApplicationRecord
+ extend SuppressCompositePrimaryKeyWarning
+
belongs_to :user
belongs_to :project
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
index 0e1ae0b7338..1c8634e47c3 100644
--- a/app/models/user_status.rb
+++ b/app/models/user_status.rb
@@ -7,6 +7,16 @@ class UserStatus < ApplicationRecord
DEFAULT_EMOJI = 'speech_balloon'
+ CLEAR_STATUS_QUICK_OPTIONS = {
+ '30_minutes' => 30.minutes,
+ '3_hours' => 3.hours,
+ '8_hours' => 8.hours,
+ '1_day' => 1.day,
+ '3_days' => 3.days,
+ '7_days' => 7.days,
+ '30_days' => 30.days
+ }.freeze
+
belongs_to :user
enum availability: { not_set: 0, busy: 1 }
@@ -15,5 +25,11 @@ class UserStatus < ApplicationRecord
validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names }
validates :message, length: { maximum: 100 }, allow_blank: true
+ scope :scheduled_for_cleanup, -> { where(arel_table[:clear_status_at].lteq(Time.current)) }
+
cache_markdown_field :message, pipeline: :emoji
+
+ def clear_status_after=(value)
+ self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
+ end
end
diff --git a/app/models/users/user_follow_user.rb b/app/models/users/user_follow_user.rb
new file mode 100644
index 00000000000..a94239a746c
--- /dev/null
+++ b/app/models/users/user_follow_user.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+module Users
+ class UserFollowUser < ApplicationRecord
+ belongs_to :follower, class_name: 'User'
+ belongs_to :followee, class_name: 'User'
+ end
+end
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index ab29afd0d08..7728c9c174e 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -12,17 +12,9 @@ class Vulnerability < ApplicationRecord
'[vulnerability:'
end
- def self.reference_prefix_escaped
- '[vulnerability&lbrack;'
- end
-
def self.reference_postfix
']'
end
-
- def self.reference_postfix_escaped
- '&rbrack;'
- end
end
-Vulnerability.prepend_if_ee('EE::Vulnerability')
+Vulnerability.prepend_ee_mod
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 11c10a61d18..45747c0b03c 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -104,7 +104,7 @@ class Wiki
end
def empty?
- list_pages(limit: 1).empty?
+ !repository_exists? || list_pages(limit: 1).empty?
end
def exists?
@@ -256,6 +256,15 @@ class Wiki
def after_post_receive
end
+ override :git_garbage_collect_worker_klass
+ def git_garbage_collect_worker_klass
+ Wikis::GitGarbageCollectWorker
+ end
+
+ def cleanup
+ @repository = nil
+ end
+
private
def commit_details(action, message = nil, title = nil)